@base44-preview/sdk 0.8.48-pr.282.8a4c1c0 → 0.8.48-pr.282.8f60b80

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, takeEmbedTokenFromUrl, } from "./utils/embed-session.js";
8
+ import { exchangeEmbedToken, frameSession, isFramed, 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,34 +58,16 @@ 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 embedOtt = takeEmbedTokenFromUrl();
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
- }
69
- const socketConfig = {
70
- serverUrl,
71
- mountPath: "/ws-user-apps/socket.io/",
72
- transports: ["websocket"],
73
- appId,
74
- getToken,
75
- };
76
- let socket = null;
77
- const getSocket = () => {
78
- if (!socket) {
79
- socket = RoomsSocket({
80
- config: socketConfig,
81
- });
82
- }
83
- return socket;
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;
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
+ const embedded = Boolean(embedOtt);
68
+ // The passed token is the OTT itself whenever one came in on the URL. That is
69
+ // not this session: it is what the exchange below trades for one.
70
+ const token = urlOtt !== null ? undefined : config.token;
89
71
  const headers = {
90
72
  ...optionalHeaders,
91
73
  "X-App-Id": String(appId),
@@ -134,11 +116,14 @@ export function createClient(config) {
134
116
  baseURL: `${serverUrl}/api`,
135
117
  headers,
136
118
  });
119
+ // Declared before the auth module so `onSessionChange` can reach it; the
120
+ // socket itself is still only built on first use, below.
121
+ let socket = null;
137
122
  const userAuthModule = createAuthModule(axiosClient, functionsAxiosClient, appId, {
138
123
  appBaseUrl: normalizedAppBaseUrl,
139
124
  serverUrl,
140
125
  token,
141
- embedded: Boolean(embedOtt),
126
+ embedded,
142
127
  // The socket carries its token on the handshake, so an identity change
143
128
  // only reaches it by opening a new connection — or, on logout, by
144
129
  // dropping the one still running as the user who just left.
@@ -148,17 +133,34 @@ export function createClient(config) {
148
133
  // requests during construction (notably analytics, which fires an init
149
134
  // event whose flush calls auth.me()). Without this, the first User/me
150
135
  // request is built before setToken runs and goes out unauthenticated.
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) {
136
+ // Skipped in a frame: the session comes from the exchange below.
137
+ if (typeof window !== "undefined" && !embedded) {
154
138
  const accessToken = token || getAccessToken();
155
139
  if (accessToken) {
156
140
  userAuthModule.setToken(accessToken);
157
141
  }
158
142
  }
143
+ // Live token for every module; outside a frame it still falls back to storage.
144
+ const getToken = () => { var _a; return (_a = userAuthModule.getToken()) !== null && _a !== void 0 ? _a : (embedded ? null : getAccessToken()); };
145
+ const socketConfig = {
146
+ serverUrl,
147
+ mountPath: "/ws-user-apps/socket.io/",
148
+ transports: ["websocket"],
149
+ appId,
150
+ getToken,
151
+ };
152
+ const getSocket = () => {
153
+ if (!socket) {
154
+ socket = RoomsSocket({ config: socketConfig });
155
+ }
156
+ return socket;
157
+ };
158
+ // The exchange belongs to the frame, not to one client: the token is off the
159
+ // URL after the first `createClient`, so a second one in the same document
160
+ // joins the session already being negotiated rather than going anonymous.
159
161
  const session = embedOtt
160
- ? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt })
161
- : null;
162
+ ? frameSession(appId, () => exchangeEmbedToken({ serverUrl, appId, ott: embedOtt }))
163
+ : frameSession(appId);
162
164
  // Settles once the exchanged session is applied (memory only); at once
163
165
  // otherwise. Never rejects: every request waits on it, so a failure here
164
166
  // must not turn into a rejection on each of them.
@@ -180,6 +182,21 @@ export function createClient(config) {
180
182
  console.error("Base44: applying the embedded session failed:", e);
181
183
  })
182
184
  : Promise.resolve();
185
+ // Everything that can wait for the session does. `aiGateway.connection()`
186
+ // and the agents connect URLs cannot: they hand back a value, not a promise,
187
+ // and a value read now would stay wrong after the session arrives. They warn
188
+ // instead of failing quietly.
189
+ let warnedEarlyRead = false;
190
+ const getTokenNow = () => {
191
+ const sessionToken = getToken();
192
+ if (sessionToken === null && session && !warnedEarlyRead) {
193
+ warnedEarlyRead = true;
194
+ console.warn("Base44: read a token before the embedded session was ready, so it is " +
195
+ "empty. Await a call such as base44.auth.me() before building a " +
196
+ "client or a URL that keeps the token.");
197
+ }
198
+ return sessionToken;
199
+ };
183
200
  if (session) {
184
201
  // Requests issued during the exchange wait for it. Registered after createAxiosClient's
185
202
  // interceptors so it runs first and the anonymous-visitor header sees the Authorization.
@@ -247,11 +264,13 @@ export function createClient(config) {
247
264
  getSocket,
248
265
  appId,
249
266
  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.
252
- getToken,
267
+ getToken: getTokenNow,
268
+ }),
269
+ aiGateway: createAiGatewayModule({
270
+ serverUrl,
271
+ getToken: getTokenNow,
272
+ appId,
253
273
  }),
254
- aiGateway: createAiGatewayModule({ serverUrl, getToken, appId }),
255
274
  appLogs: createAppLogsModule(axiosClient, appId),
256
275
  app: createAppModule(axiosClient, appId),
257
276
  users: createUsersModule(axiosClient, appId),
@@ -299,7 +318,7 @@ export function createClient(config) {
299
318
  // The user's token, as on the user-scoped module: the only thing this is
300
319
  // read for is the `?token=` on a channel URL handed to that user, which
301
320
  // the app's service credential must never end up in.
302
- getToken,
321
+ getToken: getTokenNow,
303
322
  }),
304
323
  aiGateway: createAiGatewayModule({
305
324
  serverUrl,
@@ -11,9 +11,8 @@ 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. Awaited per dial, so a session still being
15
- * exchanged is in hand before the URL is built. */
16
- getAuthToken(): Promise<string | null | undefined>;
14
+ * anonymous connects omit it. */
15
+ getAuthToken(): string | null | undefined | Promise<string | null | undefined>;
17
16
  /** Same semantics as function calls: editors with a non-prod version get the
18
17
  * draft actor script; everyone else gets the published one. */
19
18
  functionsVersion?: string;
@@ -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;
164
+ getToken: () => string | null | undefined;
165
165
  }
166
166
  /**
167
167
  * Agents module for managing AI agent conversations.
@@ -360,9 +360,7 @@ 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. 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.
363
+ * The URL includes authentication if a token is available.
366
364
  *
367
365
  * @param agentName - The name of the agent.
368
366
  * @returns WhatsApp connection URL.
@@ -380,9 +378,7 @@ export interface AgentsModule {
380
378
  * Gets Telegram connection URL for an agent.
381
379
  *
382
380
  * Generates a URL that users can use to connect with the agent through Telegram.
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
381
+ * The URL includes authentication if a token is available. When the user opens
386
382
  * this URL, they are redirected to the agent's Telegram bot with an activation
387
383
  * code that securely links their account.
388
384
  *
@@ -98,11 +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
- /** Whether a host platform embedded this client in a frame. */
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
+ */
102
105
  embedded?: boolean;
103
106
  /**
104
- * Called when the identity changes: `true` on `setToken`, `false` on
105
- * `logout`. Lets the client redial the socket, which holds its own copy.
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.
106
111
  */
107
112
  onSessionChange?: (hasSession: boolean) => void;
108
113
  }
@@ -187,13 +192,13 @@ export interface AuthModule {
187
192
  */
188
193
  redirectToLogin(nextUrl: string): void;
189
194
  /**
190
- * Whether a host platform embedded this app and signed its user in.
195
+ * Whether the app is running embedded in a host platform.
191
196
  *
192
- * Only the platform can renew that session, so {@linkcode AuthModule.redirectToLogin | redirectToLogin()} and {@linkcode AuthModule.logout | logout()} show a "session ended" notice rather than a login page. Use this to render your own notice, or to hide sign-in controls that cannot work in the frame.
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.
193
198
  *
194
- * The session lives in memory, so a reload inside the frame ends it and returns `false` here. Prefer client-side navigation.
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.
195
200
  *
196
- * @returns `true` when a host platform embedded this app.
201
+ * @returns `true` when the app was embedded by a host platform, `false` otherwise.
197
202
  *
198
203
  * @example
199
204
  * ```typescript
@@ -564,6 +569,9 @@ export interface InternalAuthModule extends AuthModule {
564
569
  * could not succeed without a session, not to decide that one is valid.
565
570
  */
566
571
  hasToken(): boolean;
567
- /** The token currently set on the client, or `null`. */
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
+ */
568
576
  getToken(): string | null;
569
577
  }
@@ -7,12 +7,6 @@
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
10
  * The exchange endpoint is rate limited per app, and its limiter refuses before
17
11
  * the one-time token is redeemed — so a 429 leaves the token still valid and is
18
12
  * worth retrying once.
@@ -21,6 +15,8 @@
21
15
  */
22
16
  export declare const EMBED_TOKEN_PARAM = "ott";
23
17
  /** @internal */
18
+ export declare function frameSession(appId: string, start?: () => Promise<string | null>): Promise<string | null> | null;
19
+ /** @internal */
24
20
  export declare function takeEmbedTokenFromUrl(): string | null;
25
21
  /** @internal */
26
22
  export declare function isFramed(): boolean;
@@ -7,12 +7,6 @@
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
10
  * The exchange endpoint is rate limited per app, and its limiter refuses before
17
11
  * the one-time token is redeemed — so a 429 leaves the token still valid and is
18
12
  * worth retrying once.
@@ -26,6 +20,20 @@ const RATE_LIMITED = 429;
26
20
  const RATE_LIMIT_RETRY_MS = 1000;
27
21
  const EXCHANGE_TIMEOUT_MS = 15000;
28
22
  const SESSION_ENDED_ELEMENT_ID = "base44-embed-session-ended";
23
+ const FRAME_SESSIONS_KEY = "__base44EmbedSessions";
24
+ /** @internal */
25
+ export function frameSession(appId, start) {
26
+ var _a, _b;
27
+ var _c;
28
+ if (typeof window === "undefined") {
29
+ return start ? start() : null;
30
+ }
31
+ const sessions = ((_a = (_c = window)[FRAME_SESSIONS_KEY]) !== null && _a !== void 0 ? _a : (_c[FRAME_SESSIONS_KEY] = {}));
32
+ if (!sessions[appId] && start) {
33
+ sessions[appId] = start();
34
+ }
35
+ return (_b = sessions[appId]) !== null && _b !== void 0 ? _b : null;
36
+ }
29
37
  /** @internal */
30
38
  export function takeEmbedTokenFromUrl() {
31
39
  if (typeof window === "undefined" || !window.location) {
@@ -44,11 +52,16 @@ export function takeEmbedTokenFromUrl() {
44
52
  catch (e) {
45
53
  console.error("Error retrieving embed token from URL:", e);
46
54
  }
47
- return isFramed() ? ott : null;
55
+ return ott;
48
56
  }
49
57
  /** @internal */
50
58
  export function isFramed() {
51
- return typeof window !== "undefined" && window.self !== window.top;
59
+ try {
60
+ return typeof window !== "undefined" && window.self !== window.top;
61
+ }
62
+ catch (_a) {
63
+ return true;
64
+ }
52
65
  }
53
66
  /** @internal */
54
67
  export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fetch, }) {
@@ -40,5 +40,6 @@ export declare function createFetchWithAuth({ axios, serviceRoleAxios, appId, se
40
40
  serverUrl: string;
41
41
  functionsVersion?: string;
42
42
  platformHeaders?: Record<string, string>;
43
+ /** Resolves once the client's session is settled. */
43
44
  waitForAuth?: () => Promise<void>;
44
45
  }): (path: string, init?: FetchWithAuthInit) => Promise<Response>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.48-pr.282.8a4c1c0",
3
+ "version": "0.8.48-pr.282.8f60b80",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",