{"record":{"id":"b25658ebea06388c","repo":"mastra-ai/mastra","slug":"factory-credential-encryption-previous-keys-values","errorCode":null,"errorMessage":"FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS values must be base64 strings.","messagePattern":"FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS values must be base64 strings\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/web/src/mastra/index.ts","lineNumber":85,"sourceCode":"    );\n    return undefined;\n  }\n\n  const previousKeys: Record<string, unknown> = process.env.FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS\n    ? JSON.parse(process.env.FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS)\n    : {};\n  if (!previousKeys || Array.isArray(previousKeys) || typeof previousKeys !== 'object') {\n    throw new Error('FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS must be a JSON object of key ids to base64 keys.');\n  }\n\n  return createFactorySecretEncryption({\n    primary: {\n      id: process.env.FACTORY_CREDENTIAL_ENCRYPTION_KEY_ID?.trim() || 'v1',\n      key: decodeCredentialEncryptionKey('FACTORY_CREDENTIAL_ENCRYPTION_KEY', encodedKey),\n    },\n    previous: Object.entries(previousKeys).map(([id, value]) => {\n      if (typeof value !== 'string') {\n        throw new Error('FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS values must be base64 strings.');\n      }\n      return { id, key: decodeCredentialEncryptionKey('FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS', value) };\n    }),\n  });\n}\n\nfunction investigateIntakeIssue(context: FactoryStageRuleContext) {\n  return {\n    type: 'invokeSkill',\n    idempotencyKey: `${context.ingress.id}:factory-triage`,\n    role: 'triage',\n    skillName: 'factory-triage',\n    arguments: context.item.url ? `GitHub issue (${context.item.url})` : context.item.title,\n  } as const;\n}\n\n// Distributed pub/sub: when `REDIS_URL` is set, events (streams, workflows,\n// signals) ride Redis Streams so multiple web server processes can share one","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/web/src/mastra/index.ts#L67-L103","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nFACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS='{\"v0\": 12345}'\n// after\nFACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS='{\"v0\": \"dGhpcy1pcy1hLTMyLWJ5dGUta2V5LWJhc2U2NC1lbmNvZGVk...\"}'","handlingStrategy":"validation","validationCode":"const prev = process.env.FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS;\nif (prev) {\n  const parsed = JSON.parse(prev);\n  const bad = Object.entries(parsed).filter(([, v]) => typeof v !== 'string');\n  if (bad.length) throw new Error(`Invalid previous-key values for ids: ${bad.map(([k]) => k).join(', ')}`);\n  for (const v of Object.values(parsed)) {\n    if (Buffer.from(v, 'base64').byteLength !== 32) throw new Error('Previous key is not base64-encoded 32 bytes');\n  }\n}","typeGuard":"function isBase64KeyMap(v: unknown): v is Record<string, string> {\n  return !!v && typeof v === 'object' && !Array.isArray(v) &&\n    Object.values(v).every(x => typeof x === 'string' && Buffer.from(x as string, 'base64').byteLength === 32);\n}","tryCatchPattern":"try {\n  startServer();\n} catch (err) {\n  if (err instanceof Error && err.message.includes('FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS')) {\n    console.error('Malformed FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS — must be JSON object of id -> base64 32-byte key:', err.message);\n    process.exit(1);\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["configuration","environment-variables","encryption","startup"],"backgroundTag":"invalid-env-var-format","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}