toeverything/AFFiNE · warning · InvalidAppConfigInput
invalid_app_config_input
invalid_app_config_input
Error message
Invalid app config input: The active signing key changed. Reload and try again.
What it means
Thrown inside SigningKeyService.rotate (within the appConfig.mutate callback) when the currently-active signing key's id does not equal expectedActiveKeyId. Category 'invalid_input', code 'invalid_app_config_input'. mutate provides the latest persisted value, so a mismatch means another rotation already advanced the active key since the caller last loaded state — the caller's optimistic precondition is stale.
Source
Thrown at packages/backend/server/src/core/auth/signing-key.ts:145
const replacement = this.generate('admin');
const now = new Date();
const verifyUntil = new Date(
now.getTime() +
(this.config.auth.token.accessTokenTtl + CLOCK_SKEW_SECONDS) * 1000
);
const updated = await this.models.appConfig.mutate(
SIGNING_KEY_STORE_ID,
actorId,
value => {
const current = this.parse(value);
const active = current.find(key => key.status === 'active');
if (!active) {
throw new Error(
'Auth session requires exactly one active signing key.'
);
}
if (active.id !== expectedActiveKeyId) {
throw new InvalidAppConfigInput({
message: 'The active signing key changed. Reload and try again.',
});
}
return [
...current.map(key =>
key.status === 'active'
? {
...key,
status: 'retiring' as const,
retiredAt: now.toISOString(),
verifyUntil: verifyUntil.toISOString(),
}
: key
),
replacement,
];
}
);View on GitHub (pinned to 26c515e050)
Solutions
- Reload the signing-key snapshot and retry the rotate with the new expectedActiveKeyId.
- Serialize key rotations (admin mutex / single concurrent actor) so only one rotate is in flight.
- Disable the 'rotate' UI action while a rotation is pending and refresh on completion.
Example fix
// before: rotate with a stale expected id
await signingKey.rotate(actorId, staleActiveKeyId, replacement);
// after: reload on conflict, then retry once
try {
await signingKey.rotate(actorId, expectedActiveKeyId, replacement);
} catch (e) {
if (e.code === 'invalid_app_config_input' && /active signing key changed/i.test(e.message)) {
const snap = await signingKey.snapshotMetadata();
await signingKey.rotate(actorId, snap.activeKeyId, replacement);
} else { throw e; }
} Defensive patterns
Strategy: retry
Validate before calling
const snap = await signingKey.snapshotMetadata();
const active = snap.keys.find(k => k.status === 'active');
if (!active) throw new Error('No active signing key to rotate from.');
const expectedActiveKeyId = active.id; // fresh, not stale
await signingKey.rotate(actorId, expectedActiveKeyId, replacement); Type guard
function isActiveKey(k: { status: string } | undefined): k is { id: string; status: 'active' } {
return !!k && k.status === 'active';
} Try / catch
try {
await signingKey.rotate(actorId, expectedActiveKeyId, replacement);
} catch (e) {
if (e.code === 'invalid_app_config_input' && /active signing key changed/i.test(e.message)) {
const snap = await signingKey.snapshotMetadata();
const active = snap.keys.find(k => k.status === 'active');
if (active) await signingKey.rotate(actorId, active.id, replacement); // one retry with fresh id
else throw e;
} else throw e;
} Prevention
- Always reload the signing-key snapshot immediately before rotating.
- Serialize rotations so only one is in flight (admin mutex).
- Disable the rotate button while a rotation is pending.
When it happens
Trigger: Two concurrent rotate calls (or a rotate racing with another mutation) on SIGNING_KEY_STORE_ID: the first succeeds and changes the active key; the second sees active.id !== its expectedActiveKeyId and throws (signing-key.ts:144-148).
Common situations: Two admins rotating keys at once; an automated key-rotation job overlapping a manual rotation; a UI that lets the user retry rotate without reloading the key list first.
Related errors
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/e4f866228404b43d.
Report an issue: GitHub.