feat(client): add fetchWithAuth for same-origin server routes - #277
Conversation
Full-stack apps serve their own server routes next to the frontend, and
those routes read the caller's identity from the Authorization header. The
access token lives in local storage and is attached to the SDK's own
clients, so a plain fetch("/api/orders") reaches the route anonymous — apps
work around it by reading the token and building the header by hand.
fetchWithAuth() is that fetch with the header already on it. It resolves the
path against the page and refuses anything landing on another origin, so an
absolute URL, a protocol-relative path or a backslash-prefixed one cannot
carry the token off-site. The token comes from the user axios client's
Authorization default, which follows setToken() and is cleared by logout(),
so a request never carries a token the user no longer has.
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.46-pr.277.b5afe0aPrefer 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.46-pr.277.b5afe0a"Or add it to your {
"dependencies": {
"@base44/sdk": "npm:@base44-preview/sdk@0.8.46-pr.277.b5afe0a"
}
}
Preview published to npm registry — try new features instantly! |
Review feedback: don't resolve the path against the document and don't gate
the method on a browser — Nitro dispatches a relative path to another server
route in-process, and the SDK should not stand in the way of that.
The origin guarantee now comes from the path shape rather than from a
comparison: one leading slash cannot carry a scheme, and rejecting "//host"
and "/\host" covers the two forms that reach another origin. The check runs on
the string a URL parser would see — tabs and newlines removed, leading
C0/space trimmed — because "/<tab>/evil.example" reads as protocol-relative by
the time the request is built and would otherwise pass a prefix test.
The path then goes to fetch untouched, so a server-side client works: the
token on such a client is the caller's own, from createClientFromRequest. Only
Authorization is added — a callee building its own client from the request
still needs the platform headers, which the JSDoc now says.
A bare relative path ("api/orders") is no longer accepted; a leading slash is
also what an in-process dispatcher expects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tTNp16fczTqi3KQ7mGEKS
|
@claude update the branch with main |
|
Claude finished @netanelgilad's task in 1m 15s —— View job Update branch with mainI'm not able to merge
That commit touches How to update the branch:
|
What
Adds
base44.fetchWithAuth(path, init?)—fetchwith the signed-in user's access token already attached, restricted to the app's own origin.Why
Full-stack apps serve their own server routes next to the frontend, and those routes read the caller's identity from the
Authorizationheader. The access token lives in local storage and is attached to the SDK's own clients, so a plainfetch("/api/orders")reaches the route anonymous — it doesn't throw, the route just sees nothing. Today apps work around it by reading the token and building the header by hand, and the builder guidance told them to do exactly that.Design notes
Same-origin only, enforced by resolution. The path is resolved against the page (
new URL(path, location.href)) and the resolved origin must equal the page's. That single check rejects absolute URLs, protocol-relative//evil.example, backslash-prefixed/\evil.exampleand a different port — so the token cannot leave the app's origin. The error names where the path resolved to and points atfunctions.fetch()for backend functions or plainfetch()for another origin.Token comes from the user axios client's
Authorizationdefault, not the constructor token. That default followsauth.setToken()and is deleted bylogout(), so a request never carries a token the user no longer has. Readingtoken || getAccessToken()(the pattern the functions module uses) would keep sending a stale constructor token after a logout.Anonymous stays working. With no user signed in the request goes out without the header, so routes open to anonymous callers behave as before. An
Authorizationheader the caller sets explicitly is kept.Browser-only. The problem it solves is browser-only — on the server the token arrives on the request and
createClientFromRequest()already picks it up. Called outside the browser it throws with that pointer.The signature stays open for a server story if one ever lands:
init.fetch(for a framework-provided dispatcher such as Nitro's in-processfetch) would be an additive optional field, no breaking change. Deliberately not built here — nothing in this PR presumes it.Testing
tests/unit/fetch-with-auth.test.ts— 13 cases: token attached; token read from local storage when the client was built without one; the post-setTokentoken wins; no header afterlogout(); no header when anonymous;initforwarded and a caller-setAuthorizationpreserved; relative path resolution; and rejection of an absolute URL, a protocol-relative path, a backslash-prefixed path, another port, an empty path, and a non-browser environment.286 passed,npm run lintandnpm run test:typesclean.npm run create-docs:generaterenders the new method on theBase44Clientreference page with both examples, 0 errors.No version bump — the publish workflow bumps.
Follow-ups (not in this PR)
base44-sdkskill inbase44/skillsis synced from SDK source bysync-sdk-skill, and the Mintlify reference is regenerated by the docs pipeline; both pick this up after a release.base44-dev/apperPR updates the full-stack capability guide to use it (paired with this one; the guide notes the version it needs).🤖 Generated with Claude Code
https://claude.ai/code/session_014tTNp16fczTqi3KQ7mGEKS
Generated by Claude Code