{"record":{"id":"e660a7edf2d51906","repo":"toeverything/AFFiNE","slug":"action-forbidden-e660a7","errorCode":"action_forbidden","errorMessage":"You are not allowed to perform this action.","messagePattern":"You are not allowed to perform this action\\.","errorType":"exception","errorClass":"ActionForbidden","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/core/auth/signing-key.ts","lineNumber":192,"sourceCode":"  async delete(actorId: string, keyId: string) {\n    const now = new Date();\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 key = current.find(key => key.id === keyId);\n        if (!key) {\n          throw new InvalidAppConfigInput({\n            message: 'Signing key does not exist.',\n          });\n        }\n        if (\n          key.status !== 'retiring' ||\n          !key.verifyUntil ||\n          new Date(key.verifyUntil) >= now\n        ) {\n          throw new ActionForbidden();\n        }\n        return current.filter(key => key.id !== keyId);\n      }\n    );\n    this.applyPersisted(updated.value);\n    this.event.emit('auth.signing_key.deleted', { actorId, keyId });\n    this.event.broadcast('auth.signing_keys.changed', {});\n    return this.snapshotMetadata();\n  }\n\n  private applyPersisted(value: unknown) {\n    const persisted = this.parse(value);\n    this.replaceSnapshot(persisted);\n  }\n\n  private replaceSnapshot(keys: unknown) {\n    const persisted = this.parse(keys);\n    this.snapshot = persisted.map(key => {","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/signing-key.ts#L174-L210","documentation":"Thrown inside SigningKeyService.delete's mutate callback when the target key exists but is not eligible for deletion: its status is not 'retiring', OR it has no verifyUntil, OR verifyUntil is still in the future (new Date(key.verifyUntil) >= now). Category 'action_forbidden', code 'action_forbidden'. Only retired keys past their verify-until cutoff may be purged; active or still-verifying keys must be kept so outstanding tokens remain validatable.","triggerScenarios":"Calling delete(actorId, keyId) (signing-key.ts:187-194) for a key that is 'active', already deleted/unknown status, or 'retiring' but whose verifyUntil cutoff has not yet elapsed (tokens signed by it may still be within their access-token TTL + clock skew).","commonSituations":"Trying to immediately delete a key right after rotating it, before the verify-until window (accessTokenTtl + CLOCK_SKEW_SECONDS) expires; an admin expecting 'retire' to mean 'instantly deletable'; attempting to delete the currently-active key.","solutions":["First rotate/retire the key so it enters 'retiring' status with a verifyUntil, then wait until now > verifyUntil before deleting.","Confirm the key status === 'retiring' and verifyUntil is set and in the past before issuing delete.","Schedule the cleanup (e.g. a delayed job) to run after verifyUntil rather than deleting manually."],"exampleFix":"// before: delete immediately after retire\nawait signingKey.retire(keyId);\nawait signingKey.delete(actorId, keyId);\n\n// after: delete only after the verify-until cutoff\nconst key = await getKey(keyId);\nif (key.status !== 'retiring' || !key.verifyUntil || Date.now() < +new Date(key.verifyUntil)) {\n  throw new Error('Key still within verify window; cannot delete yet');\n}\nawait signingKey.delete(actorId, keyId);","handlingStrategy":"validation","validationCode":"const key = (await signingKey.snapshotMetadata()).keys.find(k => k.id === keyId);\nif (!key) throw new InvalidAppConfigInput({ message: 'Signing key does not exist.' });\nconst now = Date.now();\nconst deletable = key.status === 'retiring' && !!key.verifyUntil && now > +new Date(key.verifyUntil);\nif (!deletable) throw new Error('Key is active or still within its verify window; cannot delete.');\nawait signingKey.delete(actorId, keyId);","typeGuard":"function isDeletable(k: { status: string; verifyUntil?: string }): boolean {\n  return k.status === 'retiring' && !!k.verifyUntil && Date.now() > +new Date(k.verifyUntil);\n}","tryCatchPattern":null,"preventionTips":["Only attempt delete after verifyUntil has elapsed (accessTokenTtl + clock skew).","Retire (rotate) a key before deleting; never delete an active key.","Schedule a delayed cleanup job rather than deleting immediately after retire."],"tags":["auth","signing-key","lifecycle","forbidden","key-rotation"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}