@base44-preview/sdk 0.8.48-pr.282.520fd69 → 0.8.48-pr.282.5d5566b
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 +57 -45
- package/dist/client.types.d.ts +1 -2
- package/dist/modules/actors.d.ts +3 -2
- package/dist/modules/actors.js +1 -1
- package/dist/modules/agents.types.d.ts +7 -3
- package/dist/modules/auth.js +20 -3
- package/dist/modules/auth.types.d.ts +11 -10
- package/dist/modules/functions.js +4 -0
- package/dist/modules/functions.types.d.ts +6 -0
- package/dist/utils/auth-utils.d.ts +1 -1
- package/dist/utils/auth-utils.js +15 -6
- package/dist/utils/embed-session.d.ts +13 -37
- package/dist/utils/embed-session.js +25 -77
- package/dist/utils/fetch-with-auth.d.ts +3 -1
- package/dist/utils/fetch-with-auth.js +4 -1
- package/package.json +1 -1
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,
|
|
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,19 +58,34 @@ 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
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
markEmbeddedTab();
|
|
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());
|
|
69
68
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
74
89
|
const headers = {
|
|
75
90
|
...optionalHeaders,
|
|
76
91
|
"X-App-Id": String(appId),
|
|
@@ -123,48 +138,36 @@ export function createClient(config) {
|
|
|
123
138
|
appBaseUrl: normalizedAppBaseUrl,
|
|
124
139
|
serverUrl,
|
|
125
140
|
token,
|
|
126
|
-
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(),
|
|
127
146
|
});
|
|
128
147
|
// Apply the access token before any module that may issue authenticated
|
|
129
148
|
// requests during construction (notably analytics, which fires an init
|
|
130
149
|
// event whose flush calls auth.me()). Without this, the first User/me
|
|
131
150
|
// request is built before setToken runs and goes out unauthenticated.
|
|
132
|
-
//
|
|
133
|
-
|
|
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) {
|
|
134
154
|
const accessToken = token || getAccessToken();
|
|
135
155
|
if (accessToken) {
|
|
136
156
|
userAuthModule.setToken(accessToken);
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 });
|
|
152
|
-
}
|
|
153
|
-
return socket;
|
|
154
|
-
};
|
|
155
|
-
const applyToken = (newToken, saveToStorage) => {
|
|
156
|
-
userAuthModule.setToken(newToken, saveToStorage);
|
|
157
|
-
socket === null || socket === void 0 ? void 0 : socket.reconnect();
|
|
158
|
-
};
|
|
159
|
+
const session = embedOtt
|
|
160
|
+
? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt })
|
|
161
|
+
: null;
|
|
159
162
|
// Settles once the exchanged session is applied (memory only); at once
|
|
160
163
|
// otherwise. Never rejects: every request waits on it, so a failure here
|
|
161
164
|
// must not turn into a rejection on each of them.
|
|
162
|
-
const authReady =
|
|
163
|
-
?
|
|
165
|
+
const authReady = session
|
|
166
|
+
? session
|
|
164
167
|
.then((sessionToken) => {
|
|
165
168
|
var _a;
|
|
166
169
|
if (sessionToken) {
|
|
167
|
-
|
|
170
|
+
userAuthModule.setToken(sessionToken, false);
|
|
168
171
|
return;
|
|
169
172
|
}
|
|
170
173
|
// The app is about to run anonymous. Say why, once, instead of
|
|
@@ -177,7 +180,7 @@ export function createClient(config) {
|
|
|
177
180
|
console.error("Base44: applying the embedded session failed:", e);
|
|
178
181
|
})
|
|
179
182
|
: Promise.resolve();
|
|
180
|
-
if (
|
|
183
|
+
if (session) {
|
|
181
184
|
// Requests issued during the exchange wait for it. Registered after createAxiosClient's
|
|
182
185
|
// interceptors so it runs first and the anonymous-visitor header sees the Authorization.
|
|
183
186
|
for (const client of [axiosClient, functionsAxiosClient]) {
|
|
@@ -197,7 +200,10 @@ export function createClient(config) {
|
|
|
197
200
|
// URL needs an absolute host, so fall back to the page origin.
|
|
198
201
|
host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_a = window.location) === null || _a === void 0 ? void 0 : _a.origin : undefined),
|
|
199
202
|
functionsVersion,
|
|
200
|
-
getAuthToken:
|
|
203
|
+
getAuthToken: async () => {
|
|
204
|
+
await authReady;
|
|
205
|
+
return getToken();
|
|
206
|
+
},
|
|
201
207
|
mintConnectionToken: async (actorName, room, connectionId) => {
|
|
202
208
|
await authReady;
|
|
203
209
|
const authToken = getToken();
|
|
@@ -225,6 +231,7 @@ export function createClient(config) {
|
|
|
225
231
|
connectors: createUserConnectorsModule(axiosClient, appId),
|
|
226
232
|
auth: userAuthModule,
|
|
227
233
|
functions: createFunctionsModule(functionsAxiosClient, appId, {
|
|
234
|
+
waitForAuth: () => authReady,
|
|
228
235
|
getAuthHeaders: () => {
|
|
229
236
|
const headers = {};
|
|
230
237
|
const sessionToken = getToken();
|
|
@@ -240,6 +247,8 @@ export function createClient(config) {
|
|
|
240
247
|
getSocket,
|
|
241
248
|
appId,
|
|
242
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.
|
|
243
252
|
getToken,
|
|
244
253
|
}),
|
|
245
254
|
aiGateway: createAiGatewayModule({ serverUrl, getToken, appId }),
|
|
@@ -287,7 +296,10 @@ export function createClient(config) {
|
|
|
287
296
|
getSocket,
|
|
288
297
|
appId,
|
|
289
298
|
serverUrl,
|
|
290
|
-
|
|
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,
|
|
291
303
|
}),
|
|
292
304
|
aiGateway: createAiGatewayModule({
|
|
293
305
|
serverUrl,
|
|
@@ -328,12 +340,12 @@ export function createClient(config) {
|
|
|
328
340
|
serverUrl,
|
|
329
341
|
functionsVersion,
|
|
330
342
|
platformHeaders: optionalHeaders,
|
|
343
|
+
waitForAuth: () => authReady,
|
|
331
344
|
}),
|
|
332
345
|
/**
|
|
333
346
|
* Sets a new authentication token for all subsequent requests.
|
|
334
347
|
*
|
|
335
348
|
* @param newToken - The new authentication token
|
|
336
|
-
* @param saveToStorage - Whether to also keep the token in local storage. Defaults to `true`.
|
|
337
349
|
*
|
|
338
350
|
* @example
|
|
339
351
|
* ```typescript
|
|
@@ -345,8 +357,8 @@ export function createClient(config) {
|
|
|
345
357
|
* base44.setToken(access_token);
|
|
346
358
|
* ```
|
|
347
359
|
*/
|
|
348
|
-
setToken(newToken
|
|
349
|
-
|
|
360
|
+
setToken(newToken) {
|
|
361
|
+
userAuthModule.setToken(newToken, true);
|
|
350
362
|
},
|
|
351
363
|
/**
|
|
352
364
|
* Gets the current client configuration.
|
package/dist/client.types.d.ts
CHANGED
|
@@ -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
|
|
204
|
+
setToken(newToken: string): void;
|
|
206
205
|
/**
|
|
207
206
|
* Gets the current client configuration.
|
|
208
207
|
* @internal
|
package/dist/modules/actors.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/modules/actors.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
*
|
package/dist/modules/auth.js
CHANGED
|
@@ -116,8 +116,11 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
116
116
|
if (typeof window === "undefined") {
|
|
117
117
|
throw new Error("Login method can only be used in a browser environment");
|
|
118
118
|
}
|
|
119
|
-
//
|
|
120
|
-
// login
|
|
119
|
+
// Only the host platform can sign a platform user in, so there is no
|
|
120
|
+
// login page to send them to. (The app's own login would work in the
|
|
121
|
+
// frame — `loginWithProvider` opens a popup — but it would mint a
|
|
122
|
+
// different, app-level identity.) An app that wants its own notice
|
|
123
|
+
// checks `isEmbedded()` rather than asking for a login it cannot get.
|
|
121
124
|
if (options.embedded) {
|
|
122
125
|
showEmbedSessionEnded();
|
|
123
126
|
return;
|
|
@@ -158,13 +161,18 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
158
161
|
},
|
|
159
162
|
// Logout the current user
|
|
160
163
|
logout(redirectUrl) {
|
|
161
|
-
|
|
164
|
+
var _a;
|
|
165
|
+
// Remove the token from both axios instances (always do this). Missing
|
|
166
|
+
// the functions one used to be hidden by the redirect below tearing the
|
|
167
|
+
// page down; an embedded logout returns instead, so the page lives on.
|
|
162
168
|
delete axios.defaults.headers.common["Authorization"];
|
|
169
|
+
delete functionsAxiosClient.defaults.headers.common["Authorization"];
|
|
163
170
|
// Drop identity resolved under the previous session: a `me()` already in
|
|
164
171
|
// flight would otherwise resolve into callers that run after the logout.
|
|
165
172
|
clearPendingMe();
|
|
166
173
|
resetAnalyticsSessionContext();
|
|
167
174
|
accessToken = null;
|
|
175
|
+
(_a = options.onSessionChange) === null || _a === void 0 ? void 0 : _a.call(options, false);
|
|
168
176
|
// Only do the rest if in a browser environment
|
|
169
177
|
if (typeof window !== "undefined") {
|
|
170
178
|
// Remove token from localStorage
|
|
@@ -178,6 +186,13 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
178
186
|
console.error("Failed to remove token from localStorage:", e);
|
|
179
187
|
}
|
|
180
188
|
}
|
|
189
|
+
// An embedded session holds no app cookie to clear — it lived in
|
|
190
|
+
// memory — and navigating a third-party frame to the logout endpoint
|
|
191
|
+
// would only break the frame. The state above is already cleared.
|
|
192
|
+
if (options.embedded) {
|
|
193
|
+
showEmbedSessionEnded();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
181
196
|
// Determine the from_url parameter
|
|
182
197
|
const fromUrl = redirectUrl || window.location.href;
|
|
183
198
|
// Redirect to server-side logout endpoint to clear HTTP-only cookies
|
|
@@ -187,6 +202,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
187
202
|
},
|
|
188
203
|
// Set authentication token
|
|
189
204
|
setToken(token, saveToStorage = true) {
|
|
205
|
+
var _a;
|
|
190
206
|
if (!token)
|
|
191
207
|
return;
|
|
192
208
|
// An embedded session belongs to the frame the platform minted it for.
|
|
@@ -201,6 +217,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
201
217
|
// handle token change for axios clients
|
|
202
218
|
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
203
219
|
functionsAxiosClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
220
|
+
(_a = options.onSessionChange) === null || _a === void 0 ? void 0 : _a.call(options, true);
|
|
204
221
|
// Save token to localStorage if requested
|
|
205
222
|
if (persist && typeof window !== "undefined" && window.localStorage) {
|
|
206
223
|
try {
|
|
@@ -98,11 +98,13 @@ export interface AuthModuleOptions {
|
|
|
98
98
|
* which is how the server-side SDK reports a token it never sets explicitly.
|
|
99
99
|
*/
|
|
100
100
|
token?: string;
|
|
101
|
+
/** Whether a host platform embedded this client in a frame. */
|
|
102
|
+
embedded?: boolean;
|
|
101
103
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
+
* Called when the identity changes: `true` on `setToken`, `false` on
|
|
105
|
+
* `logout`. Lets the client redial the socket, which holds its own copy.
|
|
104
106
|
*/
|
|
105
|
-
|
|
107
|
+
onSessionChange?: (hasSession: boolean) => void;
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
108
110
|
* Authentication module for managing user authentication and authorization. The module automatically stores tokens in local storage when available and manages authorization headers for API requests.
|
|
@@ -185,11 +187,13 @@ export interface AuthModule {
|
|
|
185
187
|
*/
|
|
186
188
|
redirectToLogin(nextUrl: string): void;
|
|
187
189
|
/**
|
|
188
|
-
* Whether
|
|
190
|
+
* Whether a host platform embedded this app and signed its user in.
|
|
191
|
+
*
|
|
192
|
+
* Only the platform can renew that session, so {@linkcode AuthModule.redirectToLogin | redirectToLogin()} and {@linkcode AuthModule.logout | logout()} show a "session ended" notice rather than a login page. Use this to render your own notice, or to hide sign-in controls that cannot work in the frame.
|
|
189
193
|
*
|
|
190
|
-
*
|
|
194
|
+
* The session lives in memory, so a reload inside the frame ends it and returns `false` here. Prefer client-side navigation.
|
|
191
195
|
*
|
|
192
|
-
* @returns `true` when
|
|
196
|
+
* @returns `true` when a host platform embedded this app.
|
|
193
197
|
*
|
|
194
198
|
* @example
|
|
195
199
|
* ```typescript
|
|
@@ -560,9 +564,6 @@ export interface InternalAuthModule extends AuthModule {
|
|
|
560
564
|
* could not succeed without a session, not to decide that one is valid.
|
|
561
565
|
*/
|
|
562
566
|
hasToken(): boolean;
|
|
563
|
-
/**
|
|
564
|
-
* The access token currently set on the client, or `null` when there is none.
|
|
565
|
-
* Follows {@linkcode AuthModule.setToken | setToken} and {@linkcode AuthModule.logout | logout}.
|
|
566
|
-
*/
|
|
567
|
+
/** The token currently set on the client, or `null`. */
|
|
567
568
|
getToken(): string | null;
|
|
568
569
|
}
|
|
@@ -65,8 +65,12 @@ export function createFunctionsModule(axios, appId, config) {
|
|
|
65
65
|
},
|
|
66
66
|
// Fetch a backend function endpoint directly.
|
|
67
67
|
async fetch(path, init = {}) {
|
|
68
|
+
var _a;
|
|
68
69
|
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
69
70
|
const primaryPath = `/functions${normalizedPath}`;
|
|
71
|
+
// Headers are read after this: a session still being negotiated must be
|
|
72
|
+
// in hand before the Authorization is built, not after.
|
|
73
|
+
await ((_a = config === null || config === void 0 ? void 0 : config.waitForAuth) === null || _a === void 0 ? void 0 : _a.call(config));
|
|
70
74
|
const headers = toHeaders(init.headers);
|
|
71
75
|
const requestInit = {
|
|
72
76
|
...init,
|
|
@@ -29,6 +29,12 @@ export type FunctionsFetchInit = RequestInit;
|
|
|
29
29
|
export interface FunctionsModuleConfig {
|
|
30
30
|
getAuthHeaders?: () => Record<string, string>;
|
|
31
31
|
baseURL?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Resolves once the client's session is settled. `fetch` builds its headers
|
|
34
|
+
* by hand rather than through axios, so without this it would miss the gate
|
|
35
|
+
* every other request goes through.
|
|
36
|
+
*/
|
|
37
|
+
waitForAuth?: () => Promise<void>;
|
|
32
38
|
}
|
|
33
39
|
/**
|
|
34
40
|
* Functions module for invoking custom backend functions.
|
|
@@ -5,7 +5,7 @@ import { GetAccessTokenOptions, SaveAccessTokenOptions, RemoveAccessTokenOptions
|
|
|
5
5
|
* Low-level utility for manually retrieving tokens. In most cases, the Base44 client handles
|
|
6
6
|
* token management automatically. This function is useful for custom authentication flows or when you need direct access to stored tokens. Requires a browser environment and can't be used in the backend.
|
|
7
7
|
*
|
|
8
|
-
* When a host platform has embedded the app with a one-time token (`?ott=`), that token is returned as it stands: it is what {@linkcode createClient} trades for the session, so a page that gates on "is there a token?" as it loads sees one. It is neither stored nor removed from the URL here.
|
|
8
|
+
* When a host platform has embedded the app with a one-time token (`?ott=`), that token is returned as it stands: it is what {@linkcode createClient} trades for the session, so a page that gates on "is there a token?" as it loads sees one. It is neither stored nor removed from the URL here, and it is reported only inside a frame, where such a token is redeemed.
|
|
9
9
|
*
|
|
10
10
|
* @internal
|
|
11
11
|
*
|
package/dist/utils/auth-utils.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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,53 +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
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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
|
*/
|
|
22
|
+
export declare const EMBED_TOKEN_PARAM = "ott";
|
|
23
|
+
/** @internal */
|
|
24
|
+
export declare function takeEmbedTokenFromUrl(): string | null;
|
|
25
|
+
/** @internal */
|
|
28
26
|
export declare function isFramed(): boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Whether this tab previously redeemed an embed token. Counts only inside a
|
|
31
|
-
* frame: a top-level tab that once carried a token must fall back to the
|
|
32
|
-
* regular login, or it could never sign in again.
|
|
33
|
-
*
|
|
34
|
-
* @internal
|
|
35
|
-
*/
|
|
36
|
-
export declare function isEmbeddedTab(): boolean;
|
|
37
27
|
/** @internal */
|
|
38
|
-
export declare function markEmbeddedTab(): 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
28
|
export declare function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl, }: {
|
|
46
29
|
serverUrl: string;
|
|
47
30
|
appId: string;
|
|
48
31
|
ott: string;
|
|
49
32
|
fetchImpl?: typeof fetch;
|
|
50
33
|
}): 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, 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.
|
|
56
|
-
*
|
|
57
|
-
* @internal
|
|
58
|
-
*/
|
|
34
|
+
/** @internal */
|
|
59
35
|
export declare function showEmbedSessionEnded(): void;
|
|
@@ -7,22 +7,26 @@
|
|
|
7
7
|
* grant (RFC 8693), and keeps the result in memory only — never in storage — so
|
|
8
8
|
* the session lives and dies with the frame.
|
|
9
9
|
*
|
|
10
|
+
* Taking the token off the URL is unconditional — a one-time token must not be
|
|
11
|
+
* left in the address bar, in history, or in a shared link — but it is only
|
|
12
|
+
* reported back inside a frame, the only place one is redeemed. It is gone once
|
|
13
|
+
* the first client has taken it, so the session belongs to that client — an app
|
|
14
|
+
* creates one.
|
|
15
|
+
*
|
|
16
|
+
* The exchange endpoint is rate limited per app, and its limiter refuses before
|
|
17
|
+
* the one-time token is redeemed — so a 429 leaves the token still valid and is
|
|
18
|
+
* worth retrying once.
|
|
19
|
+
*
|
|
10
20
|
* @internal
|
|
11
21
|
*/
|
|
12
|
-
/** The query parameter a host platform puts the one-time token on. @internal */
|
|
13
22
|
export const EMBED_TOKEN_PARAM = "ott";
|
|
14
|
-
const EMBED_TAB_KEY = "base44_embed_session";
|
|
15
23
|
const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
16
24
|
const SUBJECT_TOKEN_TYPE = "urn:base44:params:oauth:token-type:embed-ott";
|
|
17
25
|
const RATE_LIMITED = 429;
|
|
18
26
|
const RATE_LIMIT_RETRY_MS = 1000;
|
|
27
|
+
const EXCHANGE_TIMEOUT_MS = 15000;
|
|
19
28
|
const SESSION_ENDED_ELEMENT_ID = "base44-embed-session-ended";
|
|
20
|
-
/**
|
|
21
|
-
* Reads the one-time token off the current URL and strips it, so it is never
|
|
22
|
-
* left in the address bar, in history, or in a shared link.
|
|
23
|
-
*
|
|
24
|
-
* @internal
|
|
25
|
-
*/
|
|
29
|
+
/** @internal */
|
|
26
30
|
export function takeEmbedTokenFromUrl() {
|
|
27
31
|
if (typeof window === "undefined" || !window.location) {
|
|
28
32
|
return null;
|
|
@@ -38,73 +42,20 @@ export function takeEmbedTokenFromUrl() {
|
|
|
38
42
|
window.history.replaceState(window.history.state, "", url.toString());
|
|
39
43
|
}
|
|
40
44
|
catch (e) {
|
|
41
|
-
// The token was read but the URL could not be rewritten (history blocked,
|
|
42
|
-
// a sandboxed frame, replaceState rate-limited). Return it anyway: it is
|
|
43
|
-
// this load's identity, and dropping it here would leave `?ott=` on the
|
|
44
|
-
// URL for getAccessToken to hand back as if it were a session token.
|
|
45
45
|
console.error("Error retrieving embed token from URL:", e);
|
|
46
46
|
}
|
|
47
|
-
return ott;
|
|
47
|
+
return isFramed() ? ott : null;
|
|
48
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
|
-
*/
|
|
49
|
+
/** @internal */
|
|
56
50
|
export function isFramed() {
|
|
57
|
-
|
|
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;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Whether this tab previously redeemed an embed token. Counts only inside a
|
|
71
|
-
* frame: a top-level tab that once carried a token must fall back to the
|
|
72
|
-
* regular login, or it could never sign in again.
|
|
73
|
-
*
|
|
74
|
-
* @internal
|
|
75
|
-
*/
|
|
76
|
-
export function isEmbeddedTab() {
|
|
77
|
-
if (typeof window === "undefined") {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
// A cross-site frame is third-party storage, which some browsers block
|
|
81
|
-
// outright — every access can throw, and the marker is then unavailable.
|
|
82
|
-
try {
|
|
83
|
-
return isFramed() && window.sessionStorage.getItem(EMBED_TAB_KEY) === "1";
|
|
84
|
-
}
|
|
85
|
-
catch (_a) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
51
|
+
return typeof window !== "undefined" && window.self !== window.top;
|
|
88
52
|
}
|
|
89
53
|
/** @internal */
|
|
90
|
-
export function markEmbeddedTab() {
|
|
91
|
-
try {
|
|
92
|
-
window.sessionStorage.setItem(EMBED_TAB_KEY, "1");
|
|
93
|
-
}
|
|
94
|
-
catch (_a) {
|
|
95
|
-
/* storage blocked */
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Trades a one-time embed token for an app-user session token. Resolves to
|
|
100
|
-
* `null` when the platform refuses the token; never throws.
|
|
101
|
-
*
|
|
102
|
-
* @internal
|
|
103
|
-
*/
|
|
104
54
|
export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fetch, }) {
|
|
105
55
|
const post = () => fetchImpl(`${serverUrl}/api/apps/${appId}/auth/embed/token`, {
|
|
106
56
|
method: "POST",
|
|
107
57
|
headers: { "X-App-Id": String(appId) },
|
|
58
|
+
signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS),
|
|
108
59
|
body: new URLSearchParams({
|
|
109
60
|
grant_type: GRANT_TYPE,
|
|
110
61
|
subject_token: ott,
|
|
@@ -113,7 +64,6 @@ export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fe
|
|
|
113
64
|
});
|
|
114
65
|
try {
|
|
115
66
|
let response = await post();
|
|
116
|
-
// The limiter refuses before the token is redeemed, so it is still valid.
|
|
117
67
|
if (response.status === RATE_LIMITED) {
|
|
118
68
|
await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_RETRY_MS));
|
|
119
69
|
response = await post();
|
|
@@ -129,14 +79,7 @@ export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fe
|
|
|
129
79
|
return null;
|
|
130
80
|
}
|
|
131
81
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Covers the page with a plain "session ended" notice. An embedded session can
|
|
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.
|
|
137
|
-
*
|
|
138
|
-
* @internal
|
|
139
|
-
*/
|
|
82
|
+
/** @internal */
|
|
140
83
|
export function showEmbedSessionEnded() {
|
|
141
84
|
if (typeof document === "undefined" || !document.body) {
|
|
142
85
|
return;
|
|
@@ -144,21 +87,26 @@ export function showEmbedSessionEnded() {
|
|
|
144
87
|
if (document.getElementById(SESSION_ENDED_ELEMENT_ID)) {
|
|
145
88
|
return;
|
|
146
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;}}`;
|
|
147
95
|
const overlay = document.createElement("div");
|
|
148
96
|
overlay.id = SESSION_ENDED_ELEMENT_ID;
|
|
149
97
|
overlay.setAttribute("role", "alert");
|
|
150
98
|
overlay.style.cssText =
|
|
151
99
|
"position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;" +
|
|
152
|
-
"justify-content:center;background
|
|
100
|
+
"justify-content:center;background:var(--b44-bg);color:var(--b44-fg);" +
|
|
153
101
|
"font-family:ui-sans-serif,system-ui,sans-serif;text-align:center;padding:2rem;";
|
|
154
102
|
const title = document.createElement("h1");
|
|
155
103
|
title.textContent = "Session ended";
|
|
156
104
|
title.style.cssText = "font-size:1.5rem;font-weight:700;margin:0 0 .75rem;";
|
|
157
105
|
const body = document.createElement("p");
|
|
158
106
|
body.textContent = "Reload this page in your browser to start a new session.";
|
|
159
|
-
body.style.cssText = "margin:0;color
|
|
107
|
+
body.style.cssText = "margin:0;color:var(--b44-muted);";
|
|
160
108
|
const card = document.createElement("div");
|
|
161
109
|
card.append(title, body);
|
|
162
|
-
overlay.append(card);
|
|
110
|
+
overlay.append(style, card);
|
|
163
111
|
document.body.append(overlay);
|
|
164
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
|