{"record":{"id":"b7132eb2765908c6","repo":"toeverything/AFFiNE","slug":"invalid-auth-state","errorCode":"invalid_auth_state","errorMessage":"Invalid auth state. You might start the auth progress from another device.","messagePattern":"Invalid auth state\\. You might start the auth progress from another device\\.","errorType":"exception","errorClass":"InvalidAuthState","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/auth/challenge-store.ts","lineNumber":26,"sourceCode":"export type AuthChallengePurpose =\n  | 'oauth_state'\n  | 'open_app_sign_in'\n  | 'auth_session_exchange'\n  | 'captcha'\n  | 'passkey_registration'\n  | 'passkey_authentication';\n\n@Injectable()\nexport class AuthChallengeStore {\n  constructor(private readonly cache: SessionCache) {}\n\n  async create<T>(\n    purpose: AuthChallengePurpose,\n    payload: T | ((token: string) => T),\n    ttlMs: number\n  ): Promise<string> {\n    if (!isValidCacheTtl(ttlMs)) {\n      throw new InvalidAuthState();\n    }\n\n    const token = randomUUID();\n    const value =\n      typeof payload === 'function'\n        ? (payload as (token: string) => T)(token)\n        : payload;\n    const stored = await this.cache.set(this.key(purpose, token), value, {\n      ttl: ttlMs,\n    });\n    if (!stored) {\n      throw new InvalidAuthState();\n    }\n    return token;\n  }\n\n  async get<T>(purpose: AuthChallengePurpose, token: string) {\n    return (await this.cache.get<T>(this.key(purpose, token))) ?? null;","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/challenge-store.ts#L8-L44","documentation":"Thrown by `AuthChallengeStore.create` when `isValidCacheTtl(ttlMs)` is false — i.e. `ttlMs` is not a positive safe integer. The challenge (oauth state, open-app sign-in code, captcha, passkey) cannot be stored with an invalid expiry, so creation aborts. HTTP 400.","triggerScenarios":"A caller passes `ttlMs` as 0, a negative number, a fractional value, `undefined`, `Infinity`, or a non-number (e.g. a string TTL read from config). Any new code path that constructs a challenge without validating the TTL will hit this.","commonSituations":"Config typo where a TTL is expressed as a string (`'60000'`) instead of a number, a refactor that drops the TTL argument, or a feature flag that yields `NaN`.","solutions":["Pass a positive integer milliseconds value, e.g. `60 * 1000`.","Validate the TTL at the call site before calling `create`: `Number.isSafeInteger(ttl) && ttl > 0`.","Audit the config source feeding the TTL (env var parsing) to ensure it produces a number."],"exampleFix":"// before\nawait challenges.create('captcha', payload, cfg.captchaTtl /* string '120000' */);\n\n// after\nconst ttl = Number(cfg.captchaTtl);\nif (!Number.isSafeInteger(ttl) || ttl <= 0) throw new Error('bad ttl');\nawait challenges.create('captcha', payload, ttl);","handlingStrategy":"validation","validationCode":"function validTtl(ttl: unknown): ttl is number {\n  return Number.isSafeInteger(ttl) && (ttl as number) > 0;\n}\nif (!validTtl(ttlMs)) throw new Error(`ttl must be a positive integer, got ${ttlMs}`);\nawait challenges.create('captcha', payload, ttlMs);","typeGuard":"function isValidCacheTtl(ttl: unknown): ttl is number {\n  return typeof ttl === 'number' && Number.isSafeInteger(ttl) && ttl > 0;\n}","tryCatchPattern":null,"preventionTips":["Always validate TTL is a positive safe integer before passing to `create`.","Parse config-derived TTLs with `Number(...)` and validate, never pass raw env strings.","Unit-test challenge creation with edge-case TTLs (0, negative, fractional, NaN)."],"tags":["validation","cache","ttl","programming-error"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}