feat(client): redeem a platform embed token at createClient - #282
amitbardos wants to merge 1 commit into
Conversation
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/sdk@0.8.48-pr.282.094dd5bPrefer 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.48-pr.282.094dd5b"Or add it to your {
"dependencies": {
"@base44/sdk": "npm:@base44-preview/sdk@0.8.48-pr.282.094dd5b"
}
}
Preview published to npm registry — try new features instantly! |
c505512 to
ca4dff7
Compare
| // one-time token is redeemed nowhere else, so a top-level load still | ||
| // carrying one (a URL rewrite the browser refused) must not have it | ||
| // applied as a session — and never saved as one. | ||
| if (paramName === DEFAULT_TOKEN_PARAM && isFramed()) { |
There was a problem hiding this comment.
Reviewer note — the one decision here I'd like a second opinion on.
This returns the ?ott= so an existing app's auth gate still fires:
// templates/apps_template/src/lib/AuthContext.jsx
if (appParams.token) { await checkUserAuth(); }app-params.js calls getAccessToken() before createClient exists, so there is nothing else to ask at that moment. Return null and the app renders signed-out while every request it makes is authenticated.
The trade-off. It buys out a template change, and pays for it by handing app code a value that looks like a session token and isn't — createClient then has to defend against getting its own utility's output back (const token = embedOtt ? undefined : config.token). Two coupled changes in two files, and a reader of either one can't tell why.
The alternative is one line in that same file, which already imports the client:
if (appParams.token || base44.auth.isEmbedded()) {Cleaner — but it only reaches apps the codemod can patch, not ones that just get the sdk_min_version bump.
@netanelgilad — can the codemod edit template files, or only bump the version? That decides which way this goes. Separately, worth confirming in base44-dev/apper#24167 that the OTT is rejected as a bearer everywhere except the exchange, so an app that naively sends it gets a 401 rather than a compromise.
Apps embedded by a host platform arrive with a one-time token on the
iframe URL, `?ott=`, minted by the platform's server. `createClient` now
takes it off the URL as the client is created, trades it for an app-user
session through the OAuth token-exchange grant (RFC 8693,
`POST /api/apps/{appId}/auth/embed/token`), and seeds every module with
the result — in memory only. The session lives and dies with the frame.
Doing this in the SDK rather than in the app template is what makes it
reach every existing app: the platform bumps `sdk_min_version` and its
sandbox codemod pulls the new version in on each app's next boot.
The exchange
- The token is stripped from the URL before any other code can read
`location.href`, and reported back only inside a frame — the only place
one is redeemed. A top-level load still has it taken off the address
bar, history and any shared link.
- `authReady` gates everything that can wait: an axios request
interceptor on the two user clients, `functions.fetch`,
`fetchWithAuth`, and the actors mint and dial. It never rejects, so one
failed exchange does not become a rejection on every request. The POST
aborts after 15s rather than leaving every request parked behind it.
- A 429 is retried once: the server's limiter refuses before the token is
redeemed, so it is still valid.
- An embedded load never reads a stored token. Whatever an earlier
visitor left in `localStorage` is not this session.
One live token
`getToken()` replaces the seven `token || getAccessToken()` closures
(actors auth and mint, `functions.fetch`, agents URLs, aiGateway,
socket). Each had captured a construction-time token and could otherwise
only fall back to storage — so a session held in memory reached none of
them. The auth module now owns the current token and reports every
change through `onSessionChange`, which is how the realtime socket learns
to redial, and how `logout` drops a connection still running as the user
who just left.
- `base44.auth.isEmbedded()` — public: true when a platform embedded this
load. The session is memory-only, so a reload inside the frame ends it
and this returns false there.
- `redirectToLogin()` and `logout()` in a frame show a "session ended"
notice instead of navigating. Only the platform can mint the next
session; the app's own login would work in the frame but would mint a
different, app-level identity.
- `logout()` now clears `Authorization` on both axios instances. Missing
the functions one was masked by the redirect tearing the page down.
- The socket takes `getToken` and asks on every connect, replacing a
static token it had to be handed.
- `getAccessToken()` returns the `?ott=` value on an embedded load — not
stored, not stripped. Every app's `app-params.js` calls it once before
`createClient` runs, and `AuthContext` gates its auth check on that
snapshot, so without it an embedded app renders signed out while every
request it makes is authenticated.
Backward compatibility
An app opened normally is unaffected: with no `?ott=`, `createClient`
runs the code it ran before, and `getToken()` still falls back to storage
so a login in another tab is picked up on reconnect. `AgentsModuleConfig`
and `AiGatewayModuleConfig` (both `@internal`) take `getToken`;
`createClient` is their only caller.
Tests
`tests/unit/embed-session.test.ts` and `tests/unit/client-embed.test.ts`
cover the URL strip, the frame-only report, the RFC 8693 request shape,
refused / thrown / 429-retried / timed-out exchanges, the notice overlay,
and through `createClient`: a request issued mid-exchange carrying the
exchanged bearer, nothing written to storage, a stale stored token
ignored, the socket connecting with the exchanged session, and a normal
app left untouched. `npm test` and `eslint` pass on Node 26.
Server side ships in base44-dev/apper#24167; this supersedes
base44-dev/apper#24259.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93cd240 to
66ad29e
Compare
Apps embedded by a host platform arrive with a one-time token on the iframe URL,
?ott=.createClienttakes it off the URL, trades it for an app-user session through the RFC 8693 token-exchange grant (POST /api/apps/{appId}/auth/embed/token), and keeps the result in memory only — the session lives and dies with the frame.In the SDK rather than the app template so it reaches every existing app: the platform bumps
sdk_min_versionand the codemod pulls it in on the next boot.What changes
createClientstrips?ott=and exchanges it. Everything that can wait for the session does — axios,functions.fetch,fetchWithAuth, actors. Nothing is persisted, and an embedded load never adopts a stored token: whatever an earlier visitor left is not this session.getToken()replaces seventoken || getAccessToken()closures (socket, agents, actors, aiGateway, functions). They captured a construction-time token or fell back to storage, so a memory-only session reached none of them. The auth module now owns the token and reports changes throughonSessionChange— which is how the socket redials, and howlogoutdrops a connection still running as the user who just left.base44.auth.isEmbedded()(public). In a frame,redirectToLogin()andlogout()show a "session ended" notice instead of navigating: only the platform can mint the next session, and the app's own login would mint a different identity.getAccessToken()returns the?ott=on an embedded load, so an app's existingif (appParams.token)gate still fires and it doesn't render signed-out while authenticated. This is the piece I'd most like a second opinion on — see the inline comment.Compatibility
An app opened normally runs the code it ran before, storage fallback included.
AgentsModuleConfig/AiGatewayModuleConfig(both@internal) takegetToken;createClientis their only caller.Testing
npm test(355 unit + type tests) andeslintpass on Node 26. New coverage: the URL strip, the frame-only report, the RFC 8693 request shape, refused / thrown / 429-retried / timed-out exchanges, and throughcreateClient— a request issued mid-exchange carrying the exchanged bearer, nothing written to storage, a stale stored token ignored, and a normal app untouched.Server side ships in base44-dev/apper#24167. Supersedes base44-dev/apper#24259.
🤖 Generated with Claude Code