garrytan/gstack · error · Error
rateLimit must be >= 0
Error message
rateLimit must be >= 0
What it means
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.
Source
Thrown at browse/src/token-registry.ts:209
*/
export function createToken(opts: CreateTokenOptions): TokenInfo {
const {
clientId,
scopes = ['read', 'write'],
domains,
tabPolicy = 'own-only',
rateLimit = 10,
expiresSeconds = 86400, // 24h default
} = opts;
// Validate inputs
const validScopes: ScopeCategory[] = ['read', 'write', 'admin', 'meta', 'control'];
for (const s of scopes) {
if (!validScopes.includes(s as ScopeCategory)) {
throw new Error(`Invalid scope: ${s}. Valid: ${validScopes.join(', ')}`);
}
}
if (rateLimit < 0) throw new Error('rateLimit must be >= 0');
if (expiresSeconds !== null && expiresSeconds !== undefined && expiresSeconds < 0) {
throw new Error('expiresSeconds must be >= 0 or null');
}
const token = generateToken('gsk_sess_');
const now = new Date();
const expiresAt = expiresSeconds === null
? null
: new Date(now.getTime() + expiresSeconds * 1000).toISOString();
const info: TokenInfo = {
token,
clientId,
type: 'session',
scopes,
domains,
tabPolicy,
rateLimit,View on GitHub (pinned to 94993f7401)
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.
Example fix
// before
createToken({ clientId: 'bot', rateLimit: -1 }); // 'unlimited' intent
// after
createToken({ clientId: 'bot', rateLimit: 0 }); // 0 = no per-token cap Defensive patterns
Strategy: validation
Validate before calling
function parseRateLimit(v: unknown): number {
const n = typeof v === 'string' ? Number(v) : v;
if (typeof n !== 'number' || !Number.isFinite(n) || n < 0) {
throw new Error('rateLimit must be >= 0');
}
return Math.floor(n);
} Type guard
const isNonNegativeInt = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v >= 0 && Number.isInteger(v);
Try / catch
try {
return createToken({ clientId, rateLimit });
} catch (e: any) {
if (/rateLimit must be >= 0/.test(e.message)) {
return createToken({ clientId, rateLimit: 0 }); // 0 = no per-token cap
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- expiresSeconds must be >= 0 or null
- token-registry already initialized with a different token; e
- Invalid scope: ${s}. Valid: ${validScopes.join(', ')}
- commitSkill: tier "${opts.tier}" has no resolved path.
- commitSkill: a skill named "${opts.name}" already exists at
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/836beab3df34fed7.
Report an issue: GitHub.