@cv/auth-bearer

Shared SHA-256-hashed bearer token registry for every CareerVector + JobCache worker that needs service / agent / CI authentication. Stateless, env-var backed, scope-gated. Designed to be the canonical service-auth primitive across the monorepo, replacing the four bearer-dialect implementations that existed before extraction.

Why this exists

Before extraction, four different bearer-auth implementations lived in the codebase:

Where Mechanism
lib/qa-platform/src/api-core/middleware/auth.ts SHA-256-hashed registry, scopes array
lib/qa-platform/src/mcp-core/middleware/auth.ts SHA-256-hashed registry, single scope per entry
ops/api/src/lib/auth.ts CF-Access-JWT presence check + bearer + cookie
lib/server/src/jobcache-control-auth.ts Plain string comparison
jobcache/ingest/src/jobs/heavy-worker-app.ts Plain string comparison

One library, one model.

Storage model

Each consumer worker has an env var (name varies — QA_TOKENS_JSON, QA_MCP_TOKENS_JSON, etc.) holding a JSON object mapping the SHA-256 hex digest of each issued token to its caller metadata:

{
  "<64-char-sha256-hex>": { "label": "ci-ingest@circleci", "scopes": ["ci-ingest"] },
  "<64-char-sha256-hex>": { "label": "admin@julian",       "scopes": ["admin"] }
}

Tokens themselves never live on the worker side — only their hashes do. A leaked wrangler secret does not yield plaintext tokens.

For backward compatibility, the parser also accepts two legacy shapes that existed in qa-platform pre-extraction:

  • { token_name, scopes: [...] } (api-core flavour)
  • { label, scope: "..." } (mcp-core flavour, single-scope)

Both normalise to the canonical { label, scopes: [...] } shape internally, so no env-var rotation is required when a consumer migrates.

Provisioning a token

# Generate a random opaque token (32 bytes base32):
node -e 'console.log("cv_" + crypto.randomBytes(20).toString("base32"))'
# → cv_abc123...

# Hash it (the value that goes into the env var):
node -e 'console.log(crypto.createHash("sha256").update(process.argv[1]).digest("hex"))' cv_abc123...

# Add to the registry JSON:
{ "<sha256-hex>": { "label": "my-agent@laptop", "scopes": ["mcp-read"] } }

# Set on the worker:
wrangler secret put QA_MCP_TOKENS_JSON --env production
# (paste the entire JSON object)

Give the plaintext token to the agent / CI runner; never store it unencrypted anywhere centrally. Treat as a credential.

API

Core (@cv/auth-bearer)

sha256Hex(input: string): Promise<string>
extractBearer(authHeader: string | null | undefined): string | null
parseRegistry(source: string | undefined, opts?: { validScopes?: ReadonlySet<string> }): Map<string, Caller>
scopeAllows(callerScopes: readonly string[], required: string): boolean
authenticate({ request, registrySource, validScopes? }): Promise<AuthResult>

interface Caller { label: string; scopes: string[] }

Use authenticate(...) for plain-Workers consumers that aren't on Hono.

Hono adapter (@cv/auth-bearer/hono)

makeRequireAuth<TEnv, TVars>({ getRegistrySource, validScopes?, contextKey? }): MiddlewareHandler
makeRequireScope<TEnv, TVars>(scope, opts?): MiddlewareHandler

makeRequireAuth returns a middleware that resolves the bearer and sets the caller on the Hono context (default key 'caller'). makeRequireScope enforces a scope on the already-resolved caller (apply after makeRequireAuth in the chain).

Scope hierarchy

The built-in scopeAllows recognises two implications:

  • admin implies every scope
  • mcp-trigger implies mcp-read

Otherwise: flat equality match. Consumers with their own hierarchy needs can write their own check; nothing else in this library assumes hierarchy.

Security notes

  • 401 responses are generic by design — never disclose whether the token was unknown, malformed, or scope-insufficient. Scope failures use 403, on a valid token only.
  • Token hashes only — plaintext tokens never live on the worker side.
  • Module-scope per-process cache keyed by the env-var string — we don't re-parse the JSON on every request. CF Workers can swap modules between invocations; the cache re-warms automatically.
  • This library is for service / agent / CI authentication. Human users authenticate via Better Auth + WebAuthn passkeys (see wiki/content/architecture/PRIVACY-POSTURE.md).
Source: wiki/content/archive/2026-05/auth-stashed/spike-snapshot/lib/auth-bearer/README.md