Conversation
…Workers-compat)
The apiKeyAuth() middleware previously rewrote c.req.raw.headers.set(
"authorization", ...) after successfully authenticating an x-api-key, so the
downstream global gate and route handlers would see a synthesized Bearer.
This worked in Node but threw TypeError: Can't modify immutable headers in
Cloudflare Workers on every x-api-key request, 500ing POST /v1/tokens and
POST /v1/api-keys in production.
Replace the header rewrite with context-based signaling:
- apiKeyAuth() now calls c.set("apiKeyClaims", auth.claims) /
c.set("apiKeyVia", "api_key") and drops the synthesized bearer path.
- AppEnv["Variables"] gains apiKeyClaims / apiKeyVia typing.
- New lib/auth helpers authenticateFromContext() and
authenticateAndAuthorizeFromContext() prefer context-stashed claims,
falling back to Authorization header parsing otherwise.
- Routes mounted behind apiKeyAuth (tokens, identities, role-assignments,
plus the scope.ts requireScope middleware) now consult the context.
- The server.ts global gate admits requests when apiKeyVia === "api_key"
without falling back to reading the raw x-api-key header, preserving the
invariant that un-mounted routes reject raw x-api-key.
Added regression tests in api-key-auth-middleware.test.ts:
- Static source-level guard that the middleware source contains no
.headers.set( or c.req.raw.headers references or signHs256Token() calls.
- End-to-end tests that freeze Headers.set/delete/append on a Request to
simulate Workers' locked headers and assert POST /v1/identities and
POST /v1/tokens both succeed (reproducing the original 500).
- Bearer-wins precedence test for a mixed bearer + x-api-key request via
the new context path.
All 324 existing tests still pass; total 328.
Bumps @relayauth/server 0.2.0 -> 0.2.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a production bug where
apiKeyAuth()middleware throwsTypeError: Can't modify immutable headerson every x-api-keyrequest in Cloudflare Workers. Surfaced 2026-04-23 when cloud's
sage worker started using x-api-key to mint bearer tokens;
every call returned 500.
Previous implementation rewrote
c.req.raw.headers.set("authorization", ...)after successful api-key auth so downstream helpers reading the
Authorization header would see a synthesized bearer. That works
in Node but not in Workers (Request headers are locked).
Fix
Instead of mutating the Request headers, the middleware now stores
the authenticated claims on Hono's context:
Downstream auth helpers (
authenticateAndAuthorize, etc.) checkthe context first before falling back to header parsing. No more
in-place Request mutation.
Concrete changes:
packages/server/src/middleware/api-key-auth.ts— drop header rewrite;set
apiKeyClaims/apiKeyViaon context. Delete the now-unusedsignHs256Token+encodeBase64Urlhelpers.packages/server/src/env.ts— addapiKeyClaims/apiKeyViatoAppEnv["Variables"].packages/server/src/lib/auth.ts— addauthenticateFromContext()andauthenticateAndAuthorizeFromContext()helpers that prefercontext-stashed claims, falling back to header parsing. Existing
authenticate()/authenticateAndAuthorize()signatures preservedfor backward compat.
packages/server/src/routes/{tokens,identities,role-assignments}.ts—swap to the context-based helpers on routes mounted behind
apiKeyAuth().packages/server/src/middleware/scope.ts—requireScope/requireScopesnow prefer
c.get("apiKeyClaims")over parsing the Authorization bearer.packages/server/src/server.ts— global gate now admits a request whenc.get("apiKeyVia") === "api_key". It still refuses to read rawx-api-key, so un-mounted routes cannot silently accept unvalidated keys.Bearer-wins precedence is preserved (handled inside
authenticateBearerOrApiKey— bearer is checked first, and only on failuredoes it consider the api-key). All existing tests updated, new regression
tests added.
New tests
packages/server/src/__tests__/api-key-auth-middleware.test.ts:api-key-auth.tsand asserts (after strippingcomments) that its code contains no
.headers.set(, noc.req.raw.headers,and no
signHs256Token(call. Prevents a silent regression.Headers.set/delete/appendon theRequestto mirror Workers' immutableheaders and asserts the x-api-key-authenticated request does not 500.
cloud's sage worker hits in production.
manage scope wins over read-only api-key; was previously tested via the
header-rewrite path.
Test plan
node --test --import tsx 'packages/server/src/__tests__/*.test.ts'— 328 pass (was 324; +4 new)npx turbo typecheck --filter=@relayauth/server— clean.headers.set(orc.req.raw.headerscalls remain in middleware sourceBumps
@relayauth/server: 0.2.0 -> 0.2.1After this merges,
@relayauth/server@0.2.1needs publishing to npmand cloud's
packages/relayauth/package.jsonbumped to consume it.Scope
@relayauth/server. Externalconsumers of
authenticate()/authenticateAndAuthorize()see no API change;the new
*FromContextvariants are additive.@relayauth/sdk— server-only.🤖 Generated with Claude Code