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

  1. Use only: read, write, admin, meta, control (lowercase).
  2. If you need a privilege not in the set, request an admin scope and gate the action server-side.
  3. Cross-check against the ScopeCategory type exported from token-registry.ts.
  4. 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

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


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/0db121a13c4068f5. Report an issue: GitHub.