{"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":"validation","errorClass":"InvalidAppConfigInput","httpStatus":400,"severity":"error","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/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/auth/signing-key.ts#L127-L163","documentation":"Signing-key rotation runs inside appConfig.mutate and asserts the currently active key still has the id the actor read before starting (active.id !== expectedActiveKeyId). If another rotation landed in between, the mutate callback aborts with InvalidAppConfigInput telling the admin to reload. This is an optimistic-concurrency guard so two rotations cannot interleave.","triggerScenarios":"Two admins (or two tabs/scripts) call rotate with the same expectedActiveKeyId; the first succeeds and demotes the key to retiring, the second's precondition now fails. Also a double-click that fires rotate twice with the same snapshot.","commonSituations":"Admin UI keeps a stale snapshot while another operator rotates first; the rotate button lacks single-flight and double-fires; an automation script races a manual rotation.","solutions":["Reload the signing-key snapshot (snapshotMetadata) and re-run rotate with the new active key id","Make the rotate button single-flight/disabled while a rotation is in flight","Serialize rotations: only one admin performs key rotation at a time (coordination or lock)"],"exampleFix":"// before\nawait signingKey.rotate(actorId, expectedActiveKeyId);\n\n// after\nfor (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    await signingKey.rotate(actorId, expectedActiveKeyId);\n    break;\n  } catch (e) {\n    if (e.code !== 'invalid_app_config_input') throw e;\n    const snapshot = await signingKey.snapshotMetadata();\n    expectedActiveKeyId = snapshot.activeKeyId; // someone else rotated first\n  }\n}","handlingStrategy":"retry","validationCode":"const snapshot = await signingKey.snapshotMetadata();\nconst expectedActiveKeyId = snapshot.keys.find(k => k.status === 'active')?.id;\nif (!expectedActiveKeyId) {\n  throw new Error('No active signing key — resolve key store state first');\n}\nawait signingKey.rotate(actorId, expectedActiveKeyId);","typeGuard":"function isInvalidAppConfigInput(e: unknown): boolean {\n  return (\n    typeof e === 'object' &&\n    e !== null &&\n    'code' in e &&\n    (e as { code?: string }).code === 'invalid_app_config_input'\n  );\n}","tryCatchPattern":"Catch invalid_app_config_input from rotate, re-read snapshotMetadata for the new active key id, and retry the rotation with that id — bounded to a few attempts before surfacing a concurrent-rotation conflict.","preventionTips":["Reload the key snapshot right before initiating rotation","Single-flight/disable the rotate button while a rotation is in flight","Coordinate so only one operator rotates keys at a time"],"tags":["auth","signing-key","concurrency","admin"],"backgroundTag":"optimistic-concurrency-conflict","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}