garrytan/gstack · error · Error
Invalid scope: ${s}. Valid: ${validScopes.join(', ')}
Error message
Invalid scope: ${s}. Valid: ${validScopes.join(', ')} What it means
Thrown by createToken when one of the scopes passed in opts.scopes is not in the closed set {read, write, admin, meta, control}. Scope names are case-sensitive and there is no aliasing, so a typo or a deprecated name will fail at minting time before the token is ever issued.
Source
Thrown at browse/src/token-registry.ts:206
/**
* Create a scoped session token (for direct minting via CLI or /token endpoint).
* Only callable by root token holder.
*/
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,View on GitHub (pinned to 94993f7401)
Solutions
- Use only: read, write, admin, meta, control (lowercase).
- If you need a privilege not in the set, request an admin scope and gate the action server-side.
- Cross-check against the ScopeCategory type exported from token-registry.ts.
- Add a unit test that asserts your scope list is a subset of the valid set.
Example fix
// before
createToken({ clientId: 'bot', scopes: ['Read', 'execute'] });
// after
createToken({ clientId: 'bot', scopes: ['read', 'admin'] }); Defensive patterns
Strategy: validation
Validate before calling
const VALID_SCOPES = ['read','write','admin','meta','control'] as const;
type Scope = typeof VALID_SCOPES[number];
function normalizeScopes(input: string[]): Scope[] {
for (const s of input) {
if (!VALID_SCOPES.includes(s as Scope)) {
throw new Error(`Invalid scope: ${s}. Valid: ${VALID_SCOPES.join(', ')}`);
}
}
return input as Scope[];
} Type guard
const isScope = (s: string): s is typeof VALID_SCOPES[number] => ['read','write','admin','meta','control'].includes(s as any);
Try / catch
try {
return createToken({ clientId, scopes });
} catch (e: any) {
if (/^Invalid scope:/.test(e.message)) {
// drop the unknown scope and retry with the safe subset
const safe = scopes.filter(isScope);
return createToken({ clientId, scopes: safe });
}
throw e;
} Prevention
- Type scopes against the ScopeCategory exported type so the compiler rejects typos.
- Lower-case and trim scope input from user-facing configs.
- Keep a unit test asserting your scope list ⊆ valid set.
- When upgrading browse, diff the VALID_SCOPES list for renames.
When it happens
Trigger: Calling createToken with scopes including a typo (`['rad']`), a deprecated name (`'execute'`), wrong case (`'Read'`), or a value intended for a different field.
Common situations: Upgrading from a version that previously accepted a different scope vocabulary; copy-pasting scope names from internal docs that drifted from the code; passing the clientId or domain list into the scopes field by mistake.
Related errors
- rateLimit must be >= 0
- expiresSeconds must be >= 0 or null
- tab-each rejected: subcommand "${innerRaw}" not allowed by y
- Chain rejected: subcommand "${c.rawName}" not allowed by you
- Invalid heatmap JSON. Expected object: '{"@e1":"green","@e3"
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/0db121a13c4068f5.
Report an issue: GitHub.