toeverything/AFFiNE · error · 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

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.

Solutions

  1. Reload the signing-key snapshot (snapshotMetadata) and re-run rotate with the new active key id
  2. Make the rotate button single-flight/disabled while a rotation is in flight
  3. Serialize rotations: only one admin performs key rotation at a time (coordination or lock)

Example fix

// before
await signingKey.rotate(actorId, expectedActiveKeyId);

// after
for (let attempt = 0; attempt < 3; attempt++) {
  try {
    await signingKey.rotate(actorId, expectedActiveKeyId);
    break;
  } catch (e) {
    if (e.code !== 'invalid_app_config_input') throw e;
    const snapshot = await signingKey.snapshotMetadata();
    expectedActiveKeyId = snapshot.activeKeyId; // someone else rotated first
  }
}
Defensive patterns

Strategy: retry

Validate before calling

const snapshot = await signingKey.snapshotMetadata();
const expectedActiveKeyId = snapshot.keys.find(k => k.status === 'active')?.id;
if (!expectedActiveKeyId) {
  throw new Error('No active signing key — resolve key store state first');
}
await signingKey.rotate(actorId, expectedActiveKeyId);

Type guard

function isInvalidAppConfigInput(e: unknown): boolean {
  return (
    typeof e === 'object' &&
    e !== null &&
    'code' in e &&
    (e as { code?: string }).code === 'invalid_app_config_input'
  );
}

Try / catch

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.

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/e4f866228404b43d. Report an issue: GitHub.

Appendix: 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 b4c8548c09)