toeverything/AFFiNE · warning · ActionForbidden
action_forbidden
action_forbidden
Error message
You are not allowed to perform this action.
What it means
SigningKeyService.delete only removes keys already in the 'retiring' state whose verifyUntil timestamp is in the past — that grace period is how long old tokens signed by the key must still verify. Deleting an active key, or a retiring key before verifyUntil, throws ActionForbidden.
Solutions
- Wait until verifyUntil has passed, then delete — schedule the cleanup for after the grace period
- If the key is still active, run rotate first so it moves to retiring with a verifyUntil
- Gate the delete action in the UI on status === 'retiring' and verifyUntil < now, and show the eligible-at time
Example fix
// before
await signingKey.delete(actorId, keyId);
// after
const key = snapshot.keys.find(k => k.id === keyId);
if (!key || key.status !== 'retiring' || new Date(key.verifyUntil) >= new Date()) {
throw new Error('Key can only be deleted after rotation and once verifyUntil has passed');
}
await signingKey.delete(actorId, keyId); Defensive patterns
Strategy: validation
Validate before calling
const key = snapshot.keys.find(k => k.id === keyId);
const deletable =
!!key &&
key.status === 'retiring' &&
!!key.verifyUntil &&
new Date(key.verifyUntil) < new Date();
if (!deletable) {
throw new Error('Key must be retiring with verifyUntil in the past before delete');
}
await signingKey.delete(actorId, keyId); Type guard
function isActionForbidden(e: unknown): boolean {
return (
typeof e === 'object' &&
e !== null &&
'code' in e &&
(e as { code?: string }).code === 'action_forbidden'
);
} Try / catch
Catch action_forbidden from delete, reload the snapshot, and show why the key is not deletable yet (active, or verifyUntil in the future) with the exact eligible-at time.
Prevention
- Learn the lifecycle: active -> retiring(verifyUntil) -> deletable
- Rotate first, then schedule deletion for after verifyUntil elapses
- Gate the delete action on status and verifyUntil in the admin UI
When it happens
Trigger: Calling delete on a key whose status is 'active' (rotation never moved it to retiring), or on a 'retiring' key whose verifyUntil is still in the future (or missing), i.e. trying to delete immediately after rotate without waiting out the grace period.
Common situations: Admin rotates then deletes in one sitting without waiting for verifyUntil; UI offers delete on keys not yet eligible; misunderstanding of the lifecycle active -> retiring(verifyUntil) -> deletable.
Related errors
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/e660a7edf2d51906.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/auth/signing-key.ts:192
async delete(actorId: string, keyId: string) {
const now = new Date();
const updated = await this.models.appConfig.mutate(
SIGNING_KEY_STORE_ID,
actorId,
value => {
const current = this.parse(value);
const key = current.find(key => key.id === keyId);
if (!key) {
throw new InvalidAppConfigInput({
message: 'Signing key does not exist.',
});
}
if (
key.status !== 'retiring' ||
!key.verifyUntil ||
new Date(key.verifyUntil) >= now
) {
throw new ActionForbidden();
}
return current.filter(key => key.id !== keyId);
}
);
this.applyPersisted(updated.value);
this.event.emit('auth.signing_key.deleted', { actorId, keyId });
this.event.broadcast('auth.signing_keys.changed', {});
return this.snapshotMetadata();
}
private applyPersisted(value: unknown) {
const persisted = this.parse(value);
this.replaceSnapshot(persisted);
}
private replaceSnapshot(keys: unknown) {
const persisted = this.parse(keys);
this.snapshot = persisted.map(key => {View on GitHub (pinned to b4c8548c09)