Skip to content

feat(client): add fetchWithAuth for same-origin server routes - #277

Merged
netanelgilad merged 3 commits into
mainfrom
claude/fetch-with-auth-sdk-cyrgws
Sep 7, 2026
Merged

feat(client): add fetchWithAuth for same-origin server routes#277
netanelgilad merged 3 commits into
mainfrom
claude/fetch-with-auth-sdk-cyrgws

Conversation

@netanelgilad

Copy link
Copy Markdown
Collaborator

What

Adds base44.fetchWithAuth(path, init?)fetch with the signed-in user's access token already attached, restricted to the app's own origin.

const res = await base44.fetchWithAuth("/api/orders");
await base44.fetchWithAuth("/api/orders", { method: "POST", body });  // init passes through

Why

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 — 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.example and a different port — so the token cannot leave the app's origin. The error names where the path resolved to and points at functions.fetch() for backend functions or plain fetch() for another origin.

Token comes from the user axios client's Authorization default, not the constructor token. That default follows auth.setToken() and is deleted by logout(), so a request never carries a token the user no longer has. Reading token || 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 Authorization header 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-process fetch) 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-setToken token wins; no header after logout(); no header when anonymous; init forwarded and a caller-set Authorization preserved; 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.
  • Full unit suite 286 passed, npm run lint and npm run test:types clean.
  • npm run create-docs:generate renders the new method on the Base44Client reference page with both examples, 0 errors.

No version bump — the publish workflow bumps.

Follow-ups (not in this PR)

  • The base44-sdk skill in base44/skills is synced from SDK source by sync-sdk-skill, and the Mintlify reference is regenerated by the docs pipeline; both pick this up after a release.
  • base44-dev/apper PR 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

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
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/sdk@0.8.46-pr.277.b5afe0a

Prefer not to change any import paths? Install using npm alias so your code still imports @base44/sdk:

npm i "@base44/sdk@npm:@base44-preview/sdk@0.8.46-pr.277.b5afe0a"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "@base44/sdk": "npm:@base44-preview/sdk@0.8.46-pr.277.b5afe0a"
  }
}

Preview published to npm registry — try new features instantly!

@netanelgilad netanelgilad self-assigned this Sep 6, 2026
@github-actions github-actions Bot added the docs-draft PR has auto-drafted documentation suggestions label Sep 6, 2026
Comment thread src/utils/fetch-with-auth.ts Outdated
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
@netanelgilad

Copy link
Copy Markdown
Collaborator Author

@claude update the branch with main

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @netanelgilad's task in 1m 15s —— View job


Update branch with main

I'm not able to merge main into this branch. Merging/rebasing branches is outside what I'm permitted to do here — my git access is limited to staging, committing, and pushing commits (see the FAQ). Both git fetch and git merge are blocked in this environment, so I can't bring the branch up to date myself.

origin/main is currently 1 commit ahead of this branch:

That commit touches .github/workflows/security-audit.yml and package-lock.json, which don't overlap with this PR's files (src/client.ts, src/client.types.ts, src/utils/fetch-with-auth.ts, tests/unit/fetch-with-auth.test.ts), so a merge should be clean.

How to update the branch:

  • On GitHub: use the Update branch button on this PR, or
  • Locally:
    git checkout claude/fetch-with-auth-sdk-cyrgws
    git fetch origin
    git merge origin/main
    git push

@netanelgilad
netanelgilad merged commit ef91da9 into main Sep 7, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs-draft PR has auto-drafted documentation suggestions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants