@base44-preview/sdk 0.8.48-pr.282.2b05017 → 0.8.48-pr.282.520fd69

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, isEmbeddedTab, isFramed, markEmbeddedTab, 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";
@@ -55,37 +55,22 @@ import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
55
55
  */
56
56
  export function createClient(config) {
57
57
  var _a, _b, _c, _d;
58
- 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;
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
- const embedOtt = inBrowser ? takeEmbedTokenFromUrl() : null;
66
- const embedded = embedOtt !== null || (inBrowser && isEmbeddedTab());
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
67
  if (embedOtt) {
68
68
  markEmbeddedTab();
69
69
  }
70
- let currentToken = token !== null && token !== void 0 ? token : null;
71
- const socketConfig = {
72
- serverUrl,
73
- mountPath: "/ws-user-apps/socket.io/",
74
- transports: ["websocket"],
75
- appId,
76
- // null (not undefined): an embedded frame's only identity is the one the
77
- // platform minted, so the socket must not fall back to a stored token.
78
- token: embedded ? null : token,
79
- };
80
- let socket = null;
81
- const getSocket = () => {
82
- if (!socket) {
83
- socket = RoomsSocket({
84
- config: socketConfig,
85
- });
86
- }
87
- return socket;
88
- };
70
+ const embedded = Boolean(embedOtt) || isEmbeddedTab();
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;
89
74
  const headers = {
90
75
  ...optionalHeaders,
91
76
  "X-App-Id": String(appId),
@@ -144,46 +129,57 @@ export function createClient(config) {
144
129
  // requests during construction (notably analytics, which fires an init
145
130
  // event whose flush calls auth.me()). Without this, the first User/me
146
131
  // request is built before setToken runs and goes out unauthenticated.
147
- // An embedded load skips this: its session comes from the exchange below,
148
- // and a token an earlier visitor left in storage must not become it.
149
- if (inBrowser && !embedded) {
132
+ // Skipped in a frame: the session comes from the exchange below.
133
+ if (typeof window !== "undefined" && !embedded) {
150
134
  const accessToken = token || getAccessToken();
151
135
  if (accessToken) {
152
- currentToken = accessToken;
153
136
  userAuthModule.setToken(accessToken);
154
137
  }
155
138
  }
156
- // The one answer every module reads. Follows setToken and logout; outside an
157
- // embedded frame it still falls back to storage, so a login in another tab is
158
- // picked up on the next connection, as before.
159
- const getToken = () => {
160
- if (userAuthModule.hasToken()) {
161
- return currentToken;
139
+ // Live token for every module; outside a frame it still falls back to storage.
140
+ const getToken = () => { var _a; return (_a = userAuthModule.getToken()) !== null && _a !== void 0 ? _a : (embedded ? null : getAccessToken()); };
141
+ const socketConfig = {
142
+ serverUrl,
143
+ mountPath: "/ws-user-apps/socket.io/",
144
+ transports: ["websocket"],
145
+ appId,
146
+ getToken,
147
+ };
148
+ let socket = null;
149
+ const getSocket = () => {
150
+ if (!socket) {
151
+ socket = RoomsSocket({ config: socketConfig });
162
152
  }
163
- return embedded || !inBrowser ? null : getAccessToken();
153
+ return socket;
164
154
  };
165
155
  const applyToken = (newToken, saveToStorage) => {
166
156
  userAuthModule.setToken(newToken, saveToStorage);
167
- currentToken = newToken;
168
- socketConfig.token = newToken;
169
- if (socket) {
170
- socket.updateConfig({ token: newToken });
171
- }
157
+ socket === null || socket === void 0 ? void 0 : socket.reconnect();
172
158
  };
173
- // Resolves once the embedded session is in hand (or known not to be coming);
174
- // immediately for every other client. The exchanged token is applied without
175
- // persisting it: the session must not outlive the frame it was minted for.
159
+ // Settles once the exchanged session is applied (memory only); at once
160
+ // otherwise. Never rejects: every request waits on it, so a failure here
161
+ // must not turn into a rejection on each of them.
176
162
  const authReady = embedOtt
177
- ? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt }).then((sessionToken) => {
163
+ ? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt })
164
+ .then((sessionToken) => {
165
+ var _a;
178
166
  if (sessionToken) {
179
167
  applyToken(sessionToken, false);
168
+ return;
180
169
  }
170
+ // The app is about to run anonymous. Say why, once, instead of
171
+ // leaving only the 401s that follow.
172
+ const error = new Error("Base44: the embed token was refused, so this app is not signed in.");
173
+ console.error(error.message);
174
+ (_a = options === null || options === void 0 ? void 0 : options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
175
+ })
176
+ .catch((e) => {
177
+ console.error("Base44: applying the embedded session failed:", e);
181
178
  })
182
179
  : Promise.resolve();
183
180
  if (embedOtt) {
184
- // Requests issued while the exchange is in flight wait for it and carry the
185
- // session it produces. Registered after createAxiosClient's interceptors,
186
- // so it runs first and the anonymous-visitor header sees the Authorization.
181
+ // Requests issued during the exchange wait for it. Registered after createAxiosClient's
182
+ // interceptors so it runs first and the anonymous-visitor header sees the Authorization.
187
183
  for (const client of [axiosClient, functionsAxiosClient]) {
188
184
  client.interceptors.request.use(async (requestConfig) => {
189
185
  await authReady;
@@ -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);
@@ -161,7 +164,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
161
164
  // flight would otherwise resolve into callers that run after the logout.
162
165
  clearPendingMe();
163
166
  resetAnalyticsSessionContext();
164
- hasAccessToken = false;
167
+ accessToken = null;
165
168
  // Only do the rest if in a browser environment
166
169
  if (typeof window !== "undefined") {
167
170
  // Remove token from localStorage
@@ -186,18 +189,20 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
186
189
  setToken(token, saveToStorage = true) {
187
190
  if (!token)
188
191
  return;
192
+ // An embedded session belongs to the frame the platform minted it for.
193
+ // Persisting it would let it outlive that frame and be picked up as the
194
+ // identity on a later top-level visit, so storage is refused outright.
195
+ const persist = saveToStorage && !options.embedded;
189
196
  // Same reasoning as in `logout`: the identity changes here, so anything
190
197
  // resolved for the previous one must not be handed to later callers.
191
198
  clearPendingMe();
192
199
  resetAnalyticsSessionContext();
193
- hasAccessToken = true;
200
+ accessToken = token;
194
201
  // handle token change for axios clients
195
202
  axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
196
203
  functionsAxiosClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
197
204
  // Save token to localStorage if requested
198
- if (saveToStorage &&
199
- typeof window !== "undefined" &&
200
- window.localStorage) {
205
+ if (persist && typeof window !== "undefined" && window.localStorage) {
201
206
  try {
202
207
  window.localStorage.setItem("base44_access_token", token);
203
208
  // Set "token" that is set by the built-in SDK of platform version 2
@@ -560,4 +560,9 @@ export interface InternalAuthModule extends AuthModule {
560
560
  * could not succeed without a session, not to decide that one is valid.
561
561
  */
562
562
  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
+ getToken(): string | null;
563
568
  }
@@ -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.
9
+ *
8
10
  * @internal
9
11
  *
10
12
  * @param options - Configuration options for token retrieval.
@@ -1,9 +1,12 @@
1
+ import { EMBED_TOKEN_PARAM } from "./embed-session.js";
1
2
  /**
2
3
  * Retrieves an access token from URL parameters or local storage.
3
4
  *
4
5
  * Low-level utility for manually retrieving tokens. In most cases, the Base44 client handles
5
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.
6
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.
9
+ *
7
10
  * @internal
8
11
  *
9
12
  * @param options - Configuration options for token retrieval.
@@ -56,6 +59,14 @@ export function getAccessToken(options = {}) {
56
59
  }
57
60
  return token;
58
61
  }
62
+ // A platform-embedded load carries a one-time token instead. It is not a
63
+ // session yet — createClient takes it off the URL and exchanges it — but
64
+ // it is the identity this load arrives with, and callers that read this
65
+ // 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;
69
+ }
59
70
  }
60
71
  catch (e) {
61
72
  console.error("Error retrieving token from URL:", e);
@@ -9,6 +9,8 @@
9
9
  *
10
10
  * @internal
11
11
  */
12
+ /** The query parameter a host platform puts the one-time token on. @internal */
13
+ export declare const EMBED_TOKEN_PARAM = "ott";
12
14
  /**
13
15
  * Reads the one-time token off the current URL and strips it, so it is never
14
16
  * left in the address bar, in history, or in a shared link.
@@ -16,6 +18,14 @@
16
18
  * @internal
17
19
  */
18
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;
19
29
  /**
20
30
  * Whether this tab previously redeemed an embed token. Counts only inside a
21
31
  * frame: a top-level tab that once carried a token must fall back to the
@@ -40,8 +50,9 @@ export declare function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl, }
40
50
  }): Promise<string | null>;
41
51
  /**
42
52
  * Covers the page with a plain "session ended" notice. An embedded session can
43
- * only be renewed by the host platform (by reloading its own page), so this
44
- * stands in for the login redirect, which cannot complete inside a frame.
53
+ * only be renewed by the host platform, so this stands in for the login
54
+ * redirect, which cannot complete inside a frame. The copy points at the host
55
+ * page: reloading the frame itself carries no token and lands here again.
45
56
  *
46
57
  * @internal
47
58
  */
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * @internal
11
11
  */
12
- const OTT_PARAM = "ott";
12
+ /** The query parameter a host platform puts the one-time token on. @internal */
13
+ export const EMBED_TOKEN_PARAM = "ott";
13
14
  const EMBED_TAB_KEY = "base44_embed_session";
14
15
  const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
15
16
  const SUBJECT_TOKEN_TYPE = "urn:base44:params:oauth:token-type:embed-ott";
@@ -26,19 +27,43 @@ export function takeEmbedTokenFromUrl() {
26
27
  if (typeof window === "undefined" || !window.location) {
27
28
  return null;
28
29
  }
30
+ let ott = null;
29
31
  try {
30
32
  const url = new URL(window.location.href);
31
- const ott = url.searchParams.get(OTT_PARAM);
33
+ ott = url.searchParams.get(EMBED_TOKEN_PARAM);
32
34
  if (!ott) {
33
35
  return null;
34
36
  }
35
- url.searchParams.delete(OTT_PARAM);
37
+ url.searchParams.delete(EMBED_TOKEN_PARAM);
36
38
  window.history.replaceState(window.history.state, "", url.toString());
37
- return ott;
38
39
  }
39
40
  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.
40
45
  console.error("Error retrieving embed token from URL:", e);
41
- return null;
46
+ }
47
+ return ott;
48
+ }
49
+ /**
50
+ * Whether this document is rendered inside a frame. Read at call time, not at
51
+ * import: a module-level answer would be fixed before a host (or a test) has
52
+ * set the window up.
53
+ *
54
+ * @internal
55
+ */
56
+ export function isFramed() {
57
+ if (typeof window === "undefined") {
58
+ return false;
59
+ }
60
+ try {
61
+ return window.self !== window.top;
62
+ }
63
+ catch (_a) {
64
+ // Reaching the top window can be refused outright; if we cannot see it,
65
+ // we are certainly not it.
66
+ return true;
42
67
  }
43
68
  }
44
69
  /**
@@ -55,8 +80,7 @@ export function isEmbeddedTab() {
55
80
  // A cross-site frame is third-party storage, which some browsers block
56
81
  // outright — every access can throw, and the marker is then unavailable.
57
82
  try {
58
- return (window.self !== window.top &&
59
- window.sessionStorage.getItem(EMBED_TAB_KEY) === "1");
83
+ return isFramed() && window.sessionStorage.getItem(EMBED_TAB_KEY) === "1";
60
84
  }
61
85
  catch (_a) {
62
86
  return false;
@@ -107,8 +131,9 @@ export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fe
107
131
  }
108
132
  /**
109
133
  * Covers the page with a plain "session ended" notice. An embedded session can
110
- * only be renewed by the host platform (by reloading its own page), so this
111
- * stands in for the login redirect, which cannot complete inside a frame.
134
+ * only be renewed by the host platform, so this stands in for the login
135
+ * redirect, which cannot complete inside a frame. The copy points at the host
136
+ * page: reloading the frame itself carries no token and lands here again.
112
137
  *
113
138
  * @internal
114
139
  */
@@ -130,7 +155,7 @@ export function showEmbedSessionEnded() {
130
155
  title.textContent = "Session ended";
131
156
  title.style.cssText = "font-size:1.5rem;font-weight:700;margin:0 0 .75rem;";
132
157
  const body = document.createElement("p");
133
- body.textContent = "Refresh the page to continue.";
158
+ body.textContent = "Reload this page in your browser to start a new session.";
134
159
  body.style.cssText = "margin:0;color:#475569;";
135
160
  const card = document.createElement("div");
136
161
  card.append(title, body);
@@ -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.2b05017",
3
+ "version": "0.8.48-pr.282.520fd69",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",