{"record":{"id":"836beab3df34fed7","repo":"garrytan/gstack","slug":"ratelimit-must-be-0","errorCode":null,"errorMessage":"rateLimit must be >= 0","messagePattern":"rateLimit must be >= 0","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/token-registry.ts","lineNumber":209,"sourceCode":" */\nexport function createToken(opts: CreateTokenOptions): TokenInfo {\n  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,","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/token-registry.ts#L191-L227","documentation":"Thrown by createToken when opts.rateLimit is a negative number. The default is 10 requests per window; zero disables throttling for the token (still subject to global limits), but negative values are nonsensical and refused before the TokenInfo is built.","triggerScenarios":"Passing rateLimit: -1 explicitly; computing the limit from an env var that defaults to -1 on missing config; arithmetic that subtracts where it should add.","commonSituations":"Misreading the option as a 'grace period' or 'buffer'; migrating from a config schema that used -1 to mean 'unlimited' (this library uses null or 0 instead); env var coercion turning an empty string into NaN (which actually slips through — but a literal -1 hits this).","solutions":["Use 0 to disable per-token rate limiting, or a positive integer for requests/window.","Read env vars with a guarded parser: `const rl = Number(process.env.RL ?? 10); if (!Number.isFinite(rl) || rl < 0) throw ...`.","If your legacy config used -1 for unlimited, map it to 0 at the boundary before calling createToken."],"exampleFix":"// before\ncreateToken({ clientId: 'bot', rateLimit: -1 }); // 'unlimited' intent\n// after\ncreateToken({ clientId: 'bot', rateLimit: 0 }); // 0 = no per-token cap","handlingStrategy":"validation","validationCode":"function parseRateLimit(v: unknown): number {\n  const n = typeof v === 'string' ? Number(v) : v;\n  if (typeof n !== 'number' || !Number.isFinite(n) || n < 0) {\n    throw new Error('rateLimit must be >= 0');\n  }\n  return Math.floor(n);\n}","typeGuard":"const isNonNegativeInt = (v: unknown): v is number =>\n  typeof v === 'number' && Number.isFinite(v) && v >= 0 && Number.isInteger(v);","tryCatchPattern":"try {\n  return createToken({ clientId, rateLimit });\n} catch (e: any) {\n  if (/rateLimit must be >= 0/.test(e.message)) {\n    return createToken({ clientId, rateLimit: 0 }); // 0 = no per-token cap\n  }\n  throw e;\n}","preventionTips":["Coerce env-var inputs with Number() and validate before passing.","Map legacy 'unlimited' sentinels (-1) to 0 at the boundary.","Use a config schema (zod/yup) that rejects negative numbers.","Default to the built-in 10 when the input is absent."],"tags":["input-validation","tokens","rate-limiting","configuration"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}