{"record":{"id":"e4f866228404b43d","repo":"toeverything/AFFiNE","slug":"invalid-app-config-input","errorCode":"invalid_app_config_input","errorMessage":"Invalid app config input: The active signing key changed. Reload and try again.","messagePattern":"Invalid app config input: The active signing key changed\\. Reload and try again\\.","errorType":"exception","errorClass":"InvalidAppConfigInput","httpStatus":400,"severity":"warning","filePath":"packages/backend/server/src/core/auth/signing-key.ts","lineNumber":145,"sourceCode":"    const replacement = this.generate('admin');\n    const now = new Date();\n    const verifyUntil = new Date(\n      now.getTime() +\n        (this.config.auth.token.accessTokenTtl + CLOCK_SKEW_SECONDS) * 1000\n    );\n    const updated = await this.models.appConfig.mutate(\n      SIGNING_KEY_STORE_ID,\n      actorId,\n      value => {\n        const current = this.parse(value);\n        const active = current.find(key => key.status === 'active');\n        if (!active) {\n          throw new Error(\n            'Auth session requires exactly one active signing key.'\n          );\n        }\n        if (active.id !== expectedActiveKeyId) {\n          throw new InvalidAppConfigInput({\n            message: 'The active signing key changed. Reload and try again.',\n          });\n        }\n        return [\n          ...current.map(key =>\n            key.status === 'active'\n              ? {\n                  ...key,\n                  status: 'retiring' as const,\n                  retiredAt: now.toISOString(),\n                  verifyUntil: verifyUntil.toISOString(),\n                }\n              : key\n          ),\n          replacement,\n        ];\n      }\n    );","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/signing-key.ts#L127-L163","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: rotate with a stale expected id\nawait signingKey.rotate(actorId, staleActiveKeyId, replacement);\n\n// after: reload on conflict, then retry once\ntry {\n  await signingKey.rotate(actorId, expectedActiveKeyId, replacement);\n} catch (e) {\n  if (e.code === 'invalid_app_config_input' && /active signing key changed/i.test(e.message)) {\n    const snap = await signingKey.snapshotMetadata();\n    await signingKey.rotate(actorId, snap.activeKeyId, replacement);\n  } else { throw e; }\n}","handlingStrategy":"retry","validationCode":"const snap = await signingKey.snapshotMetadata();\nconst active = snap.keys.find(k => k.status === 'active');\nif (!active) throw new Error('No active signing key to rotate from.');\nconst expectedActiveKeyId = active.id; // fresh, not stale\nawait signingKey.rotate(actorId, expectedActiveKeyId, replacement);","typeGuard":"function isActiveKey(k: { status: string } | undefined): k is { id: string; status: 'active' } {\n  return !!k && k.status === 'active';\n}","tryCatchPattern":"try {\n  await signingKey.rotate(actorId, expectedActiveKeyId, replacement);\n} catch (e) {\n  if (e.code === 'invalid_app_config_input' && /active signing key changed/i.test(e.message)) {\n    const snap = await signingKey.snapshotMetadata();\n    const active = snap.keys.find(k => k.status === 'active');\n    if (active) await signingKey.rotate(actorId, active.id, replacement); // one retry with fresh id\n    else throw e;\n  } else throw e;\n}","preventionTips":["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."],"tags":["auth","signing-key","concurrency","optimistic-lock","config"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}