garrytan/gstack · error · Error

token-registry already initialized with a different token; e

Error message

token-registry already initialized with a different token; embedders must call buildFetchHandler before any registry-mutating code path

What it means

Thrown by initRegistry when it is called a second time with a different root token. The registry is meant to be initialized exactly once per process; same-token re-init is a no-op, but a different token signals two embedders fighting over who owns the registry and would silently invalidate every scoped session token already minted.

Source

Thrown at browse/src/token-registry.ts:155

    return { allowed: false, retryAfterMs: Math.max(retryAfterMs, 100) };
  }

  bucket.count++;
  return { allowed: true };
}

// ─── Token Registry ─────────────────────────────────────────────

const tokens = new Map<string, TokenInfo>();
let rootToken: string = '';

export function initRegistry(root: string): void {
  // Idempotent re-init: same token is a no-op so embedders can call this
  // alongside any prior call without fighting. Different token after init
  // means a misconfigured caller — throw clearly rather than silently
  // invalidate every scoped token already issued.
  if (rootToken !== '' && rootToken !== root) {
    throw new Error(
      'token-registry already initialized with a different token; ' +
      'embedders must call buildFetchHandler before any registry-mutating code path'
    );
  }
  rootToken = root;
}

export function getRootToken(): string {
  return rootToken;
}

export function isRootToken(token: string): boolean {
  // Constant-time compare so a tunnel-reachable caller who can provoke an
  // isRootToken() call (e.g., via the 403 "root over tunnel" rejection path)
  // can't measure byte-by-byte string-compare timing to recover the token.
  // Compare UTF-8 byte lengths (not JS string length) before timingSafeEqual,
  // which throws on length-mismatched buffers. A multibyte input whose JS
  // string length matches rootToken but whose UTF-8 byte length differs must

View on GitHub (pinned to 94993f7401)

Solutions

  1. Ensure only one caller invokes buildFetchHandler / initRegistry per process — the one that owns the root token.
  2. In tests, isolate the token-registry module per test (jest.resetModules()) or factor the root token into a beforeAll fixture.
  3. If two services legitimately need different roots, run them in separate processes.
  4. Check for accidental double import (different import paths resolving to different module instances) that each call init.

Example fix

// before
buildFetchHandler({ rootToken: 'tok-a' });
buildFetchHandler({ rootToken: 'tok-b' }); // throws
// after
buildFetchHandler({ rootToken: 'tok-a' }); // single owner; later calls reuse tok-a
Defensive patterns

Strategy: validation

Validate before calling

import { getRootToken } from './token-registry';
function safeInitRegistry(root: string): void {
  const current = getRootToken();
  if (current && current !== root) {
    throw new Error(`registry already owns a different root token; refusing to re-init`);
  }
  initRegistry(root); // no-op when same token
}

Try / catch

try {
  buildFetchHandler({ rootToken });
} catch (e: any) {
  if (/token-registry already initialized/.test(e.message)) {
    // another embedder owns the registry; adopt its root instead
    rootToken = getRootToken();
  } else throw e;
}

Prevention

When it happens

Trigger: Two embedders in the same process both calling buildFetchHandler (which internally calls initRegistry) with different root tokens; a test harness that reuses the module across cases without resetting rootToken; hot-reload re-importing the token-registry module.

Common situations: Embedding the browse server inside a larger app that also has its own auth bootstrap; Jest/Vitest module caches that retain rootToken across tests; a worker pool where multiple workers share a single registry module.

Related errors


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