@base44-preview/sdk 0.8.48-pr.281.97e1467 → 0.8.48-pr.282.081f33c

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,6 +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, isFramed, markEmbeddedTab, takeEmbedTokenFromUrl, } from "./utils/embed-session.js";
8
9
  import { createFetchWithAuth } from "./utils/fetch-with-auth.js";
9
10
  import { createFunctionsModule } from "./modules/functions.js";
10
11
  import { createAgentsModule } from "./modules/agents.js";
@@ -54,25 +55,22 @@ import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
54
55
  */
55
56
  export function createClient(config) {
56
57
  var _a, _b, _c, _d;
57
- const { serverUrl = "https://base44.app", appId, analytics, token, serviceToken, requiresAuth = false, appBaseUrl, options, functionsVersion, headers: optionalHeaders, } = config;
58
+ const { serverUrl = "https://base44.app", appId, analytics, serviceToken, requiresAuth = false, appBaseUrl, options, functionsVersion, headers: optionalHeaders, } = config;
58
59
  // Normalize appBaseUrl to always be a string (empty if not provided or invalid)
59
60
  const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";
60
- const socketConfig = {
61
- serverUrl,
62
- mountPath: "/ws-user-apps/socket.io/",
63
- transports: ["websocket"],
64
- appId,
65
- token,
66
- };
67
- let socket = null;
68
- const getSocket = () => {
69
- if (!socket) {
70
- socket = RoomsSocket({
71
- config: socketConfig,
72
- });
73
- }
74
- return socket;
75
- };
61
+ // Always taken off the URL, even outside a frame: a one-time token must not
62
+ // be left in the address bar, in history, or in a shared link.
63
+ const urlOtt = takeEmbedTokenFromUrl();
64
+ // Only redeemed inside a frame. Opened as a normal tab, the same URL falls
65
+ // back to the app's own login, which works there and cannot work in a frame.
66
+ const embedOtt = isFramed() ? urlOtt : null;
67
+ if (embedOtt) {
68
+ markEmbeddedTab(appId);
69
+ }
70
+ const embedded = Boolean(embedOtt) || isEmbeddedTab(appId);
71
+ // The passed token is the OTT itself whenever one came in on the URL, and a
72
+ // stale stored one in a frame. Neither of those is this session.
73
+ const token = urlOtt !== null || embedded ? undefined : config.token;
76
74
  const headers = {
77
75
  ...optionalHeaders,
78
76
  "X-App-Id": String(appId),
@@ -125,26 +123,103 @@ export function createClient(config) {
125
123
  appBaseUrl: normalizedAppBaseUrl,
126
124
  serverUrl,
127
125
  token,
126
+ embedded,
127
+ onEmbedSessionEnded: options === null || options === void 0 ? void 0 : options.onEmbedSessionEnded,
128
128
  });
129
129
  // Apply the access token before any module that may issue authenticated
130
130
  // requests during construction (notably analytics, which fires an init
131
131
  // event whose flush calls auth.me()). Without this, the first User/me
132
132
  // request is built before setToken runs and goes out unauthenticated.
133
- if (typeof window !== "undefined") {
133
+ // Skipped in a frame: the session comes from the exchange below.
134
+ if (typeof window !== "undefined" && !embedded) {
134
135
  const accessToken = token || getAccessToken();
135
136
  if (accessToken) {
136
137
  userAuthModule.setToken(accessToken);
137
138
  }
138
139
  }
140
+ // Live token for every module; outside a frame it still falls back to storage.
141
+ const getToken = () => { var _a; return (_a = userAuthModule.getToken()) !== null && _a !== void 0 ? _a : (embedded ? null : getAccessToken()); };
142
+ const socketConfig = {
143
+ serverUrl,
144
+ mountPath: "/ws-user-apps/socket.io/",
145
+ transports: ["websocket"],
146
+ appId,
147
+ getToken,
148
+ };
149
+ let socket = null;
150
+ const getSocket = () => {
151
+ if (!socket) {
152
+ socket = RoomsSocket({ config: socketConfig });
153
+ }
154
+ return socket;
155
+ };
156
+ const applyToken = (newToken, saveToStorage) => {
157
+ userAuthModule.setToken(newToken, saveToStorage);
158
+ socket === null || socket === void 0 ? void 0 : socket.reconnect();
159
+ };
160
+ // Settles once the exchanged session is applied (memory only); at once
161
+ // otherwise. Never rejects: every request waits on it, so a failure here
162
+ // must not turn into a rejection on each of them.
163
+ const authReady = embedOtt
164
+ ? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt })
165
+ .then((sessionToken) => {
166
+ var _a;
167
+ if (sessionToken) {
168
+ applyToken(sessionToken, false);
169
+ return;
170
+ }
171
+ // The app is about to run anonymous. Say why, once, instead of
172
+ // leaving only the 401s that follow.
173
+ const error = new Error("Base44: the embed token was refused, so this app is not signed in.");
174
+ console.error(error.message);
175
+ (_a = options === null || options === void 0 ? void 0 : options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
176
+ })
177
+ .catch((e) => {
178
+ console.error("Base44: applying the embedded session failed:", e);
179
+ })
180
+ : Promise.resolve();
181
+ // Everything that can wait for the session does. `aiGateway.connection()`
182
+ // and the agents connect URLs cannot: they hand back a value, not a promise,
183
+ // and a value read now would stay wrong after the session arrives. They warn
184
+ // instead of failing quietly.
185
+ let warnedEarlyRead = false;
186
+ const getTokenNow = () => {
187
+ const sessionToken = getToken();
188
+ if (sessionToken === null && embedOtt && !warnedEarlyRead) {
189
+ warnedEarlyRead = true;
190
+ console.warn("Base44: read a token before the embedded session was ready, so it is " +
191
+ "empty. Await a call such as base44.auth.me() before building a " +
192
+ "client or a URL that keeps the token.");
193
+ }
194
+ return sessionToken;
195
+ };
196
+ if (embedOtt) {
197
+ // Requests issued during the exchange wait for it. Registered after createAxiosClient's
198
+ // interceptors so it runs first and the anonymous-visitor header sees the Authorization.
199
+ for (const client of [axiosClient, functionsAxiosClient]) {
200
+ client.interceptors.request.use(async (requestConfig) => {
201
+ await authReady;
202
+ const sessionToken = getToken();
203
+ if (sessionToken && !requestConfig.headers.get("Authorization")) {
204
+ requestConfig.headers.set("Authorization", `Bearer ${sessionToken}`);
205
+ }
206
+ return requestConfig;
207
+ });
208
+ }
209
+ }
139
210
  const actorsModule = createActorsModule({
140
211
  appId,
141
212
  // serverUrl is often relative/empty (same-origin app); the proxy-fallback
142
213
  // URL needs an absolute host, so fall back to the page origin.
143
214
  host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_a = window.location) === null || _a === void 0 ? void 0 : _a.origin : undefined),
144
215
  functionsVersion,
145
- getAuthToken: () => token || getAccessToken(),
216
+ getAuthToken: async () => {
217
+ await authReady;
218
+ return getToken();
219
+ },
146
220
  mintConnectionToken: async (actorName, room, connectionId) => {
147
- const authToken = token || getAccessToken();
221
+ await authReady;
222
+ const authToken = getToken();
148
223
  return await actorsAxiosClient.post(`/apps/${appId}/actors/${encodeURIComponent(actorName)}/connection-token`, { room, connection_id: connectionId }, {
149
224
  headers: {
150
225
  ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
@@ -169,12 +244,12 @@ export function createClient(config) {
169
244
  connectors: createUserConnectorsModule(axiosClient, appId),
170
245
  auth: userAuthModule,
171
246
  functions: createFunctionsModule(functionsAxiosClient, appId, {
247
+ waitForAuth: () => authReady,
172
248
  getAuthHeaders: () => {
173
249
  const headers = {};
174
- // Get current token from storage or initial config
175
- const currentToken = token || getAccessToken();
176
- if (currentToken) {
177
- headers["Authorization"] = `Bearer ${currentToken}`;
250
+ const sessionToken = getToken();
251
+ if (sessionToken) {
252
+ headers["Authorization"] = `Bearer ${sessionToken}`;
178
253
  }
179
254
  return headers;
180
255
  },
@@ -185,9 +260,13 @@ export function createClient(config) {
185
260
  getSocket,
186
261
  appId,
187
262
  serverUrl,
188
- token,
263
+ getToken: getTokenNow,
264
+ }),
265
+ aiGateway: createAiGatewayModule({
266
+ serverUrl,
267
+ getToken: getTokenNow,
268
+ appId,
189
269
  }),
190
- aiGateway: createAiGatewayModule({ serverUrl, token, appId }),
191
270
  appLogs: createAppLogsModule(axiosClient, appId),
192
271
  app: createAppModule(axiosClient, appId),
193
272
  users: createUsersModule(axiosClient, appId),
@@ -232,9 +311,16 @@ export function createClient(config) {
232
311
  getSocket,
233
312
  appId,
234
313
  serverUrl,
235
- token,
314
+ // The user's token, as on the user-scoped module: the only thing this is
315
+ // read for is the `?token=` on a channel URL handed to that user, which
316
+ // the app's service credential must never end up in.
317
+ getToken: getTokenNow,
318
+ }),
319
+ aiGateway: createAiGatewayModule({
320
+ serverUrl,
321
+ getToken: () => serviceToken,
322
+ appId,
236
323
  }),
237
- aiGateway: createAiGatewayModule({ serverUrl, token: serviceToken, appId }),
238
324
  appLogs: createAppLogsModule(serviceRoleAxiosClient, appId),
239
325
  cleanup: () => {
240
326
  if (socket) {
@@ -269,6 +355,7 @@ export function createClient(config) {
269
355
  serverUrl,
270
356
  functionsVersion,
271
357
  platformHeaders: optionalHeaders,
358
+ waitForAuth: () => authReady,
272
359
  }),
273
360
  /**
274
361
  * Sets a new authentication token for all subsequent requests.
@@ -286,13 +373,7 @@ export function createClient(config) {
286
373
  * ```
287
374
  */
288
375
  setToken(newToken) {
289
- userModules.auth.setToken(newToken);
290
- if (socket) {
291
- socket.updateConfig({
292
- token: newToken,
293
- });
294
- }
295
- socketConfig.token = newToken;
376
+ applyToken(newToken, true);
296
377
  },
297
378
  /**
298
379
  * Gets the current client configuration.
@@ -22,6 +22,18 @@ export interface CreateClientOptions {
22
22
  * are usually {@linkcode Base44Error} instances — check `error.status`.
23
23
  */
24
24
  onError?: (error: Error) => void;
25
+ /**
26
+ * Called when an embedded app's session ends, instead of the SDK's built-in
27
+ * notice.
28
+ *
29
+ * A platform-embedded app holds its session in memory and only the platform
30
+ * can mint the next one, so there is no login page to send the visitor to.
31
+ * When {@link AuthModule.redirectToLogin | redirectToLogin()} or
32
+ * {@link AuthModule.logout | logout()} is reached in that state the SDK
33
+ * covers the page with a plain "session ended" notice; pass this to render
34
+ * your own instead. See {@link AuthModule.isEmbedded | auth.isEmbedded()}.
35
+ */
36
+ onEmbedSessionEnded?: () => void;
25
37
  /**
26
38
  * Forces the actors transport. `"auto"` (default) connects directly to the
27
39
  * actor and falls back to the platform proxy when the app's actors don't
@@ -12,7 +12,7 @@ interface ActorsConfig {
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
14
  * anonymous connects omit it. */
15
- getAuthToken(): string | null | undefined;
15
+ getAuthToken(): string | null | undefined | Promise<string | null | undefined>;
16
16
  /** Same semantics as function calls: editors with a non-prod version get the
17
17
  * draft actor script; everyone else gets the published one. */
18
18
  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;
@@ -1,2 +1,2 @@
1
1
  import { AgentsModule, AgentsModuleConfig } from "./agents.types.js";
2
- export declare function createAgentsModule({ axios, getSocket, appId, serverUrl, token, }: AgentsModuleConfig): AgentsModule;
2
+ export declare function createAgentsModule({ axios, getSocket, appId, serverUrl, getToken, }: AgentsModuleConfig): AgentsModule;
@@ -1,5 +1,4 @@
1
- import { getAccessToken } from "../utils/auth-utils.js";
2
- export function createAgentsModule({ axios, getSocket, appId, serverUrl, token, }) {
1
+ export function createAgentsModule({ axios, getSocket, appId, serverUrl, getToken, }) {
3
2
  const baseURL = `/apps/${appId}/agents`;
4
3
  // Track active conversations
5
4
  const currentConversations = {};
@@ -56,7 +55,7 @@ export function createAgentsModule({ axios, getSocket, appId, serverUrl, token,
56
55
  };
57
56
  const getWhatsAppConnectURL = (agentName) => {
58
57
  const baseUrl = `${serverUrl}/api/apps/${appId}/agents/${encodeURIComponent(agentName)}/whatsapp`;
59
- const accessToken = token !== null && token !== void 0 ? token : getAccessToken();
58
+ const accessToken = getToken();
60
59
  if (accessToken) {
61
60
  return `${baseUrl}?token=${accessToken}`;
62
61
  }
@@ -67,7 +66,7 @@ export function createAgentsModule({ axios, getSocket, appId, serverUrl, token,
67
66
  };
68
67
  const getTelegramConnectURL = (agentName) => {
69
68
  const baseUrl = `${serverUrl}/api/apps/${appId}/agents/${encodeURIComponent(agentName)}/telegram`;
70
- const accessToken = token !== null && token !== void 0 ? token : getAccessToken();
69
+ const accessToken = getToken();
71
70
  if (accessToken) {
72
71
  return `${baseUrl}?token=${accessToken}`;
73
72
  }
@@ -160,8 +160,8 @@ export interface AgentsModuleConfig {
160
160
  appId: string;
161
161
  /** Server URL */
162
162
  serverUrl?: string;
163
- /** Authentication token */
164
- token?: string;
163
+ /** Returns the current authentication token, if any */
164
+ getToken: () => string | null | undefined;
165
165
  }
166
166
  /**
167
167
  * Agents module for managing AI agent conversations.
@@ -1,2 +1,2 @@
1
1
  import { AiGatewayModule, AiGatewayModuleConfig } from "./ai-gateway.types.js";
2
- export declare function createAiGatewayModule({ serverUrl, token, appId, }: AiGatewayModuleConfig): AiGatewayModule;
2
+ export declare function createAiGatewayModule({ serverUrl, getToken, appId, }: AiGatewayModuleConfig): AiGatewayModule;
@@ -1,10 +1,9 @@
1
- import { getAccessToken } from "../utils/auth-utils.js";
2
- export function createAiGatewayModule({ serverUrl, token, appId, }) {
1
+ export function createAiGatewayModule({ serverUrl, getToken, appId, }) {
3
2
  const connection = () => {
4
3
  var _a;
5
4
  return ({
6
5
  baseURL: `${serverUrl}/api/apps/${appId}/ai/openai/v1`,
7
- token: (_a = token !== null && token !== void 0 ? token : getAccessToken()) !== null && _a !== void 0 ? _a : "",
6
+ token: (_a = getToken()) !== null && _a !== void 0 ? _a : "",
8
7
  });
9
8
  };
10
9
  return {
@@ -17,8 +17,8 @@ export interface AiGatewayConnection {
17
17
  export interface AiGatewayModuleConfig {
18
18
  /** Server URL */
19
19
  serverUrl?: string;
20
- /** Authentication token */
21
- token?: string;
20
+ /** Returns the current authentication token, if any */
21
+ getToken: () => string | null | undefined;
22
22
  /** Application ID */
23
23
  appId: string;
24
24
  }
@@ -1,4 +1,5 @@
1
1
  import { resetAnalyticsSessionContext } from "./analytics.js";
2
+ import { showEmbedSessionEnded } from "../utils/embed-session.js";
2
3
  function isInsideIframe() {
3
4
  if (typeof window === "undefined")
4
5
  return false;
@@ -81,10 +82,27 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
81
82
  // Tracked here rather than read off `axios.defaults` so the answer stays tied
82
83
  // to the identity transitions below (`setToken`, `logout`) instead of to the
83
84
  // header a caller may have set on the instance directly.
84
- let hasAccessToken = Boolean(options.token);
85
+ let accessToken = options.token || null;
86
+ // Where an embedded app goes when it has no session: nowhere. Only the host
87
+ // platform can mint the next one — a login here would sign the visitor in as
88
+ // an app user, a different identity from the platform user the frame is for.
89
+ // The app's own notice wins when it passed one.
90
+ const endEmbeddedSession = () => {
91
+ if (options.onEmbedSessionEnded) {
92
+ options.onEmbedSessionEnded();
93
+ return;
94
+ }
95
+ showEmbedSessionEnded();
96
+ };
85
97
  return {
86
98
  hasToken() {
87
- return hasAccessToken;
99
+ return accessToken !== null;
100
+ },
101
+ getToken() {
102
+ return accessToken;
103
+ },
104
+ isEmbedded() {
105
+ return Boolean(options.embedded);
88
106
  },
89
107
  // Get current user information
90
108
  async me() {
@@ -109,6 +127,14 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
109
127
  if (typeof window === "undefined") {
110
128
  throw new Error("Login method can only be used in a browser environment");
111
129
  }
130
+ // Only the host platform can sign a platform user in, so there is no
131
+ // login page to send them to. (The app's own login would work in the
132
+ // frame — `loginWithProvider` opens a popup — but it would mint a
133
+ // different, app-level identity.)
134
+ if (options.embedded) {
135
+ endEmbeddedSession();
136
+ return;
137
+ }
112
138
  // If nextUrl is not provided, use the current URL
113
139
  const redirectUrl = nextUrl
114
140
  ? new URL(nextUrl, window.location.origin).toString()
@@ -151,7 +177,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
151
177
  // flight would otherwise resolve into callers that run after the logout.
152
178
  clearPendingMe();
153
179
  resetAnalyticsSessionContext();
154
- hasAccessToken = false;
180
+ accessToken = null;
155
181
  // Only do the rest if in a browser environment
156
182
  if (typeof window !== "undefined") {
157
183
  // Remove token from localStorage
@@ -165,6 +191,13 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
165
191
  console.error("Failed to remove token from localStorage:", e);
166
192
  }
167
193
  }
194
+ // An embedded session holds no app cookie to clear — it lived in
195
+ // memory — and navigating a third-party frame to the logout endpoint
196
+ // would only break the frame. The state above is already cleared.
197
+ if (options.embedded) {
198
+ endEmbeddedSession();
199
+ return;
200
+ }
168
201
  // Determine the from_url parameter
169
202
  const fromUrl = redirectUrl || window.location.href;
170
203
  // Redirect to server-side logout endpoint to clear HTTP-only cookies
@@ -176,18 +209,20 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
176
209
  setToken(token, saveToStorage = true) {
177
210
  if (!token)
178
211
  return;
212
+ // An embedded session belongs to the frame the platform minted it for.
213
+ // Persisting it would let it outlive that frame and be picked up as the
214
+ // identity on a later top-level visit, so storage is refused outright.
215
+ const persist = saveToStorage && !options.embedded;
179
216
  // Same reasoning as in `logout`: the identity changes here, so anything
180
217
  // resolved for the previous one must not be handed to later callers.
181
218
  clearPendingMe();
182
219
  resetAnalyticsSessionContext();
183
- hasAccessToken = true;
220
+ accessToken = token;
184
221
  // handle token change for axios clients
185
222
  axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
186
223
  functionsAxiosClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
187
224
  // Save token to localStorage if requested
188
- if (saveToStorage &&
189
- typeof window !== "undefined" &&
190
- window.localStorage) {
225
+ if (persist && typeof window !== "undefined" && window.localStorage) {
191
226
  try {
192
227
  window.localStorage.setItem("base44_access_token", token);
193
228
  // Set "token" that is set by the built-in SDK of platform version 2
@@ -98,6 +98,16 @@ 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
+ /**
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
+ */
105
+ embedded?: boolean;
106
+ /**
107
+ * Called instead of the SDK's built-in notice when an embedded session ends.
108
+ * See {@link CreateClientOptions.onEmbedSessionEnded}.
109
+ */
110
+ onEmbedSessionEnded?: () => void;
101
111
  }
102
112
  /**
103
113
  * 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.
@@ -179,6 +189,24 @@ export interface AuthModule {
179
189
  * ```
180
190
  */
181
191
  redirectToLogin(nextUrl: string): void;
192
+ /**
193
+ * Whether the app is running embedded in a host platform.
194
+ *
195
+ * 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 (pass `options.onEmbedSessionEnded` to {@linkcode createClient} to show your own). Use this to render your own notice or to hide sign-in and sign-out controls that cannot work inside the frame.
196
+ *
197
+ * 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. Prefer client-side navigation in an embedded app.
198
+ *
199
+ * @returns `true` when the app was embedded by a host platform, `false` otherwise.
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * // Show your own message instead of a login screen
204
+ * if (!user && base44.auth.isEmbedded()) {
205
+ * return <SessionEnded />;
206
+ * }
207
+ * ```
208
+ */
209
+ isEmbedded(): boolean;
182
210
  /**
183
211
  * Redirects the user to a third-party authentication provider's login page.
184
212
  *
@@ -539,4 +567,9 @@ export interface InternalAuthModule extends AuthModule {
539
567
  * could not succeed without a session, not to decide that one is valid.
540
568
  */
541
569
  hasToken(): boolean;
570
+ /**
571
+ * The access token currently set on the client, or `null` when there is none.
572
+ * Follows {@linkcode AuthModule.setToken | setToken} and {@linkcode AuthModule.logout | logout}.
573
+ */
574
+ getToken(): string | null;
542
575
  }
@@ -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,6 +5,8 @@ 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, and it is reported only inside a frame, where such a token is redeemed.
9
+ *
8
10
  * @internal
9
11
  *
10
12
  * @param options - Configuration options for token retrieval.
@@ -1,9 +1,14 @@
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";
1
4
  /**
2
5
  * Retrieves an access token from URL parameters or local storage.
3
6
  *
4
7
  * Low-level utility for manually retrieving tokens. In most cases, the Base44 client handles
5
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.
6
9
  *
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.
11
+ *
7
12
  * @internal
8
13
  *
9
14
  * @param options - Configuration options for token retrieval.
@@ -35,7 +40,7 @@
35
40
  * ```
36
41
  */
37
42
  export function getAccessToken(options = {}) {
38
- 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;
39
44
  let token = null;
40
45
  // Try to get token from URL parameters
41
46
  if (typeof window !== "undefined" && window.location) {
@@ -56,6 +61,21 @@ export function getAccessToken(options = {}) {
56
61
  }
57
62
  return token;
58
63
  }
64
+ // A platform-embedded load carries a one-time token instead. It is not a
65
+ // session yet — createClient takes it off the URL and exchanges it — but
66
+ // it is the identity this load arrives with, and callers that read this
67
+ // once as the page loads must not conclude there is none.
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
+ }
78
+ }
59
79
  }
60
80
  catch (e) {
61
81
  console.error("Error retrieving token from URL:", e);
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Sessions for apps embedded in a host platform.
3
+ *
4
+ * The platform's server mints a one-time token for one of its users and puts
5
+ * it on the iframe URL as `?ott=`. The client takes it off the URL as it is
6
+ * created, trades it for an app-user session through the OAuth token-exchange
7
+ * grant (RFC 8693), and keeps the result in memory only — never in storage — so
8
+ * the session lives and dies with the frame.
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.
17
+ *
18
+ * @internal
19
+ */
20
+ export declare function takeEmbedTokenFromUrl(): string | null;
21
+ /**
22
+ * Whether this document is rendered inside a frame. Read at call time, not at
23
+ * import: a module-level answer would be fixed before a host (or a test) has
24
+ * set the window up.
25
+ *
26
+ * @internal
27
+ */
28
+ export declare function isFramed(): boolean;
29
+ /**
30
+ * Whether this tab previously redeemed an embed token for this app. Counts only
31
+ * inside a frame: a top-level tab that once carried a token must fall back to
32
+ * the regular login, or it could never sign in again.
33
+ *
34
+ * @internal
35
+ */
36
+ export declare function isEmbeddedTab(appId: string): boolean;
37
+ /** @internal */
38
+ export declare function markEmbeddedTab(appId: string): void;
39
+ /**
40
+ * Trades a one-time embed token for an app-user session token. Resolves to
41
+ * `null` when the platform refuses the token; never throws.
42
+ *
43
+ * @internal
44
+ */
45
+ export declare function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl, }: {
46
+ serverUrl: string;
47
+ appId: string;
48
+ ott: string;
49
+ fetchImpl?: typeof fetch;
50
+ }): Promise<string | null>;
51
+ /**
52
+ * Covers the page with a plain "session ended" notice. An embedded session can
53
+ * only be renewed by the host platform, so this stands in for the login
54
+ * redirect: only the platform can mint the next session. The copy points at the
55
+ * host page: reloading the frame itself carries no token and lands here again.
56
+ *
57
+ * This is the default an app that predates embedding gets. An app that wants its
58
+ * own can pass `options.onEmbedSessionEnded` to {@linkcode createClient}, or
59
+ * check {@linkcode AuthModule.isEmbedded} before asking for a login.
60
+ *
61
+ * @internal
62
+ */
63
+ export declare function showEmbedSessionEnded(): void;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Sessions for apps embedded in a host platform.
3
+ *
4
+ * The platform's server mints a one-time token for one of its users and puts
5
+ * it on the iframe URL as `?ott=`. The client takes it off the URL as it is
6
+ * created, trades it for an app-user session through the OAuth token-exchange
7
+ * grant (RFC 8693), and keeps the result in memory only — never in storage — so
8
+ * the session lives and dies with the frame.
9
+ *
10
+ * @internal
11
+ */
12
+ /** The query parameter a host platform puts the one-time token on. @internal */
13
+ export const EMBED_TOKEN_PARAM = "ott";
14
+ const EMBED_TAB_KEY_PREFIX = "base44_embed_session";
15
+ /** Per-app, so a second app framed on the same origin keeps its own answer. */
16
+ const embedTabKey = (appId) => `${EMBED_TAB_KEY_PREFIX}:${appId}`;
17
+ const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
18
+ const SUBJECT_TOKEN_TYPE = "urn:base44:params:oauth:token-type:embed-ott";
19
+ const RATE_LIMITED = 429;
20
+ const RATE_LIMIT_RETRY_MS = 1000;
21
+ const SESSION_ENDED_ELEMENT_ID = "base44-embed-session-ended";
22
+ /**
23
+ * Reads the one-time token off the current URL and strips it, so it is never
24
+ * left in the address bar, in history, or in a shared link.
25
+ *
26
+ * @internal
27
+ */
28
+ export function takeEmbedTokenFromUrl() {
29
+ if (typeof window === "undefined" || !window.location) {
30
+ return null;
31
+ }
32
+ let ott = null;
33
+ try {
34
+ const url = new URL(window.location.href);
35
+ ott = url.searchParams.get(EMBED_TOKEN_PARAM);
36
+ if (!ott) {
37
+ return null;
38
+ }
39
+ url.searchParams.delete(EMBED_TOKEN_PARAM);
40
+ window.history.replaceState(window.history.state, "", url.toString());
41
+ }
42
+ catch (e) {
43
+ // The token was read but the URL could not be rewritten (history blocked,
44
+ // a sandboxed frame, replaceState rate-limited). Return it anyway: it is
45
+ // this load's identity, and dropping it here would leave `?ott=` on the
46
+ // URL for getAccessToken to hand back as if it were a session token.
47
+ console.error("Error retrieving embed token from URL:", e);
48
+ }
49
+ return ott;
50
+ }
51
+ /**
52
+ * Whether this document is rendered inside a frame. Read at call time, not at
53
+ * import: a module-level answer would be fixed before a host (or a test) has
54
+ * set the window up.
55
+ *
56
+ * @internal
57
+ */
58
+ export function isFramed() {
59
+ if (typeof window === "undefined") {
60
+ return false;
61
+ }
62
+ try {
63
+ return window.self !== window.top;
64
+ }
65
+ catch (_a) {
66
+ // Reaching the top window can be refused outright; if we cannot see it,
67
+ // we are certainly not it.
68
+ return true;
69
+ }
70
+ }
71
+ /**
72
+ * Whether this tab previously redeemed an embed token for this app. Counts only
73
+ * inside a frame: a top-level tab that once carried a token must fall back to
74
+ * the regular login, or it could never sign in again.
75
+ *
76
+ * @internal
77
+ */
78
+ export function isEmbeddedTab(appId) {
79
+ if (typeof window === "undefined") {
80
+ return false;
81
+ }
82
+ // A cross-site frame is third-party storage, which some browsers block
83
+ // outright — every access can throw, and the marker is then unavailable.
84
+ try {
85
+ return (isFramed() && window.sessionStorage.getItem(embedTabKey(appId)) === "1");
86
+ }
87
+ catch (_a) {
88
+ return false;
89
+ }
90
+ }
91
+ /** @internal */
92
+ export function markEmbeddedTab(appId) {
93
+ try {
94
+ window.sessionStorage.setItem(embedTabKey(appId), "1");
95
+ }
96
+ catch (_a) {
97
+ /* storage blocked */
98
+ }
99
+ }
100
+ /**
101
+ * Trades a one-time embed token for an app-user session token. Resolves to
102
+ * `null` when the platform refuses the token; never throws.
103
+ *
104
+ * @internal
105
+ */
106
+ export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fetch, }) {
107
+ const post = () => fetchImpl(`${serverUrl}/api/apps/${appId}/auth/embed/token`, {
108
+ method: "POST",
109
+ headers: { "X-App-Id": String(appId) },
110
+ body: new URLSearchParams({
111
+ grant_type: GRANT_TYPE,
112
+ subject_token: ott,
113
+ subject_token_type: SUBJECT_TOKEN_TYPE,
114
+ }),
115
+ });
116
+ try {
117
+ let response = await post();
118
+ // The limiter refuses before the token is redeemed, so it is still valid.
119
+ if (response.status === RATE_LIMITED) {
120
+ await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_RETRY_MS));
121
+ response = await post();
122
+ }
123
+ if (!response.ok) {
124
+ return null;
125
+ }
126
+ const { access_token: token } = (await response.json());
127
+ return token || null;
128
+ }
129
+ catch (e) {
130
+ console.error("Embed token exchange failed:", e);
131
+ return null;
132
+ }
133
+ }
134
+ /**
135
+ * Covers the page with a plain "session ended" notice. An embedded session can
136
+ * only be renewed by the host platform, so this stands in for the login
137
+ * redirect: only the platform can mint the next session. The copy points at the
138
+ * host page: reloading the frame itself carries no token and lands here again.
139
+ *
140
+ * This is the default an app that predates embedding gets. An app that wants its
141
+ * own can pass `options.onEmbedSessionEnded` to {@linkcode createClient}, or
142
+ * check {@linkcode AuthModule.isEmbedded} before asking for a login.
143
+ *
144
+ * @internal
145
+ */
146
+ export function showEmbedSessionEnded() {
147
+ if (typeof document === "undefined" || !document.body) {
148
+ return;
149
+ }
150
+ if (document.getElementById(SESSION_ENDED_ELEMENT_ID)) {
151
+ return;
152
+ }
153
+ // Colors as custom properties in a stylesheet rather than inline, so the
154
+ // notice follows the reader's theme — including a change made while it is up.
155
+ const style = document.createElement("style");
156
+ style.textContent =
157
+ `#${SESSION_ENDED_ELEMENT_ID}{--b44-bg:#fff;--b44-fg:#0f172a;--b44-muted:#475569;}` +
158
+ `@media (prefers-color-scheme:dark){#${SESSION_ENDED_ELEMENT_ID}` +
159
+ `{--b44-bg:#0f172a;--b44-fg:#f8fafc;--b44-muted:#94a3b8;}}`;
160
+ const overlay = document.createElement("div");
161
+ overlay.id = SESSION_ENDED_ELEMENT_ID;
162
+ overlay.setAttribute("role", "alert");
163
+ overlay.style.cssText =
164
+ "position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;" +
165
+ "justify-content:center;background:var(--b44-bg);color:var(--b44-fg);" +
166
+ "font-family:ui-sans-serif,system-ui,sans-serif;text-align:center;padding:2rem;";
167
+ const title = document.createElement("h1");
168
+ title.textContent = "Session ended";
169
+ title.style.cssText = "font-size:1.5rem;font-weight:700;margin:0 0 .75rem;";
170
+ const body = document.createElement("p");
171
+ body.textContent = "Reload this page in your browser to start a new session.";
172
+ body.style.cssText = "margin:0;color:var(--b44-muted);";
173
+ const card = document.createElement("div");
174
+ card.append(title, body);
175
+ overlay.append(style, card);
176
+ document.body.append(overlay);
177
+ }
@@ -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,7 +4,8 @@ export interface RoomsSocketConfig {
4
4
  mountPath: string;
5
5
  transports: string[];
6
6
  appId: string;
7
- token?: string;
7
+ /** Asked on every connect, so the socket always carries the current session. */
8
+ getToken: () => string | null;
8
9
  }
9
10
  export type TSocketRoom = string;
10
11
  export type TJsonStr = string;
@@ -40,7 +41,7 @@ export declare function RoomsSocket({ config }: {
40
41
  leave: (room: string) => void;
41
42
  }>;
42
43
  subscribeToRoom: (room: TSocketRoom, handlers: Partial<{ [k in TEvent]: THandler<k>; }>) => () => void;
43
- updateConfig: (config: Partial<RoomsSocketConfig>) => void;
44
+ reconnect: () => void;
44
45
  updateModel: (room: string, data: any) => Promise<void>;
45
46
  disconnect: () => void;
46
47
  };
@@ -1,14 +1,12 @@
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) {
6
- var _a;
7
5
  // On unauthenticated clients, send a stable anonymous visitor id on the
8
6
  // handshake so the backend can verify room access for anonymous agent
9
7
  // conversations (mirrors the X-Base44-Anonymous-Id HTTP header). Authenticated
10
8
  // clients are identified by their token instead.
11
- const resolvedToken = (_a = config.token) !== null && _a !== void 0 ? _a : getAccessToken();
9
+ const resolvedToken = config.getToken();
12
10
  const query = {
13
11
  app_id: config.appId,
14
12
  token: resolvedToken,
@@ -42,7 +40,6 @@ function initializeSocket(config, handlers) {
42
40
  return socket;
43
41
  }
44
42
  export function RoomsSocket({ config }) {
45
- let currentConfig = { ...config };
46
43
  const roomsToListeners = {};
47
44
  const pendingRoomLeaves = {};
48
45
  const handlers = {
@@ -84,13 +81,10 @@ export function RoomsSocket({ config }) {
84
81
  socket.disconnect();
85
82
  }
86
83
  }
87
- function updateConfig(config) {
84
+ /** Drops the connection and opens a new one with the current token. */
85
+ function reconnect() {
88
86
  cleanup();
89
- currentConfig = {
90
- ...currentConfig,
91
- ...config,
92
- };
93
- socket = initializeSocket(currentConfig, handlers);
87
+ socket = initializeSocket(config, handlers);
94
88
  }
95
89
  function joinRoom(room) {
96
90
  socket.emit("join", room);
@@ -163,7 +157,7 @@ export function RoomsSocket({ config }) {
163
157
  return {
164
158
  socket,
165
159
  subscribeToRoom,
166
- updateConfig,
160
+ reconnect,
167
161
  updateModel,
168
162
  disconnect,
169
163
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.48-pr.281.97e1467",
3
+ "version": "0.8.48-pr.282.081f33c",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",