@base44-preview/sdk 0.8.48-pr.282.028b41c → 0.8.48-pr.282.094dd5b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -5,7 +5,7 @@ import { createAuthModule } from "./modules/auth.js";
5
5
  import { createSsoModule } from "./modules/sso.js";
6
6
  import { createConnectorsModule, createUserConnectorsModule, } from "./modules/connectors.js";
7
7
  import { getAccessToken } from "./utils/auth-utils.js";
8
- import { exchangeEmbedToken, isEmbeddedTab, markEmbeddedTab, takeEmbedTokenFromUrl, } from "./utils/embed-session.js";
8
+ import { exchangeEmbedToken, takeEmbedTokenFromUrl, } from "./utils/embed-session.js";
9
9
  import { createFetchWithAuth } from "./utils/fetch-with-auth.js";
10
10
  import { createFunctionsModule } from "./modules/functions.js";
11
11
  import { createAgentsModule } from "./modules/agents.js";
@@ -59,12 +59,30 @@ export function createClient(config) {
59
59
  // Normalize appBaseUrl to always be a string (empty if not provided or invalid)
60
60
  const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";
61
61
  const embedOtt = takeEmbedTokenFromUrl();
62
- if (embedOtt) {
63
- markEmbeddedTab();
62
+ // A declaration, not a const: this block sits above the auth module.
63
+ function getToken() {
64
+ var _a;
65
+ return (_a = userAuthModule.getToken()) !== null && _a !== void 0 ? _a : (embedOtt ? null : getAccessToken());
64
66
  }
65
- const embedded = Boolean(embedOtt) || isEmbeddedTab();
66
- // In a frame the passed token is the OTT itself or a stale stored one, not this session.
67
- const token = embedded ? undefined : config.token;
67
+ const socketConfig = {
68
+ serverUrl,
69
+ mountPath: "/ws-user-apps/socket.io/",
70
+ transports: ["websocket"],
71
+ appId,
72
+ getToken,
73
+ };
74
+ let socket = null;
75
+ const getSocket = () => {
76
+ if (!socket) {
77
+ socket = RoomsSocket({
78
+ config: socketConfig,
79
+ });
80
+ }
81
+ return socket;
82
+ };
83
+ // Apps pass getAccessToken() in as `token`, which in a frame is the OTT —
84
+ // what the exchange trades for a session, never a bearer itself.
85
+ const token = embedOtt ? undefined : config.token;
68
86
  const headers = {
69
87
  ...optionalHeaders,
70
88
  "X-App-Id": String(appId),
@@ -117,52 +135,35 @@ export function createClient(config) {
117
135
  appBaseUrl: normalizedAppBaseUrl,
118
136
  serverUrl,
119
137
  token,
120
- embedded,
138
+ embedded: Boolean(embedOtt),
139
+ // The socket carries its token on the handshake, so it can only pick a
140
+ // new one up by redialling — or, on logout, by dropping what it has.
141
+ onSessionChange: (hasSession) => hasSession ? socket === null || socket === void 0 ? void 0 : socket.reconnect() : socket === null || socket === void 0 ? void 0 : socket.disconnect(),
121
142
  });
122
143
  // Apply the access token before any module that may issue authenticated
123
144
  // requests during construction (notably analytics, which fires an init
124
145
  // event whose flush calls auth.me()). Without this, the first User/me
125
146
  // request is built before setToken runs and goes out unauthenticated.
126
- // Skipped in a frame: the session comes from the exchange below.
127
- if (typeof window !== "undefined" && !embedded) {
147
+ // Not in a frame: a stored token there belongs to an earlier visitor.
148
+ if (typeof window !== "undefined" && !embedOtt) {
128
149
  const accessToken = token || getAccessToken();
129
150
  if (accessToken) {
130
151
  userAuthModule.setToken(accessToken);
131
152
  }
132
153
  }
133
- // Live token for every module; outside a frame it still falls back to storage.
134
- const getToken = () => { var _a; return (_a = userAuthModule.getToken()) !== null && _a !== void 0 ? _a : (embedded ? null : getAccessToken()); };
135
- const socketConfig = {
136
- serverUrl,
137
- mountPath: "/ws-user-apps/socket.io/",
138
- transports: ["websocket"],
139
- appId,
140
- getToken,
141
- };
142
- let socket = null;
143
- const getSocket = () => {
144
- if (!socket) {
145
- socket = RoomsSocket({ config: socketConfig });
146
- }
147
- return socket;
148
- };
149
- const applyToken = (newToken, saveToStorage) => {
150
- userAuthModule.setToken(newToken, saveToStorage);
151
- socket === null || socket === void 0 ? void 0 : socket.reconnect();
152
- };
153
- // Settles once the exchanged session is applied (memory only); at once
154
- // otherwise. Never rejects: every request waits on it, so a failure here
155
- // must not turn into a rejection on each of them.
156
- const authReady = embedOtt
154
+ const session = embedOtt
157
155
  ? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt })
156
+ : null;
157
+ // Never rejects: every request waits on it, so one failure here must not
158
+ // become a rejection on each of them.
159
+ const authReady = session
160
+ ? session
158
161
  .then((sessionToken) => {
159
162
  var _a;
160
163
  if (sessionToken) {
161
- applyToken(sessionToken, false);
164
+ userAuthModule.setToken(sessionToken, false);
162
165
  return;
163
166
  }
164
- // The app is about to run anonymous. Say why, once, instead of
165
- // leaving only the 401s that follow.
166
167
  const error = new Error("Base44: the embed token was refused, so this app is not signed in.");
167
168
  console.error(error.message);
168
169
  (_a = options === null || options === void 0 ? void 0 : options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
@@ -171,9 +172,9 @@ export function createClient(config) {
171
172
  console.error("Base44: applying the embedded session failed:", e);
172
173
  })
173
174
  : Promise.resolve();
174
- if (embedOtt) {
175
- // Requests issued during the exchange wait for it. Registered after createAxiosClient's
176
- // interceptors so it runs first and the anonymous-visitor header sees the Authorization.
175
+ if (session) {
176
+ // Registered after createAxiosClient's so it runs first (axios unshifts),
177
+ // letting the anonymous-visitor header see the Authorization we just set.
177
178
  for (const client of [axiosClient, functionsAxiosClient]) {
178
179
  client.interceptors.request.use(async (requestConfig) => {
179
180
  await authReady;
@@ -191,7 +192,10 @@ export function createClient(config) {
191
192
  // URL needs an absolute host, so fall back to the page origin.
192
193
  host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_a = window.location) === null || _a === void 0 ? void 0 : _a.origin : undefined),
193
194
  functionsVersion,
194
- getAuthToken: getToken,
195
+ getAuthToken: async () => {
196
+ await authReady;
197
+ return getToken();
198
+ },
195
199
  mintConnectionToken: async (actorName, room, connectionId) => {
196
200
  await authReady;
197
201
  const authToken = getToken();
@@ -219,6 +223,7 @@ export function createClient(config) {
219
223
  connectors: createUserConnectorsModule(axiosClient, appId),
220
224
  auth: userAuthModule,
221
225
  functions: createFunctionsModule(functionsAxiosClient, appId, {
226
+ waitForAuth: () => authReady,
222
227
  getAuthHeaders: () => {
223
228
  const headers = {};
224
229
  const sessionToken = getToken();
@@ -234,6 +239,7 @@ export function createClient(config) {
234
239
  getSocket,
235
240
  appId,
236
241
  serverUrl,
242
+ // Sync, unlike everything else: these return a URL, not a promise.
237
243
  getToken,
238
244
  }),
239
245
  aiGateway: createAiGatewayModule({ serverUrl, getToken, appId }),
@@ -281,7 +287,9 @@ export function createClient(config) {
281
287
  getSocket,
282
288
  appId,
283
289
  serverUrl,
284
- getToken: () => token,
290
+ // The user's token, deliberately: this is read only for the `?token=` on
291
+ // a channel URL handed to that user. Never the service credential.
292
+ getToken,
285
293
  }),
286
294
  aiGateway: createAiGatewayModule({
287
295
  serverUrl,
@@ -322,12 +330,12 @@ export function createClient(config) {
322
330
  serverUrl,
323
331
  functionsVersion,
324
332
  platformHeaders: optionalHeaders,
333
+ waitForAuth: () => authReady,
325
334
  }),
326
335
  /**
327
336
  * Sets a new authentication token for all subsequent requests.
328
337
  *
329
338
  * @param newToken - The new authentication token
330
- * @param saveToStorage - Whether to also keep the token in local storage. Defaults to `true`.
331
339
  *
332
340
  * @example
333
341
  * ```typescript
@@ -339,8 +347,8 @@ export function createClient(config) {
339
347
  * base44.setToken(access_token);
340
348
  * ```
341
349
  */
342
- setToken(newToken, saveToStorage = true) {
343
- applyToken(newToken, saveToStorage);
350
+ setToken(newToken) {
351
+ userAuthModule.setToken(newToken, true);
344
352
  },
345
353
  /**
346
354
  * Gets the current client configuration.
@@ -200,9 +200,8 @@ export interface Base44Client {
200
200
  * Updates the token for both HTTP requests and WebSocket connections.
201
201
  *
202
202
  * @param newToken - The new authentication token.
203
- * @param saveToStorage - Whether to also keep the token in local storage so it survives a page load. Defaults to `true`.
204
203
  */
205
- setToken(newToken: string, saveToStorage?: boolean): void;
204
+ setToken(newToken: string): void;
206
205
  /**
207
206
  * Gets the current client configuration.
208
207
  * @internal
@@ -11,8 +11,9 @@ interface ActorsConfig {
11
11
  appId: string;
12
12
  /** Current user access token, if authenticated. Rides the WS query on the
13
13
  * proxy-fallback path so the platform proxy can authenticate the connection;
14
- * anonymous connects omit it. */
15
- getAuthToken(): string | null | undefined;
14
+ * anonymous connects omit it. Awaited per dial, so a session still being
15
+ * exchanged is in hand before the URL is built. */
16
+ getAuthToken(): Promise<string | null | undefined>;
16
17
  /** Same semantics as function calls: editors with a non-prod version get the
17
18
  * draft actor script; everyone else gets the published one. */
18
19
  functionsVersion?: string;
@@ -83,7 +83,7 @@ class Connection {
83
83
  }
84
84
  }
85
85
  // Rebuilt per attempt so a login/logout is picked up on reconnect.
86
- return buildProxyActorUrl(config.host, actorName, instanceId, this.id, config.appId, config.getAuthToken(), config.functionsVersion);
86
+ return buildProxyActorUrl(config.host, actorName, instanceId, this.id, config.appId, await config.getAuthToken(), config.functionsVersion);
87
87
  };
88
88
  const ws = new ReconnectingWebSocket(urlProvider);
89
89
  this.ws = ws;
@@ -161,7 +161,7 @@ export interface AgentsModuleConfig {
161
161
  /** Server URL */
162
162
  serverUrl?: string;
163
163
  /** Returns the current authentication token, if any */
164
- getToken: () => string | null | undefined;
164
+ getToken: () => string | null;
165
165
  }
166
166
  /**
167
167
  * Agents module for managing AI agent conversations.
@@ -360,7 +360,9 @@ export interface AgentsModule {
360
360
  * Gets WhatsApp connection URL for an agent.
361
361
  *
362
362
  * Generates a URL that users can use to connect with the agent through WhatsApp.
363
- * The URL includes authentication if a token is available.
363
+ * The URL includes authentication if a token is available. In an app a
364
+ * platform has embedded, that is only once the session has been exchanged —
365
+ * await a call such as `base44.auth.me()` before building the URL.
364
366
  *
365
367
  * @param agentName - The name of the agent.
366
368
  * @returns WhatsApp connection URL.
@@ -378,7 +380,9 @@ export interface AgentsModule {
378
380
  * Gets Telegram connection URL for an agent.
379
381
  *
380
382
  * Generates a URL that users can use to connect with the agent through Telegram.
381
- * The URL includes authentication if a token is available. When the user opens
383
+ * The URL includes authentication if a token is available. In an app a
384
+ * platform has embedded, that is only once the session has been exchanged —
385
+ * await a call such as `base44.auth.me()` before building the URL. When the user opens
382
386
  * this URL, they are redirected to the agent's Telegram bot with an activation
383
387
  * code that securely links their account.
384
388
  *
@@ -116,8 +116,11 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
116
116
  if (typeof window === "undefined") {
117
117
  throw new Error("Login method can only be used in a browser environment");
118
118
  }
119
- // An embedded session can only be renewed by the host platform; the
120
- // login round trip cannot complete inside its frame.
119
+ // Only the host platform can sign a platform user in, so there is no
120
+ // login page to send them to. (The app's own login would work in the
121
+ // frame — `loginWithProvider` opens a popup — but it would mint a
122
+ // different, app-level identity.) An app that wants its own notice
123
+ // checks `isEmbedded()` rather than asking for a login it cannot get.
121
124
  if (options.embedded) {
122
125
  showEmbedSessionEnded();
123
126
  return;
@@ -158,13 +161,18 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
158
161
  },
159
162
  // Logout the current user
160
163
  logout(redirectUrl) {
161
- // Remove token from axios headers (always do this)
164
+ var _a;
165
+ // Remove the token from both axios instances (always do this). Missing
166
+ // the functions one used to be hidden by the redirect below tearing the
167
+ // page down; an embedded logout returns instead, so the page lives on.
162
168
  delete axios.defaults.headers.common["Authorization"];
169
+ delete functionsAxiosClient.defaults.headers.common["Authorization"];
163
170
  // Drop identity resolved under the previous session: a `me()` already in
164
171
  // flight would otherwise resolve into callers that run after the logout.
165
172
  clearPendingMe();
166
173
  resetAnalyticsSessionContext();
167
174
  accessToken = null;
175
+ (_a = options.onSessionChange) === null || _a === void 0 ? void 0 : _a.call(options, false);
168
176
  // Only do the rest if in a browser environment
169
177
  if (typeof window !== "undefined") {
170
178
  // Remove token from localStorage
@@ -178,6 +186,13 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
178
186
  console.error("Failed to remove token from localStorage:", e);
179
187
  }
180
188
  }
189
+ // An embedded session holds no app cookie to clear — it lived in
190
+ // memory — and navigating a third-party frame to the logout endpoint
191
+ // would only break the frame. The state above is already cleared.
192
+ if (options.embedded) {
193
+ showEmbedSessionEnded();
194
+ return;
195
+ }
181
196
  // Determine the from_url parameter
182
197
  const fromUrl = redirectUrl || window.location.href;
183
198
  // Redirect to server-side logout endpoint to clear HTTP-only cookies
@@ -187,6 +202,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
187
202
  },
188
203
  // Set authentication token
189
204
  setToken(token, saveToStorage = true) {
205
+ var _a;
190
206
  if (!token)
191
207
  return;
192
208
  // An embedded session belongs to the frame the platform minted it for.
@@ -201,6 +217,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
201
217
  // handle token change for axios clients
202
218
  axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
203
219
  functionsAxiosClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
220
+ (_a = options.onSessionChange) === null || _a === void 0 ? void 0 : _a.call(options, true);
204
221
  // Save token to localStorage if requested
205
222
  if (persist && typeof window !== "undefined" && window.localStorage) {
206
223
  try {
@@ -98,11 +98,13 @@ export interface AuthModuleOptions {
98
98
  * which is how the server-side SDK reports a token it never sets explicitly.
99
99
  */
100
100
  token?: string;
101
+ /** Whether a host platform embedded this client in a frame. */
102
+ embedded?: boolean;
101
103
  /**
102
- * Whether this client runs embedded in a host platform's frame, where a
103
- * session is minted by the platform and cannot be renewed by a login redirect.
104
+ * Called when the identity changes: `true` on `setToken`, `false` on
105
+ * `logout`. Lets the client redial the socket, which holds its own copy.
104
106
  */
105
- embedded?: boolean;
107
+ onSessionChange?: (hasSession: boolean) => void;
106
108
  }
107
109
  /**
108
110
  * Authentication module for managing user authentication and authorization. The module automatically stores tokens in local storage when available and manages authorization headers for API requests.
@@ -185,11 +187,13 @@ export interface AuthModule {
185
187
  */
186
188
  redirectToLogin(nextUrl: string): void;
187
189
  /**
188
- * Whether the app is running embedded in a host platform.
190
+ * Whether a host platform embedded this app and signed its user in.
191
+ *
192
+ * Only the platform can renew that session, so {@linkcode AuthModule.redirectToLogin | redirectToLogin()} and {@linkcode AuthModule.logout | logout()} show a "session ended" notice rather than a login page. Use this to render your own notice, or to hide sign-in controls that cannot work in the frame.
189
193
  *
190
- * A platform embeds an app in an iframe and signs its user in by minting a one-time token onto the frame's URL; the SDK trades it for a session as the client is created. That session is held in memory only and can be renewed only by the platform, so when it ends there is no login page to send the user to — {@linkcode AuthModule.redirectToLogin | redirectToLogin()} shows a "session ended" notice instead. Use this to render your own notice or to hide sign-in and sign-out controls that cannot work inside the frame.
194
+ * The session lives in memory, so a reload inside the frame ends it and returns `false` here. Prefer client-side navigation.
191
195
  *
192
- * @returns `true` when the app was embedded by a host platform, `false` otherwise.
196
+ * @returns `true` when a host platform embedded this app.
193
197
  *
194
198
  * @example
195
199
  * ```typescript
@@ -560,9 +564,6 @@ export interface InternalAuthModule extends AuthModule {
560
564
  * could not succeed without a session, not to decide that one is valid.
561
565
  */
562
566
  hasToken(): boolean;
563
- /**
564
- * The access token currently set on the client, or `null` when there is none.
565
- * Follows {@linkcode AuthModule.setToken | setToken} and {@linkcode AuthModule.logout | logout}.
566
- */
567
+ /** The token currently set on the client, or `null`. */
567
568
  getToken(): string | null;
568
569
  }
@@ -65,8 +65,12 @@ export function createFunctionsModule(axios, appId, config) {
65
65
  },
66
66
  // Fetch a backend function endpoint directly.
67
67
  async fetch(path, init = {}) {
68
+ var _a;
68
69
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
69
70
  const primaryPath = `/functions${normalizedPath}`;
71
+ // Headers are read after this: a session still being negotiated must be
72
+ // in hand before the Authorization is built, not after.
73
+ await ((_a = config === null || config === void 0 ? void 0 : config.waitForAuth) === null || _a === void 0 ? void 0 : _a.call(config));
70
74
  const headers = toHeaders(init.headers);
71
75
  const requestInit = {
72
76
  ...init,
@@ -29,6 +29,12 @@ export type FunctionsFetchInit = RequestInit;
29
29
  export interface FunctionsModuleConfig {
30
30
  getAuthHeaders?: () => Record<string, string>;
31
31
  baseURL?: string;
32
+ /**
33
+ * Resolves once the client's session is settled. `fetch` builds its headers
34
+ * by hand rather than through axios, so without this it would miss the gate
35
+ * every other request goes through.
36
+ */
37
+ waitForAuth?: () => Promise<void>;
32
38
  }
33
39
  /**
34
40
  * Functions module for invoking custom backend functions.
@@ -5,7 +5,7 @@ import { GetAccessTokenOptions, SaveAccessTokenOptions, RemoveAccessTokenOptions
5
5
  * Low-level utility for manually retrieving tokens. In most cases, the Base44 client handles
6
6
  * token management automatically. This function is useful for custom authentication flows or when you need direct access to stored tokens. Requires a browser environment and can't be used in the backend.
7
7
  *
8
- * When a host platform has embedded the app with a one-time token (`?ott=`), that token is returned as it stands: it is what {@linkcode createClient} trades for the session, so a page that gates on "is there a token?" as it loads sees one. It is neither stored nor removed from the URL here.
8
+ * When a host platform has embedded the app with a one-time token (`?ott=`), that token is returned as it stands: it is what {@linkcode createClient} trades for the session, so a page that gates on "is there a token?" as it loads sees one. It is neither stored nor removed from the URL here, and it is reported only inside a frame, where such a token is redeemed.
9
9
  *
10
10
  * @internal
11
11
  *
@@ -1,11 +1,13 @@
1
- import { EMBED_TOKEN_PARAM } from "./embed-session.js";
1
+ import { EMBED_TOKEN_PARAM, isFramed } from "./embed-session.js";
2
+ /** The URL parameter a Base44 session token arrives on. */
3
+ const DEFAULT_TOKEN_PARAM = "access_token";
2
4
  /**
3
5
  * Retrieves an access token from URL parameters or local storage.
4
6
  *
5
7
  * Low-level utility for manually retrieving tokens. In most cases, the Base44 client handles
6
8
  * token management automatically. This function is useful for custom authentication flows or when you need direct access to stored tokens. Requires a browser environment and can't be used in the backend.
7
9
  *
8
- * When a host platform has embedded the app with a one-time token (`?ott=`), that token is returned as it stands: it is what {@linkcode createClient} trades for the session, so a page that gates on "is there a token?" as it loads sees one. It is neither stored nor removed from the URL here.
10
+ * When a host platform has embedded the app with a one-time token (`?ott=`), that token is returned as it stands: it is what {@linkcode createClient} trades for the session, so a page that gates on "is there a token?" as it loads sees one. It is neither stored nor removed from the URL here, and it is reported only inside a frame, where such a token is redeemed.
9
11
  *
10
12
  * @internal
11
13
  *
@@ -38,7 +40,7 @@ import { EMBED_TOKEN_PARAM } from "./embed-session.js";
38
40
  * ```
39
41
  */
40
42
  export function getAccessToken(options = {}) {
41
- const { storageKey = "base44_access_token", paramName = "access_token", saveToStorage = true, removeFromUrl = true, } = options;
43
+ const { storageKey = "base44_access_token", paramName = DEFAULT_TOKEN_PARAM, saveToStorage = true, removeFromUrl = true, } = options;
42
44
  let token = null;
43
45
  // Try to get token from URL parameters
44
46
  if (typeof window !== "undefined" && window.location) {
@@ -63,9 +65,16 @@ export function getAccessToken(options = {}) {
63
65
  // session yet — createClient takes it off the URL and exchanges it — but
64
66
  // it is the identity this load arrives with, and callers that read this
65
67
  // once as the page loads must not conclude there is none.
66
- const embedToken = urlParams.get(EMBED_TOKEN_PARAM);
67
- if (embedToken) {
68
- return embedToken;
68
+ //
69
+ // Only in a frame, and only for the parameter a session arrives on: a
70
+ // one-time token is redeemed nowhere else, so a top-level load still
71
+ // carrying one (a URL rewrite the browser refused) must not have it
72
+ // applied as a session — and never saved as one.
73
+ if (paramName === DEFAULT_TOKEN_PARAM && isFramed()) {
74
+ const embedToken = urlParams.get(EMBED_TOKEN_PARAM);
75
+ if (embedToken) {
76
+ return embedToken;
77
+ }
69
78
  }
70
79
  }
71
80
  catch (e) {
@@ -7,45 +7,29 @@
7
7
  * grant (RFC 8693), and keeps the result in memory only — never in storage — so
8
8
  * the session lives and dies with the frame.
9
9
  *
10
- * @internal
11
- */
12
- /** The query parameter a host platform puts the one-time token on. @internal */
13
- export declare const EMBED_TOKEN_PARAM = "ott";
14
- /**
15
- * Reads the one-time token off the current URL and strips it, so it is never
16
- * left in the address bar, in history, or in a shared link.
10
+ * Taking the token off the URL is unconditional — a one-time token must not be
11
+ * left in the address bar, in history, or in a shared link — but it is only
12
+ * reported back inside a frame, the only place one is redeemed. It is gone once
13
+ * the first client has taken it, so the session belongs to that client — an app
14
+ * creates one.
17
15
  *
18
- * @internal
19
- */
20
- export declare function takeEmbedTokenFromUrl(): string | null;
21
- /**
22
- * Whether this tab previously redeemed an embed token. Counts only inside a
23
- * frame: a top-level tab that once carried a token must fall back to the
24
- * regular login, or it could never sign in again.
16
+ * The exchange endpoint is rate limited per app, and its limiter refuses before
17
+ * the one-time token is redeemed — so a 429 leaves the token still valid and is
18
+ * worth retrying once.
25
19
  *
26
20
  * @internal
27
21
  */
28
- export declare function isEmbeddedTab(): boolean;
22
+ export declare const EMBED_TOKEN_PARAM = "ott";
23
+ /** @internal */
24
+ export declare function takeEmbedTokenFromUrl(): string | null;
25
+ /** @internal */
26
+ export declare function isFramed(): boolean;
29
27
  /** @internal */
30
- export declare function markEmbeddedTab(): void;
31
- /**
32
- * Trades a one-time embed token for an app-user session token. Resolves to
33
- * `null` when the platform refuses the token; never throws.
34
- *
35
- * @internal
36
- */
37
28
  export declare function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl, }: {
38
29
  serverUrl: string;
39
30
  appId: string;
40
31
  ott: string;
41
32
  fetchImpl?: typeof fetch;
42
33
  }): Promise<string | null>;
43
- /**
44
- * Covers the page with a plain "session ended" notice. An embedded session can
45
- * only be renewed by the host platform, so this stands in for the login
46
- * redirect, which cannot complete inside a frame. The copy points at the host
47
- * page: reloading the frame itself carries no token and lands here again.
48
- *
49
- * @internal
50
- */
34
+ /** @internal */
51
35
  export declare function showEmbedSessionEnded(): void;
@@ -7,22 +7,26 @@
7
7
  * grant (RFC 8693), and keeps the result in memory only — never in storage — so
8
8
  * the session lives and dies with the frame.
9
9
  *
10
+ * Taking the token off the URL is unconditional — a one-time token must not be
11
+ * left in the address bar, in history, or in a shared link — but it is only
12
+ * reported back inside a frame, the only place one is redeemed. It is gone once
13
+ * the first client has taken it, so the session belongs to that client — an app
14
+ * creates one.
15
+ *
16
+ * The exchange endpoint is rate limited per app, and its limiter refuses before
17
+ * the one-time token is redeemed — so a 429 leaves the token still valid and is
18
+ * worth retrying once.
19
+ *
10
20
  * @internal
11
21
  */
12
- /** The query parameter a host platform puts the one-time token on. @internal */
13
22
  export const EMBED_TOKEN_PARAM = "ott";
14
- const EMBED_TAB_KEY = "base44_embed_session";
15
23
  const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
16
24
  const SUBJECT_TOKEN_TYPE = "urn:base44:params:oauth:token-type:embed-ott";
17
25
  const RATE_LIMITED = 429;
18
26
  const RATE_LIMIT_RETRY_MS = 1000;
27
+ const EXCHANGE_TIMEOUT_MS = 15000;
19
28
  const SESSION_ENDED_ELEMENT_ID = "base44-embed-session-ended";
20
- /**
21
- * Reads the one-time token off the current URL and strips it, so it is never
22
- * left in the address bar, in history, or in a shared link.
23
- *
24
- * @internal
25
- */
29
+ /** @internal */
26
30
  export function takeEmbedTokenFromUrl() {
27
31
  if (typeof window === "undefined" || !window.location) {
28
32
  return null;
@@ -38,54 +42,20 @@ export function takeEmbedTokenFromUrl() {
38
42
  window.history.replaceState(window.history.state, "", url.toString());
39
43
  }
40
44
  catch (e) {
41
- // The token was read but the URL could not be rewritten (history blocked,
42
- // a sandboxed frame, replaceState rate-limited). Return it anyway: it is
43
- // this load's identity, and dropping it here would leave `?ott=` on the
44
- // URL for getAccessToken to hand back as if it were a session token.
45
45
  console.error("Error retrieving embed token from URL:", e);
46
46
  }
47
- return ott;
48
- }
49
- /**
50
- * Whether this tab previously redeemed an embed token. Counts only inside a
51
- * frame: a top-level tab that once carried a token must fall back to the
52
- * regular login, or it could never sign in again.
53
- *
54
- * @internal
55
- */
56
- export function isEmbeddedTab() {
57
- if (typeof window === "undefined") {
58
- return false;
59
- }
60
- // A cross-site frame is third-party storage, which some browsers block
61
- // outright — every access can throw, and the marker is then unavailable.
62
- try {
63
- return (window.self !== window.top &&
64
- window.sessionStorage.getItem(EMBED_TAB_KEY) === "1");
65
- }
66
- catch (_a) {
67
- return false;
68
- }
47
+ return isFramed() ? ott : null;
69
48
  }
70
49
  /** @internal */
71
- export function markEmbeddedTab() {
72
- try {
73
- window.sessionStorage.setItem(EMBED_TAB_KEY, "1");
74
- }
75
- catch (_a) {
76
- /* storage blocked */
77
- }
50
+ export function isFramed() {
51
+ return typeof window !== "undefined" && window.self !== window.top;
78
52
  }
79
- /**
80
- * Trades a one-time embed token for an app-user session token. Resolves to
81
- * `null` when the platform refuses the token; never throws.
82
- *
83
- * @internal
84
- */
53
+ /** @internal */
85
54
  export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fetch, }) {
86
55
  const post = () => fetchImpl(`${serverUrl}/api/apps/${appId}/auth/embed/token`, {
87
56
  method: "POST",
88
57
  headers: { "X-App-Id": String(appId) },
58
+ signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS),
89
59
  body: new URLSearchParams({
90
60
  grant_type: GRANT_TYPE,
91
61
  subject_token: ott,
@@ -94,7 +64,6 @@ export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fe
94
64
  });
95
65
  try {
96
66
  let response = await post();
97
- // The limiter refuses before the token is redeemed, so it is still valid.
98
67
  if (response.status === RATE_LIMITED) {
99
68
  await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_RETRY_MS));
100
69
  response = await post();
@@ -110,14 +79,7 @@ export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fe
110
79
  return null;
111
80
  }
112
81
  }
113
- /**
114
- * Covers the page with a plain "session ended" notice. An embedded session can
115
- * only be renewed by the host platform, so this stands in for the login
116
- * redirect, which cannot complete inside a frame. The copy points at the host
117
- * page: reloading the frame itself carries no token and lands here again.
118
- *
119
- * @internal
120
- */
82
+ /** @internal */
121
83
  export function showEmbedSessionEnded() {
122
84
  if (typeof document === "undefined" || !document.body) {
123
85
  return;
@@ -125,21 +87,26 @@ export function showEmbedSessionEnded() {
125
87
  if (document.getElementById(SESSION_ENDED_ELEMENT_ID)) {
126
88
  return;
127
89
  }
90
+ const style = document.createElement("style");
91
+ style.textContent =
92
+ `#${SESSION_ENDED_ELEMENT_ID}{--b44-bg:#fff;--b44-fg:#0f172a;--b44-muted:#475569;}` +
93
+ `@media (prefers-color-scheme:dark){#${SESSION_ENDED_ELEMENT_ID}` +
94
+ `{--b44-bg:#0f172a;--b44-fg:#f8fafc;--b44-muted:#94a3b8;}}`;
128
95
  const overlay = document.createElement("div");
129
96
  overlay.id = SESSION_ENDED_ELEMENT_ID;
130
97
  overlay.setAttribute("role", "alert");
131
98
  overlay.style.cssText =
132
99
  "position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;" +
133
- "justify-content:center;background:#fff;color:#0f172a;" +
100
+ "justify-content:center;background:var(--b44-bg);color:var(--b44-fg);" +
134
101
  "font-family:ui-sans-serif,system-ui,sans-serif;text-align:center;padding:2rem;";
135
102
  const title = document.createElement("h1");
136
103
  title.textContent = "Session ended";
137
104
  title.style.cssText = "font-size:1.5rem;font-weight:700;margin:0 0 .75rem;";
138
105
  const body = document.createElement("p");
139
106
  body.textContent = "Reload this page in your browser to start a new session.";
140
- body.style.cssText = "margin:0;color:#475569;";
107
+ body.style.cssText = "margin:0;color:var(--b44-muted);";
141
108
  const card = document.createElement("div");
142
109
  card.append(title, body);
143
- overlay.append(card);
110
+ overlay.append(style, card);
144
111
  document.body.append(overlay);
145
112
  }
@@ -33,11 +33,12 @@ export interface FetchWithAuthInit extends RequestInit {
33
33
  * has none, so nothing is sent.
34
34
  * @internal
35
35
  */
36
- export declare function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl, functionsVersion, platformHeaders, }: {
36
+ export declare function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl, functionsVersion, platformHeaders, waitForAuth, }: {
37
37
  axios: AxiosInstance;
38
38
  serviceRoleAxios: AxiosInstance;
39
39
  appId: string;
40
40
  serverUrl: string;
41
41
  functionsVersion?: string;
42
42
  platformHeaders?: Record<string, string>;
43
+ waitForAuth?: () => Promise<void>;
43
44
  }): (path: string, init?: FetchWithAuthInit) => Promise<Response>;
@@ -21,7 +21,7 @@
21
21
  * has none, so nothing is sent.
22
22
  * @internal
23
23
  */
24
- export function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl, functionsVersion, platformHeaders, }) {
24
+ export function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl, functionsVersion, platformHeaders, waitForAuth, }) {
25
25
  const inherited = new Headers(platformHeaders);
26
26
  const bearer = (client) => {
27
27
  const header = client.defaults.headers.common["Authorization"];
@@ -31,6 +31,9 @@ export function createFetchWithAuth({ axios, serviceRoleAxios, appId, serverUrl,
31
31
  };
32
32
  return async function fetchWithAuth(path, init = {}) {
33
33
  assertOwnOriginPath(path);
34
+ // The Authorization below is read off the axios defaults, which a session
35
+ // still being negotiated has not written yet.
36
+ await (waitForAuth === null || waitForAuth === void 0 ? void 0 : waitForAuth());
34
37
  const { fetch: transport = fetch, ...requestInit } = init;
35
38
  const headers = new Headers(init.headers);
36
39
  // A caller-supplied value always wins, so a route can hand the callee a
@@ -4,7 +4,6 @@ export interface RoomsSocketConfig {
4
4
  mountPath: string;
5
5
  transports: string[];
6
6
  appId: string;
7
- /** Asked on every connect, so the socket always carries the current session. */
8
7
  getToken: () => string | null;
9
8
  }
10
9
  export type TSocketRoom = string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.48-pr.282.028b41c",
3
+ "version": "0.8.48-pr.282.094dd5b",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",