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 one

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Inspect FACTORY_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS and make every value a plain base64 string (a JSON string), e.g. {"v0":"<base64>"}.
  2. Generate valid keys with `openssl rand -base64 32` and base64-encode any raw key material before embedding it in the JSON.
  3. 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.
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/b25658ebea06388c. Report an issue: GitHub.