mastra-ai/mastra · error
FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS values must be b
Error message
FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS values must be base64 strings.
What it means
This error is thrown by the credentialEncryption() setup in the mastracode web entry when FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS parses as a JSON object but one of its values is not a string. The env var maps key ids to base64-encoded 32-byte encryption keys used to decrypt secrets that were encrypted under a previous primary key (key rotation). Since the app cannot derive a decryption key from a non-string value, startup fails fast rather than silently losing access to encrypted credentials.
Source
Thrown at mastracode/web/src/mastra/index.ts:85
);
return undefined;
}
const previousKeys: Record<string, unknown> = process.env.FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS
? JSON.parse(process.env.FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS)
: {};
if (!previousKeys || Array.isArray(previousKeys) || typeof previousKeys !== 'object') {
throw new Error('FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS must be a JSON object of key ids to base64 keys.');
}
return createFactorySecretEncryption({
primary: {
id: process.env.FACTORY_CREDENTIAL_ENCRYPTION_KEY_ID?.trim() || 'v1',
key: decodeCredentialEncryptionKey('FACTORY_CREDENTIAL_ENCRYPTION_KEY', encodedKey),
},
previous: Object.entries(previousKeys).map(([id, value]) => {
if (typeof value !== 'string') {
throw new Error('FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS values must be base64 strings.');
}
return { id, key: decodeCredentialEncryptionKey('FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS', value) };
}),
});
}
function investigateIntakeIssue(context: FactoryStageRuleContext) {
return {
type: 'invokeSkill',
idempotencyKey: `${context.ingress.id}:factory-triage`,
role: 'triage',
skillName: 'factory-triage',
arguments: context.item.url ? `GitHub issue (${context.item.url})` : context.item.title,
} as const;
}
// Distributed pub/sub: when `REDIS_URL` is set, events (streams, workflows,
// signals) ride Redis Streams so multiple web server processes can share oneView on GitHub (pinned to 75dd419e61)
Solutions
- Inspect FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS and make every value a plain base64 string (a JSON string), e.g. {"v0":"<base64>"}.
- Generate valid keys with `openssl rand -base64 32` and base64-encode any raw key material before embedding it in the JSON.
- Validate the JSON locally with `node -e 'const o=JSON.parse(process.env.FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS); Object.values(o).forEach(v=>{if(typeof v!=="string") throw new Error("bad value")})'` before deploying.
- If a previous key is no longer needed and all secrets have been re-encrypted under the primary key, remove the stale entry instead of leaving a placeholder.
Example fix
// before
FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS='{"v0": 12345}'
// after
FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS='{"v0": "dGhpcy1pcy1hLTMyLWJ5dGUta2V5LWJhc2U2NC1lbmNvZGVk..."}' Defensive patterns
Strategy: validation
Validate before calling
const prev = process.env.FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS;
if (prev) {
const parsed = JSON.parse(prev);
const bad = Object.entries(parsed).filter(([, v]) => typeof v !== 'string');
if (bad.length) throw new Error(`Invalid previous-key values for ids: ${bad.map(([k]) => k).join(', ')}`);
for (const v of Object.values(parsed)) {
if (Buffer.from(v, 'base64').byteLength !== 32) throw new Error('Previous key is not base64-encoded 32 bytes');
}
} Type guard
function isBase64KeyMap(v: unknown): v is Record<string, string> {
return !!v && typeof v === 'object' && !Array.isArray(v) &&
Object.values(v).every(x => typeof x === 'string' && Buffer.from(x as string, 'base64').byteLength === 32);
} Try / catch
try {
startServer();
} catch (err) {
if (err instanceof Error && err.message.includes('FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS')) {
console.error('Malformed FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS — must be JSON object of id -> base64 32-byte key:', err.message);
process.exit(1);
}
throw err;
} Prevention
- Validate the JSON env var in CI or an entrypoint preflight script before booting the server.
- Always generate keys with `openssl rand -base64 32` — never paste raw or hex keys.
- Keep keys in a secrets manager that emits flat string values, not nested JSON.
- Document the expected shape `{"<keyId>": "<base64>"}` next to where ops edit the variable.
When it happens
Trigger: FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS is set to a JSON object whose value for some id is a number, boolean, null, or nested object/array instead of a base64 string, e.g. {"v0": 12345} or {"v0": {"key": "..."}}. This happens at server startup while constructing the factory secret encryption config.
Common situations: Hand-editing the JSON in a deploy dashboard and quoting mistakes or truncation; a secrets manager injecting a value as a nested structure; copy-pasting a key with surrounding braces; a typo dropping the base64 string while keeping the id.
Related errors
- DATABASE_URL is required outside local development and tests
- Cookie password must be at least 32 characters for SSO. Set
- Google client ID is required. Provide it in the options or s
- Cookie password must be at least 32 characters for SSO. Set
- Telegram installation secrets are encrypted at rest, but no
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/b25658ebea06388c.
Report an issue: GitHub.