mastra-ai/mastra · error
[FactorySecretEncryption] Duplicate key id "${previous.id}".
Error message
[FactorySecretEncryption] Duplicate key id "${previous.id}". What it means
createFactorySecretEncryption builds a Map of key id -> key material, seeded with the primary key, then inserts each previous key. This error is thrown at construction time when two keys in the config share the same id, because a duplicate id would silently overwrite one key's material and make values encrypted under it undecryptable.
Source
Thrown at mastracode/factory/src/secret-encryption.ts:73
typeof (parsed as SecretEnvelopeV1).keyId !== 'string' ||
typeof (parsed as SecretEnvelopeV1).iv !== 'string' ||
typeof (parsed as SecretEnvelopeV1).ciphertext !== 'string' ||
typeof (parsed as SecretEnvelopeV1).tag !== 'string'
) {
throw new Error('[FactorySecretEncryption] Invalid encrypted value.');
}
return parsed as SecretEnvelopeV1;
}
/**
* Creates a versioned AES-256-GCM encryptor. The primary key is used for new
* writes; previous keys remain decrypt-only until stored values are rotated.
*/
export function createFactorySecretEncryption(config: FactorySecretEncryptionConfig): FactorySecretEncryption {
const primaryKey = validateKey(config.primary);
const keys = new Map<string, Buffer>([[config.primary.id, primaryKey]]);
for (const previous of config.previous ?? []) {
if (keys.has(previous.id)) throw new Error(`[FactorySecretEncryption] Duplicate key id "${previous.id}".`);
keys.set(previous.id, validateKey(previous));
}
return {
async encrypt<T>(value: T): Promise<string> {
const iv = randomBytes(IV_BYTES);
const cipher = createCipheriv(ALGORITHM, primaryKey, iv);
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);
const envelope: SecretEnvelopeV1 = {
keyId: config.primary.id,
iv: iv.toString('base64url'),
ciphertext: ciphertext.toString('base64url'),
tag: cipher.getAuthTag().toString('base64url'),
};
return `${ENVELOPE_PREFIX}${Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64url')}`;
},
async decrypt<T>(value: unknown): Promise<DecryptedFactorySecret<T>> {View on GitHub (pinned to 75dd419e61)
Solutions
- Assign a unique id to each key in config (primary plus every previous key) and redeploy.
- Audit how config is assembled (env vars, config files, DI) to find where the same key/id is injected twice; deduplicate before construction.
- If the same key material genuinely appears twice under different purposes, keep one entry — duplicate material does not need two ids.
- List the ids before constructing (e.g. [primary, ...previous].map(k => k.id) and check for duplicates) to fail with a clearer message in your own config layer.
Example fix
// before
createFactorySecretEncryption({
primary: { id: 'k1', key: primaryKey },
previous: [{ id: 'k1', key: oldKey }], // duplicate id
});
// after
createFactorySecretEncryption({
primary: { id: 'k1', key: primaryKey },
previous: [{ id: 'k0', key: oldKey }],
}); Defensive patterns
Strategy: validation
Validate before calling
function assertUniqueKeyIds(config: { primary: { id: string }; previous?: { id: string }[] }) {
const ids = [config.primary.id, ...(config.previous ?? []).map(k => k.id)];
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
if (dupes.length) throw new Error(`Duplicate secret key ids: ${dupes.join(', ')}`);
} Try / catch
let encryption: FactorySecretEncryption;
try {
encryption = createFactorySecretEncryption(keyConfig);
} catch (err) {
if (err instanceof Error && err.message.includes('Duplicate key id')) {
throw new Error(`Key configuration error: ${err.message} — check env var merging for secret keys`);
}
throw err;
} Prevention
- Generate a fresh unique id (e.g. uuid or monotonic k1/k2...) for every key at creation time.
- Deduplicate and validate key config at the configuration-loading layer before constructing the encryptor.
- Add a unit test that constructs the encryptor from your real config shape to catch id collisions in CI.
- During rotation, append new keys to `previous` with their original ids — never rename old keys.
When it happens
Trigger: Calling createFactorySecretEncryption({ primary, previous }) where primary.id equals some previous[n].id, or two entries in previous share the same id string.
Common situations: Copy-pasting a key entry and forgetting to change its id, generating a previous key from the same config object as primary, environment config merging two sources that both define the same key id, or a typo reusing an old rotation key's id.
Related errors
- Google RBAC roleMapping is required.
- Cookie password must be at least 32 characters. Set OKTA_COO
- heartbeatMs must be a finite number no greater than ${MAX_TI
- Expected a PEM-encoded public key or certificate string for
- terminationGraceMs must be greater than zero.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/6cd6c0cfdc2dd162.
Report an issue: GitHub.