{"record":{"id":"46216ef12dbb17bc","repo":"garrytan/gstack","slug":"expiresseconds-must-be-0-or-null","errorCode":null,"errorMessage":"expiresSeconds must be >= 0 or null","messagePattern":"expiresSeconds must be >= 0 or null","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/token-registry.ts","lineNumber":211,"sourceCode":"  const {\n    clientId,\n    scopes = ['read', 'write'],\n    domains,\n    tabPolicy = 'own-only',\n    rateLimit = 10,\n    expiresSeconds = 86400, // 24h default\n  } = opts;\n\n  // Validate inputs\n  const validScopes: ScopeCategory[] = ['read', 'write', 'admin', 'meta', 'control'];\n  for (const s of scopes) {\n    if (!validScopes.includes(s as ScopeCategory)) {\n      throw new Error(`Invalid scope: ${s}. Valid: ${validScopes.join(', ')}`);\n    }\n  }\n  if (rateLimit < 0) throw new Error('rateLimit must be >= 0');\n  if (expiresSeconds !== null && expiresSeconds !== undefined && expiresSeconds < 0) {\n    throw new Error('expiresSeconds must be >= 0 or null');\n  }\n\n  const token = generateToken('gsk_sess_');\n  const now = new Date();\n  const expiresAt = expiresSeconds === null\n    ? null\n    : new Date(now.getTime() + expiresSeconds * 1000).toISOString();\n\n  const info: TokenInfo = {\n    token,\n    clientId,\n    type: 'session',\n    scopes,\n    domains,\n    tabPolicy,\n    rateLimit,\n    expiresAt,\n    createdAt: now.toISOString(),","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/token-registry.ts#L193-L229","documentation":"Thrown by createToken when opts.expiresSeconds is a negative number. The field accepts a positive integer (seconds until expiry), 0 (expired immediately), or null (never expires). Negative TTLs are refused because they would mint a token whose expiry is in the past without the caller realizing.","triggerScenarios":"Passing expiresSeconds: -60 explicitly; subtracting a renewal window from a budget that has already elapsed; coercing a malformed env var into a negative integer.","commonSituations":"A 'refresh' code path that computes `remaining - buffer` and the buffer exceeds remaining; migrating from a config that used negative values to mean 'already expired, force re-login'; timezone arithmetic producing a negative delta.","solutions":["Use null for a non-expiring token, 0 for immediate expiry, or a positive number of seconds (86400 = 1 day is the default).","Clamp computed TTLs: `Math.max(0, ttl)` before passing to createToken.","If you intend 'mint expired to test revocation', call createToken with a positive value then revoke it instead."],"exampleFix":"// before\ncreateToken({ clientId: 'bot', expiresSeconds: remaining - buffer }); // can go negative\n// after\ncreateToken({ clientId: 'bot', expiresSeconds: Math.max(0, remaining - buffer) });","handlingStrategy":"validation","validationCode":"function parseTtl(v: unknown): number | null {\n  if (v === null || v === undefined) return null;\n  const n = typeof v === 'string' ? Number(v) : v;\n  if (typeof n !== 'number' || !Number.isFinite(n) || n < 0) {\n    throw new Error('expiresSeconds must be >= 0 or null');\n  }\n  return Math.floor(n);\n}","typeGuard":"const isTtl = (v: unknown): v is number | null =>\n  v === null || (typeof v === 'number' && Number.isFinite(v) && v >= 0);","tryCatchPattern":"try {\n  return createToken({ clientId, expiresSeconds: ttl });\n} catch (e: any) {\n  if (/expiresSeconds must be >= 0/.test(e.message)) {\n    return createToken({ clientId, expiresSeconds: Math.max(0, ttl) });\n  }\n  throw e;\n}","preventionTips":["Clamp computed TTLs with Math.max(0, n) before minting.","Use null for non-expiring tokens, not a magic negative number.","Validate env-var TTLs with a schema that rejects negatives.","Unit-test the renewal path that subtracts buffers from a budget."],"tags":["input-validation","tokens","ttl","configuration","time"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}