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

  1. Reload the signing-key snapshot and retry the rotate with the new expectedActiveKeyId.
  2. Serialize key rotations (admin mutex / single concurrent actor) so only one rotate is in flight.
  3. 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

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.