fetchWithAuth carries the whole request context, so route-to-route calls work - #279
Merged
Merged
Conversation
A sub-request carries nothing from the request that triggered it — a framework builds it from the caller's arguments alone — so a route reached by an in-process hop sees no Authorization and none of the platform headers Base44 sets. Its createClientFromRequest then throws on the missing Base44-App-Id, and service role is unavailable there. Apps were papering over this by copying a hand-maintained list of header names off the inbound request. That list belongs in the SDK: it is exactly the set createClientFromRequest reads, it goes stale in every app the moment the platform adds one, and getting it wrong is a security bug rather than a broken feature. fetchWithAuth now sends whatever credentials the client holds, so the callee rebuilds the client the caller is holding — same user, same data environment, same signed Base44-State, and service role. One method covers both situations because a client can only send what it has: a browser client is built with no serviceToken, so there is no service credential for it to send, and its behavior is unchanged. The service credential is included deliberately. It is minted per request for the app as a whole, not for one route, so a handler in the same worker already runs with that authority — reaching it through a route hop is the privilege it would have had by importing a shared function. What must not happen is the credential leaving the app, and the relative-path rule is what prevents that: the trust boundary is the destination, not the header. An optional init.fetch supplies the transport, since the global fetch cannot resolve a root-relative path on a server. host is deliberately never sent: a runtime that routes a relative path in-process synthesizes the sub-request's origin from it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tTNp16fczTqi3KQ7mGEKS
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/sdk@0.8.47-pr.279.2624371Prefer not to change any import paths? Install using npm alias so your code still imports npm i "@base44/sdk@npm:@base44-preview/sdk@0.8.47-pr.279.2624371"Or add it to your {
"dependencies": {
"@base44/sdk": "npm:@base44-preview/sdk@0.8.47-pr.279.2624371"
}
}
Preview published to npm registry — try new features instantly! |
FetchWithAuthInit is exported and named in fetchWithAuth's signature, so referencing a non-exported alias from it gave consumers a type they could not name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tTNp16fczTqi3KQ7mGEKS
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.
Follow-up to #277.
fetchWithAuthshipped solving the browser half of the problem; this does the server half.The problem
A sub-request carries nothing from the request that triggered it — a framework builds it from the caller's arguments alone — so a route reached by an in-process hop sees no
Authorizationand none of the platform headers Base44 sets. ItscreateClientFromRequestthen throws on the missingBase44-App-Id, and service role is unavailable there.Apps were papering over this by copying a hand-maintained list of header names off the inbound request:
That list belongs here. It is exactly the set
createClientFromRequestreads, it goes stale in every app the moment the platform adds a header, and getting it wrong is a security bug rather than a broken feature — the guidance that produced the snippet above shipped withBase44-Statemissing (breaking IP-allowlisted apps) and then, on the fix, briefly copied the request wholesale (leaking the service credential).The change
fetchWithAuthnow sends whatever credentials the client holds, so a route reached this way rebuilds the client the caller is holding — same user, same data environment, same signedBase44-State, and service role:One method covers both situations because a client can only send what it has:
createClient({ appId })in a browserAuthorizationonly — unchangedcreateClientFromRequest(event.req)in a routeAn optional
init.fetchsupplies the transport, since the globalfetchcannot resolve a root-relative path on a server. In Nitro that is its ownfetch, which dispatches a leading-slash path in-process with no network hop.Why the service credential is included
It is minted per request for the app as a whole, not for one route —
issue_virtual_service_token(app_id, client_id=…)signsapp_id+client_id+ scope + a TTL, with nothing request-specific. A handler in the same worker already runs with that authority, so reaching it through a route hop is the privilege it would have had by importing a shared function. Withholding it costsasServiceRolein the callee and buys nothing.What must not happen is a credential leaving the app, and the relative-path rule is what prevents that: the trust boundary is the destination, not the header. An absolute URL, a protocol-relative
//host, or anything else a URL parser would read as another origin throws before anything is sent.hostis deliberately never forwarded — h3'stoRequestsynthesizes an in-process sub-request's origin from it, so passing the inbound one would point the hop at another host.Verification
tests/unit/fetch-with-auth.test.ts: 18 → 28 tests. 301 unit tests pass; lint,tsc --noEmit, andtest:typesall clean.sends no service credential, having none— the browser case, which is the load-bearing fact that lets one method serve both. If browser clients ever gain a service token, this fails and the decision gets re-examined.does not forward host, which would repoint the sub-request's origin.Authorization: ""), headers absent from the inbound request stay absent rather than becoming empty strings, andinit.fetchis not passed through as request init.Nitro's in-process dispatch and h3's
event/contextwiring were read from the pinnednitro@3.0.260610-betaandh3@2.0.1-rc.22packages, not from memory.Notes for review
fetchWithAuth's released JSDoc promised "only theAuthorizationheader is added", so that block is rewritten rather than appended to. An earlier revision of this description claimed that text is what Mintlify publishes — that was wrong, and I'm correcting it here rather than leaving it to mislead the review. I rannpm run create-docson both this branch and the merged 0.8.47 code:Base44Clientis not intypes-to-expose.json, so no interface page is generated andfetchWithAuthappears nowhere in the output either way. The rewritten block's audience is editor intellisense and source readers, and no pipeline config change is needed.The behavior change reaches only server-side callers, whose flow the old docs described as not working anyway ("a route that builds its own client from the incoming request also needs the platform's
Base44-App-IdandBase44-Api-Url, which a request you construct yourself does not have").Once this releases, the apper fullstack capability guide drops the
FORWARDEDblock and restates the hop rule as destination-scoped.🤖 Generated with Claude Code
https://claude.ai/code/session_014tTNp16fczTqi3KQ7mGEKS