@base44-preview/sdk 0.8.48-pr.282.8443e7d → 0.8.48-pr.282.86ef42a

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";
@@ -58,28 +58,20 @@ export function createClient(config) {
58
58
  const { serverUrl = "https://base44.app", appId, analytics, serviceToken, requiresAuth = false, appBaseUrl, options, functionsVersion, headers: optionalHeaders, } = 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
- const inBrowser = typeof window !== "undefined";
62
- // A host platform embeds the app with a one-time token on the iframe URL.
63
- // It comes off the URL here, before any other code can read it; the
64
- // exchange that turns it into a session runs once the modules exist.
65
61
  const embedOtt = takeEmbedTokenFromUrl();
66
- if (embedOtt) {
67
- markEmbeddedTab();
62
+ // Asked again on every use, so a session that arrives later — from the
63
+ // exchange, or from a login — reaches the socket and every other module.
64
+ // A declaration, not a const, so this block can stay above the auth module.
65
+ function getToken() {
66
+ var _a;
67
+ return (_a = userAuthModule.getToken()) !== null && _a !== void 0 ? _a : (embedOtt ? null : getAccessToken());
68
68
  }
69
- const embedded = Boolean(embedOtt) || isEmbeddedTab();
70
- // What an app passes as `token` is whatever getAccessToken() returned as the
71
- // page loaded. In an embedded frame that is the one-time token itself, or a
72
- // token an earlier visitor left in storage; neither is this session.
73
- const token = embedded ? undefined : config.token;
74
- let currentToken = token !== null && token !== void 0 ? token : null;
75
69
  const socketConfig = {
76
70
  serverUrl,
77
71
  mountPath: "/ws-user-apps/socket.io/",
78
72
  transports: ["websocket"],
79
73
  appId,
80
- // null (not undefined): an embedded frame's only identity is the one the
81
- // platform minted, so the socket must not fall back to a stored token.
82
- token: embedded ? null : token,
74
+ getToken,
83
75
  };
84
76
  let socket = null;
85
77
  const getSocket = () => {
@@ -90,6 +82,10 @@ export function createClient(config) {
90
82
  }
91
83
  return socket;
92
84
  };
85
+ // Apps read getAccessToken() as they load and pass the result in as `token`,
86
+ // so on an embedded load this is the OTT: proof of an identity, not one to
87
+ // send as a bearer. The exchange below is what turns it into a session.
88
+ const token = embedOtt ? undefined : config.token;
93
89
  const headers = {
94
90
  ...optionalHeaders,
95
91
  "X-App-Id": String(appId),
@@ -142,52 +138,51 @@ export function createClient(config) {
142
138
  appBaseUrl: normalizedAppBaseUrl,
143
139
  serverUrl,
144
140
  token,
145
- embedded,
141
+ embedded: Boolean(embedOtt),
142
+ // The socket carries its token on the handshake, so an identity change
143
+ // only reaches it by opening a new connection — or, on logout, by
144
+ // dropping the one still running as the user who just left.
145
+ onSessionChange: (hasSession) => hasSession ? socket === null || socket === void 0 ? void 0 : socket.reconnect() : socket === null || socket === void 0 ? void 0 : socket.disconnect(),
146
146
  });
147
147
  // Apply the access token before any module that may issue authenticated
148
148
  // requests during construction (notably analytics, which fires an init
149
149
  // event whose flush calls auth.me()). Without this, the first User/me
150
150
  // request is built before setToken runs and goes out unauthenticated.
151
- // An embedded load skips this: its session comes from the exchange below,
152
- // and a token an earlier visitor left in storage must not become it.
153
- if (inBrowser && !embedded) {
151
+ // Not in a frame: a stored token there belongs to an earlier visitor, not to
152
+ // this session, which only the exchange below can produce.
153
+ if (typeof window !== "undefined" && !embedOtt) {
154
154
  const accessToken = token || getAccessToken();
155
155
  if (accessToken) {
156
- currentToken = accessToken;
157
156
  userAuthModule.setToken(accessToken);
158
157
  }
159
158
  }
160
- // The one answer every module reads. Follows setToken and logout; outside an
161
- // embedded frame it still falls back to storage, so a login in another tab is
162
- // picked up on the next connection, as before.
163
- const getToken = () => {
164
- if (userAuthModule.hasToken()) {
165
- return currentToken;
166
- }
167
- return embedded ? null : getAccessToken();
168
- };
169
- const applyToken = (newToken, saveToStorage) => {
170
- userAuthModule.setToken(newToken, saveToStorage);
171
- currentToken = newToken;
172
- socketConfig.token = newToken;
173
- if (socket) {
174
- socket.updateConfig({ token: newToken });
175
- }
176
- };
177
- // Resolves once the embedded session is in hand (or known not to be coming);
178
- // immediately for every other client. The exchanged token is applied without
179
- // persisting it: the session must not outlive the frame it was minted for.
180
- const authReady = embedOtt
181
- ? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt }).then((sessionToken) => {
159
+ const session = embedOtt
160
+ ? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt })
161
+ : null;
162
+ // Settles once the exchanged session is applied (memory only); at once
163
+ // otherwise. Never rejects: every request waits on it, so a failure here
164
+ // must not turn into a rejection on each of them.
165
+ const authReady = session
166
+ ? session
167
+ .then((sessionToken) => {
168
+ var _a;
182
169
  if (sessionToken) {
183
- applyToken(sessionToken, false);
170
+ userAuthModule.setToken(sessionToken, false);
171
+ return;
184
172
  }
173
+ // The app is about to run anonymous. Say why, once, instead of
174
+ // leaving only the 401s that follow.
175
+ const error = new Error("Base44: the embed token was refused, so this app is not signed in.");
176
+ console.error(error.message);
177
+ (_a = options === null || options === void 0 ? void 0 : options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
178
+ })
179
+ .catch((e) => {
180
+ console.error("Base44: applying the embedded session failed:", e);
185
181
  })
186
182
  : Promise.resolve();
187
- if (embedOtt) {
188
- // Requests issued while the exchange is in flight wait for it and carry the
189
- // session it produces. Registered after createAxiosClient's interceptors,
190
- // so it runs first and the anonymous-visitor header sees the Authorization.
183
+ if (session) {
184
+ // Requests issued during the exchange wait for it. Registered after createAxiosClient's
185
+ // interceptors so it runs first and the anonymous-visitor header sees the Authorization.
191
186
  for (const client of [axiosClient, functionsAxiosClient]) {
192
187
  client.interceptors.request.use(async (requestConfig) => {
193
188
  await authReady;
@@ -205,7 +200,10 @@ export function createClient(config) {
205
200
  // URL needs an absolute host, so fall back to the page origin.
206
201
  host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_a = window.location) === null || _a === void 0 ? void 0 : _a.origin : undefined),
207
202
  functionsVersion,
208
- getAuthToken: getToken,
203
+ getAuthToken: async () => {
204
+ await authReady;
205
+ return getToken();
206
+ },
209
207
  mintConnectionToken: async (actorName, room, connectionId) => {
210
208
  await authReady;
211
209
  const authToken = getToken();
@@ -233,6 +231,7 @@ export function createClient(config) {
233
231
  connectors: createUserConnectorsModule(axiosClient, appId),
234
232
  auth: userAuthModule,
235
233
  functions: createFunctionsModule(functionsAxiosClient, appId, {
234
+ waitForAuth: () => authReady,
236
235
  getAuthHeaders: () => {
237
236
  const headers = {};
238
237
  const sessionToken = getToken();
@@ -248,6 +247,8 @@ export function createClient(config) {
248
247
  getSocket,
249
248
  appId,
250
249
  serverUrl,
250
+ // Read synchronously, unlike everything else: these build a URL rather
251
+ // than issue a request, so during the exchange they see no token.
251
252
  getToken,
252
253
  }),
253
254
  aiGateway: createAiGatewayModule({ serverUrl, getToken, appId }),
@@ -295,7 +296,10 @@ export function createClient(config) {
295
296
  getSocket,
296
297
  appId,
297
298
  serverUrl,
298
- getToken: () => token,
299
+ // The user's token, as on the user-scoped module: the only thing this is
300
+ // read for is the `?token=` on a channel URL handed to that user, which
301
+ // the app's service credential must never end up in.
302
+ getToken,
299
303
  }),
300
304
  aiGateway: createAiGatewayModule({
301
305
  serverUrl,
@@ -336,12 +340,12 @@ export function createClient(config) {
336
340
  serverUrl,
337
341
  functionsVersion,
338
342
  platformHeaders: optionalHeaders,
343
+ waitForAuth: () => authReady,
339
344
  }),
340
345
  /**
341
346
  * Sets a new authentication token for all subsequent requests.
342
347
  *
343
348
  * @param newToken - The new authentication token
344
- * @param saveToStorage - Whether to also keep the token in local storage. Defaults to `true`.
345
349
  *
346
350
  * @example
347
351
  * ```typescript
@@ -353,8 +357,8 @@ export function createClient(config) {
353
357
  * base44.setToken(access_token);
354
358
  * ```
355
359
  */
356
- setToken(newToken, saveToStorage = true) {
357
- applyToken(newToken, saveToStorage);
360
+ setToken(newToken) {
361
+ userAuthModule.setToken(newToken, true);
358
362
  },
359
363
  /**
360
364
  * 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
  *
@@ -82,10 +82,13 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
82
82
  // Tracked here rather than read off `axios.defaults` so the answer stays tied
83
83
  // to the identity transitions below (`setToken`, `logout`) instead of to the
84
84
  // header a caller may have set on the instance directly.
85
- let hasAccessToken = Boolean(options.token);
85
+ let accessToken = options.token || null;
86
86
  return {
87
87
  hasToken() {
88
- return hasAccessToken;
88
+ return accessToken !== null;
89
+ },
90
+ getToken() {
91
+ return accessToken;
89
92
  },
90
93
  isEmbedded() {
91
94
  return Boolean(options.embedded);
@@ -113,8 +116,11 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
113
116
  if (typeof window === "undefined") {
114
117
  throw new Error("Login method can only be used in a browser environment");
115
118
  }
116
- // An embedded session can only be renewed by the host platform; the
117
- // 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.
118
124
  if (options.embedded) {
119
125
  showEmbedSessionEnded();
120
126
  return;
@@ -155,13 +161,18 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
155
161
  },
156
162
  // Logout the current user
157
163
  logout(redirectUrl) {
158
- // 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.
159
168
  delete axios.defaults.headers.common["Authorization"];
169
+ delete functionsAxiosClient.defaults.headers.common["Authorization"];
160
170
  // Drop identity resolved under the previous session: a `me()` already in
161
171
  // flight would otherwise resolve into callers that run after the logout.
162
172
  clearPendingMe();
163
173
  resetAnalyticsSessionContext();
164
- hasAccessToken = false;
174
+ accessToken = null;
175
+ (_a = options.onSessionChange) === null || _a === void 0 ? void 0 : _a.call(options, false);
165
176
  // Only do the rest if in a browser environment
166
177
  if (typeof window !== "undefined") {
167
178
  // Remove token from localStorage
@@ -175,6 +186,13 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
175
186
  console.error("Failed to remove token from localStorage:", e);
176
187
  }
177
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
+ }
178
196
  // Determine the from_url parameter
179
197
  const fromUrl = redirectUrl || window.location.href;
180
198
  // Redirect to server-side logout endpoint to clear HTTP-only cookies
@@ -184,20 +202,24 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
184
202
  },
185
203
  // Set authentication token
186
204
  setToken(token, saveToStorage = true) {
205
+ var _a;
187
206
  if (!token)
188
207
  return;
208
+ // An embedded session belongs to the frame the platform minted it for.
209
+ // Persisting it would let it outlive that frame and be picked up as the
210
+ // identity on a later top-level visit, so storage is refused outright.
211
+ const persist = saveToStorage && !options.embedded;
189
212
  // Same reasoning as in `logout`: the identity changes here, so anything
190
213
  // resolved for the previous one must not be handed to later callers.
191
214
  clearPendingMe();
192
215
  resetAnalyticsSessionContext();
193
- hasAccessToken = true;
216
+ accessToken = token;
194
217
  // handle token change for axios clients
195
218
  axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
196
219
  functionsAxiosClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
220
+ (_a = options.onSessionChange) === null || _a === void 0 ? void 0 : _a.call(options, true);
197
221
  // Save token to localStorage if requested
198
- if (saveToStorage &&
199
- typeof window !== "undefined" &&
200
- window.localStorage) {
222
+ if (persist && typeof window !== "undefined" && window.localStorage) {
201
223
  try {
202
224
  window.localStorage.setItem("base44_access_token", token);
203
225
  // Set "token" that is set by the built-in SDK of platform version 2
@@ -103,6 +103,13 @@ export interface AuthModuleOptions {
103
103
  * session is minted by the platform and cannot be renewed by a login redirect.
104
104
  */
105
105
  embedded?: boolean;
106
+ /**
107
+ * Called whenever the identity changes: `true` when a token is set, `false`
108
+ * on logout. Lets the client reach what holds its own copy of the identity —
109
+ * the realtime socket, which carries a token on the handshake and so has to
110
+ * open a new connection to pick one up, or drop the one it has.
111
+ */
112
+ onSessionChange?: (hasSession: boolean) => void;
106
113
  }
107
114
  /**
108
115
  * 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.
@@ -187,7 +194,9 @@ export interface AuthModule {
187
194
  /**
188
195
  * Whether the app is running embedded in a host platform.
189
196
  *
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.
197
+ * 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()} and {@linkcode AuthModule.logout | logout()} show 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.
198
+ *
199
+ * Because the session lives in memory, it does not survive a full page load inside the frame: routing within the app keeps it, while a reload or a real navigation ends it and the platform has to embed the app again — and this returns `false` on that load, since nothing about it says it was embedded. Prefer client-side navigation in an embedded app.
191
200
  *
192
201
  * @returns `true` when the app was embedded by a host platform, `false` otherwise.
193
202
  *
@@ -560,4 +569,9 @@ export interface InternalAuthModule extends AuthModule {
560
569
  * could not succeed without a session, not to decide that one is valid.
561
570
  */
562
571
  hasToken(): boolean;
572
+ /**
573
+ * The access token currently set on the client, or `null` when there is none.
574
+ * Follows {@linkcode AuthModule.setToken | setToken} and {@linkcode AuthModule.logout | logout}.
575
+ */
576
+ getToken(): string | null;
563
577
  }
@@ -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,44 +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 (by reloading its own page), so this
46
- * stands in for the login redirect, which cannot complete inside a frame.
47
- *
48
- * @internal
49
- */
34
+ /** @internal */
50
35
  export declare function showEmbedSessionEnded(): void;
@@ -7,81 +7,55 @@
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;
29
33
  }
34
+ let ott = null;
30
35
  try {
31
36
  const url = new URL(window.location.href);
32
- const ott = url.searchParams.get(EMBED_TOKEN_PARAM);
37
+ ott = url.searchParams.get(EMBED_TOKEN_PARAM);
33
38
  if (!ott) {
34
39
  return null;
35
40
  }
36
41
  url.searchParams.delete(EMBED_TOKEN_PARAM);
37
42
  window.history.replaceState(window.history.state, "", url.toString());
38
- return ott;
39
43
  }
40
44
  catch (e) {
41
45
  console.error("Error retrieving embed token from URL:", e);
42
- return null;
43
- }
44
- }
45
- /**
46
- * Whether this tab previously redeemed an embed token. Counts only inside a
47
- * frame: a top-level tab that once carried a token must fall back to the
48
- * regular login, or it could never sign in again.
49
- *
50
- * @internal
51
- */
52
- export function isEmbeddedTab() {
53
- if (typeof window === "undefined") {
54
- return false;
55
- }
56
- // A cross-site frame is third-party storage, which some browsers block
57
- // outright — every access can throw, and the marker is then unavailable.
58
- try {
59
- return (window.self !== window.top &&
60
- window.sessionStorage.getItem(EMBED_TAB_KEY) === "1");
61
- }
62
- catch (_a) {
63
- return false;
64
46
  }
47
+ return isFramed() ? ott : null;
65
48
  }
66
49
  /** @internal */
67
- export function markEmbeddedTab() {
68
- try {
69
- window.sessionStorage.setItem(EMBED_TAB_KEY, "1");
70
- }
71
- catch (_a) {
72
- /* storage blocked */
73
- }
50
+ export function isFramed() {
51
+ return typeof window !== "undefined" && window.self !== window.top;
74
52
  }
75
- /**
76
- * Trades a one-time embed token for an app-user session token. Resolves to
77
- * `null` when the platform refuses the token; never throws.
78
- *
79
- * @internal
80
- */
53
+ /** @internal */
81
54
  export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fetch, }) {
82
55
  const post = () => fetchImpl(`${serverUrl}/api/apps/${appId}/auth/embed/token`, {
83
56
  method: "POST",
84
57
  headers: { "X-App-Id": String(appId) },
58
+ signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS),
85
59
  body: new URLSearchParams({
86
60
  grant_type: GRANT_TYPE,
87
61
  subject_token: ott,
@@ -90,7 +64,6 @@ export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fe
90
64
  });
91
65
  try {
92
66
  let response = await post();
93
- // The limiter refuses before the token is redeemed, so it is still valid.
94
67
  if (response.status === RATE_LIMITED) {
95
68
  await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_RETRY_MS));
96
69
  response = await post();
@@ -106,13 +79,7 @@ export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fe
106
79
  return null;
107
80
  }
108
81
  }
109
- /**
110
- * Covers the page with a plain "session ended" notice. An embedded session can
111
- * only be renewed by the host platform (by reloading its own page), so this
112
- * stands in for the login redirect, which cannot complete inside a frame.
113
- *
114
- * @internal
115
- */
82
+ /** @internal */
116
83
  export function showEmbedSessionEnded() {
117
84
  if (typeof document === "undefined" || !document.body) {
118
85
  return;
@@ -120,21 +87,26 @@ export function showEmbedSessionEnded() {
120
87
  if (document.getElementById(SESSION_ENDED_ELEMENT_ID)) {
121
88
  return;
122
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;}}`;
123
95
  const overlay = document.createElement("div");
124
96
  overlay.id = SESSION_ENDED_ELEMENT_ID;
125
97
  overlay.setAttribute("role", "alert");
126
98
  overlay.style.cssText =
127
99
  "position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;" +
128
- "justify-content:center;background:#fff;color:#0f172a;" +
100
+ "justify-content:center;background:var(--b44-bg);color:var(--b44-fg);" +
129
101
  "font-family:ui-sans-serif,system-ui,sans-serif;text-align:center;padding:2rem;";
130
102
  const title = document.createElement("h1");
131
103
  title.textContent = "Session ended";
132
104
  title.style.cssText = "font-size:1.5rem;font-weight:700;margin:0 0 .75rem;";
133
105
  const body = document.createElement("p");
134
- body.textContent = "Refresh the page to continue.";
135
- body.style.cssText = "margin:0;color:#475569;";
106
+ body.textContent = "Reload this page in your browser to start a new session.";
107
+ body.style.cssText = "margin:0;color:var(--b44-muted);";
136
108
  const card = document.createElement("div");
137
109
  card.append(title, body);
138
- overlay.append(card);
110
+ overlay.append(style, card);
139
111
  document.body.append(overlay);
140
112
  }
@@ -33,11 +33,13 @@ 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
+ /** Resolves once the client's session is settled. */
44
+ waitForAuth?: () => Promise<void>;
43
45
  }): (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,9 +4,8 @@ export interface RoomsSocketConfig {
4
4
  mountPath: string;
5
5
  transports: string[];
6
6
  appId: string;
7
- /** `undefined` means unknown — the socket looks in storage; `null` means
8
- * known to be anonymous, so storage is never consulted. */
9
- token?: string | null;
7
+ /** Asked on every connect, so the socket always carries the current session. */
8
+ getToken: () => string | null;
10
9
  }
11
10
  export type TSocketRoom = string;
12
11
  export type TJsonStr = string;
@@ -42,7 +41,7 @@ export declare function RoomsSocket({ config }: {
42
41
  leave: (room: string) => void;
43
42
  }>;
44
43
  subscribeToRoom: (room: TSocketRoom, handlers: Partial<{ [k in TEvent]: THandler<k>; }>) => () => void;
45
- updateConfig: (config: Partial<RoomsSocketConfig>) => void;
44
+ reconnect: () => void;
46
45
  updateModel: (room: string, data: any) => Promise<void>;
47
46
  disconnect: () => void;
48
47
  };
@@ -1,5 +1,4 @@
1
1
  import { io } from "socket.io-client";
2
- import { getAccessToken } from "./auth-utils.js";
3
2
  import { getAnalyticsSessionId } from "../modules/analytics.js";
4
3
  const ROOM_LEAVE_GRACE_MS = 250;
5
4
  function initializeSocket(config, handlers) {
@@ -7,7 +6,7 @@ function initializeSocket(config, handlers) {
7
6
  // handshake so the backend can verify room access for anonymous agent
8
7
  // conversations (mirrors the X-Base44-Anonymous-Id HTTP header). Authenticated
9
8
  // clients are identified by their token instead.
10
- const resolvedToken = config.token === undefined ? getAccessToken() : config.token;
9
+ const resolvedToken = config.getToken();
11
10
  const query = {
12
11
  app_id: config.appId,
13
12
  token: resolvedToken,
@@ -41,7 +40,6 @@ function initializeSocket(config, handlers) {
41
40
  return socket;
42
41
  }
43
42
  export function RoomsSocket({ config }) {
44
- let currentConfig = { ...config };
45
43
  const roomsToListeners = {};
46
44
  const pendingRoomLeaves = {};
47
45
  const handlers = {
@@ -83,13 +81,10 @@ export function RoomsSocket({ config }) {
83
81
  socket.disconnect();
84
82
  }
85
83
  }
86
- function updateConfig(config) {
84
+ /** Drops the connection and opens a new one with the current token. */
85
+ function reconnect() {
87
86
  cleanup();
88
- currentConfig = {
89
- ...currentConfig,
90
- ...config,
91
- };
92
- socket = initializeSocket(currentConfig, handlers);
87
+ socket = initializeSocket(config, handlers);
93
88
  }
94
89
  function joinRoom(room) {
95
90
  socket.emit("join", room);
@@ -162,7 +157,7 @@ export function RoomsSocket({ config }) {
162
157
  return {
163
158
  socket,
164
159
  subscribeToRoom,
165
- updateConfig,
160
+ reconnect,
166
161
  updateModel,
167
162
  disconnect,
168
163
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.48-pr.282.8443e7d",
3
+ "version": "0.8.48-pr.282.86ef42a",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",