{"record":{"id":"726c2923094918c3","repo":"garrytan/gstack","slug":"token-registry-already-initialized-with-a-differen","errorCode":null,"errorMessage":"token-registry already initialized with a different token; embedders must call buildFetchHandler before any registry-mutating code path","messagePattern":"token-registry already initialized with a different token; embedders must call buildFetchHandler before any registry-mutating code path","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/token-registry.ts","lineNumber":155,"sourceCode":"    return { allowed: false, retryAfterMs: Math.max(retryAfterMs, 100) };\n  }\n\n  bucket.count++;\n  return { allowed: true };\n}\n\n// ─── Token Registry ─────────────────────────────────────────────\n\nconst tokens = new Map<string, TokenInfo>();\nlet rootToken: string = '';\n\nexport function initRegistry(root: string): void {\n  // Idempotent re-init: same token is a no-op so embedders can call this\n  // alongside any prior call without fighting. Different token after init\n  // means a misconfigured caller — throw clearly rather than silently\n  // invalidate every scoped token already issued.\n  if (rootToken !== '' && rootToken !== root) {\n    throw new Error(\n      'token-registry already initialized with a different token; ' +\n      'embedders must call buildFetchHandler before any registry-mutating code path'\n    );\n  }\n  rootToken = root;\n}\n\nexport function getRootToken(): string {\n  return rootToken;\n}\n\nexport function isRootToken(token: string): boolean {\n  // Constant-time compare so a tunnel-reachable caller who can provoke an\n  // isRootToken() call (e.g., via the 403 \"root over tunnel\" rejection path)\n  // can't measure byte-by-byte string-compare timing to recover the token.\n  // Compare UTF-8 byte lengths (not JS string length) before timingSafeEqual,\n  // which throws on length-mismatched buffers. A multibyte input whose JS\n  // string length matches rootToken but whose UTF-8 byte length differs must","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/token-registry.ts#L137-L173","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure only one caller invokes buildFetchHandler / initRegistry per process — the one that owns the root token.","In tests, isolate the token-registry module per test (jest.resetModules()) or factor the root token into a beforeAll fixture.","If two services legitimately need different roots, run them in separate processes.","Check for accidental double import (different import paths resolving to different module instances) that each call init."],"exampleFix":"// before\nbuildFetchHandler({ rootToken: 'tok-a' });\nbuildFetchHandler({ rootToken: 'tok-b' }); // throws\n// after\nbuildFetchHandler({ rootToken: 'tok-a' }); // single owner; later calls reuse tok-a","handlingStrategy":"validation","validationCode":"import { getRootToken } from './token-registry';\nfunction safeInitRegistry(root: string): void {\n  const current = getRootToken();\n  if (current && current !== root) {\n    throw new Error(`registry already owns a different root token; refusing to re-init`);\n  }\n  initRegistry(root); // no-op when same token\n}","typeGuard":null,"tryCatchPattern":"try {\n  buildFetchHandler({ rootToken });\n} catch (e: any) {\n  if (/token-registry already initialized/.test(e.message)) {\n    // another embedder owns the registry; adopt its root instead\n    rootToken = getRootToken();\n  } else throw e;\n}","preventionTips":["Centralize buildFetchHandler / initRegistry in a single bootstrap path.","In tests, call jest.resetModules() between cases that touch the registry.","Run services that need different root tokens in separate processes.","Read the existing root with getRootToken() before initializing."],"tags":["initialization","configuration","ordering","tokens","embedder"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}