@base44-preview/sdk 0.8.48-pr.282.028b41c → 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 +46 -13
- package/dist/client.types.d.ts +13 -2
- package/dist/modules/actors.d.ts +1 -1
- package/dist/modules/actors.js +1 -1
- package/dist/modules/auth.js +23 -3
- package/dist/modules/auth.types.d.ts +8 -1
- 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 +19 -7
- package/dist/utils/embed-session.js +46 -14
- 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, 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";
|
|
@@ -58,13 +58,19 @@ 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
|
-
|
|
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;
|
|
62
67
|
if (embedOtt) {
|
|
63
|
-
markEmbeddedTab();
|
|
68
|
+
markEmbeddedTab(appId);
|
|
64
69
|
}
|
|
65
|
-
const embedded = Boolean(embedOtt) || isEmbeddedTab();
|
|
66
|
-
//
|
|
67
|
-
|
|
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;
|
|
68
74
|
const headers = {
|
|
69
75
|
...optionalHeaders,
|
|
70
76
|
"X-App-Id": String(appId),
|
|
@@ -118,6 +124,7 @@ export function createClient(config) {
|
|
|
118
124
|
serverUrl,
|
|
119
125
|
token,
|
|
120
126
|
embedded,
|
|
127
|
+
onEmbedSessionEnded: options === null || options === void 0 ? void 0 : options.onEmbedSessionEnded,
|
|
121
128
|
});
|
|
122
129
|
// Apply the access token before any module that may issue authenticated
|
|
123
130
|
// requests during construction (notably analytics, which fires an init
|
|
@@ -171,6 +178,21 @@ export function createClient(config) {
|
|
|
171
178
|
console.error("Base44: applying the embedded session failed:", e);
|
|
172
179
|
})
|
|
173
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
|
+
};
|
|
174
196
|
if (embedOtt) {
|
|
175
197
|
// Requests issued during the exchange wait for it. Registered after createAxiosClient's
|
|
176
198
|
// interceptors so it runs first and the anonymous-visitor header sees the Authorization.
|
|
@@ -191,7 +213,10 @@ export function createClient(config) {
|
|
|
191
213
|
// URL needs an absolute host, so fall back to the page origin.
|
|
192
214
|
host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_a = window.location) === null || _a === void 0 ? void 0 : _a.origin : undefined),
|
|
193
215
|
functionsVersion,
|
|
194
|
-
getAuthToken:
|
|
216
|
+
getAuthToken: async () => {
|
|
217
|
+
await authReady;
|
|
218
|
+
return getToken();
|
|
219
|
+
},
|
|
195
220
|
mintConnectionToken: async (actorName, room, connectionId) => {
|
|
196
221
|
await authReady;
|
|
197
222
|
const authToken = getToken();
|
|
@@ -219,6 +244,7 @@ export function createClient(config) {
|
|
|
219
244
|
connectors: createUserConnectorsModule(axiosClient, appId),
|
|
220
245
|
auth: userAuthModule,
|
|
221
246
|
functions: createFunctionsModule(functionsAxiosClient, appId, {
|
|
247
|
+
waitForAuth: () => authReady,
|
|
222
248
|
getAuthHeaders: () => {
|
|
223
249
|
const headers = {};
|
|
224
250
|
const sessionToken = getToken();
|
|
@@ -234,9 +260,13 @@ export function createClient(config) {
|
|
|
234
260
|
getSocket,
|
|
235
261
|
appId,
|
|
236
262
|
serverUrl,
|
|
237
|
-
getToken,
|
|
263
|
+
getToken: getTokenNow,
|
|
264
|
+
}),
|
|
265
|
+
aiGateway: createAiGatewayModule({
|
|
266
|
+
serverUrl,
|
|
267
|
+
getToken: getTokenNow,
|
|
268
|
+
appId,
|
|
238
269
|
}),
|
|
239
|
-
aiGateway: createAiGatewayModule({ serverUrl, getToken, appId }),
|
|
240
270
|
appLogs: createAppLogsModule(axiosClient, appId),
|
|
241
271
|
app: createAppModule(axiosClient, appId),
|
|
242
272
|
users: createUsersModule(axiosClient, appId),
|
|
@@ -281,7 +311,10 @@ export function createClient(config) {
|
|
|
281
311
|
getSocket,
|
|
282
312
|
appId,
|
|
283
313
|
serverUrl,
|
|
284
|
-
|
|
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,
|
|
285
318
|
}),
|
|
286
319
|
aiGateway: createAiGatewayModule({
|
|
287
320
|
serverUrl,
|
|
@@ -322,12 +355,12 @@ export function createClient(config) {
|
|
|
322
355
|
serverUrl,
|
|
323
356
|
functionsVersion,
|
|
324
357
|
platformHeaders: optionalHeaders,
|
|
358
|
+
waitForAuth: () => authReady,
|
|
325
359
|
}),
|
|
326
360
|
/**
|
|
327
361
|
* Sets a new authentication token for all subsequent requests.
|
|
328
362
|
*
|
|
329
363
|
* @param newToken - The new authentication token
|
|
330
|
-
* @param saveToStorage - Whether to also keep the token in local storage. Defaults to `true`.
|
|
331
364
|
*
|
|
332
365
|
* @example
|
|
333
366
|
* ```typescript
|
|
@@ -339,8 +372,8 @@ export function createClient(config) {
|
|
|
339
372
|
* base44.setToken(access_token);
|
|
340
373
|
* ```
|
|
341
374
|
*/
|
|
342
|
-
setToken(newToken
|
|
343
|
-
applyToken(newToken,
|
|
375
|
+
setToken(newToken) {
|
|
376
|
+
applyToken(newToken, true);
|
|
344
377
|
},
|
|
345
378
|
/**
|
|
346
379
|
* Gets the current client configuration.
|
package/dist/client.types.d.ts
CHANGED
|
@@ -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
|
|
@@ -200,9 +212,8 @@ export interface Base44Client {
|
|
|
200
212
|
* Updates the token for both HTTP requests and WebSocket connections.
|
|
201
213
|
*
|
|
202
214
|
* @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
215
|
*/
|
|
205
|
-
setToken(newToken: string
|
|
216
|
+
setToken(newToken: string): void;
|
|
206
217
|
/**
|
|
207
218
|
* Gets the current client configuration.
|
|
208
219
|
* @internal
|
package/dist/modules/actors.d.ts
CHANGED
|
@@ -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;
|
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;
|
package/dist/modules/auth.js
CHANGED
|
@@ -83,6 +83,17 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
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
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
|
+
};
|
|
86
97
|
return {
|
|
87
98
|
hasToken() {
|
|
88
99
|
return accessToken !== null;
|
|
@@ -116,10 +127,12 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
116
127
|
if (typeof window === "undefined") {
|
|
117
128
|
throw new Error("Login method can only be used in a browser environment");
|
|
118
129
|
}
|
|
119
|
-
//
|
|
120
|
-
// login
|
|
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.)
|
|
121
134
|
if (options.embedded) {
|
|
122
|
-
|
|
135
|
+
endEmbeddedSession();
|
|
123
136
|
return;
|
|
124
137
|
}
|
|
125
138
|
// If nextUrl is not provided, use the current URL
|
|
@@ -178,6 +191,13 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
178
191
|
console.error("Failed to remove token from localStorage:", e);
|
|
179
192
|
}
|
|
180
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
|
+
}
|
|
181
201
|
// Determine the from_url parameter
|
|
182
202
|
const fromUrl = redirectUrl || window.location.href;
|
|
183
203
|
// Redirect to server-side logout endpoint to clear HTTP-only cookies
|
|
@@ -103,6 +103,11 @@ export interface AuthModuleOptions {
|
|
|
103
103
|
* session is minted by the platform and cannot be renewed by a login redirect.
|
|
104
104
|
*/
|
|
105
105
|
embedded?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Called instead of the SDK's built-in notice when an embedded session ends.
|
|
108
|
+
* See {@link CreateClientOptions.onEmbedSessionEnded}.
|
|
109
|
+
*/
|
|
110
|
+
onEmbedSessionEnded?: () => void;
|
|
106
111
|
}
|
|
107
112
|
/**
|
|
108
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.
|
|
@@ -187,7 +192,9 @@ export interface AuthModule {
|
|
|
187
192
|
/**
|
|
188
193
|
* Whether the app is running embedded in a host platform.
|
|
189
194
|
*
|
|
190
|
-
* A platform embeds an app in an iframe and signs its user in by minting a one-time token onto the frame's URL; the SDK trades it for a session as the client is created. That session is held in memory only and can be renewed only by the platform, so when it ends there is no login page to send the user to — {@linkcode AuthModule.redirectToLogin | redirectToLogin()}
|
|
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.
|
|
191
198
|
*
|
|
192
199
|
* @returns `true` when the app was embedded by a host platform, `false` otherwise.
|
|
193
200
|
*
|
|
@@ -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) {
|
|
@@ -19,15 +19,23 @@ export declare const EMBED_TOKEN_PARAM = "ott";
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function takeEmbedTokenFromUrl(): string | null;
|
|
21
21
|
/**
|
|
22
|
-
* Whether this
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
25
|
*
|
|
26
26
|
* @internal
|
|
27
27
|
*/
|
|
28
|
-
export declare function
|
|
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;
|
|
29
37
|
/** @internal */
|
|
30
|
-
export declare function markEmbeddedTab(): void;
|
|
38
|
+
export declare function markEmbeddedTab(appId: string): void;
|
|
31
39
|
/**
|
|
32
40
|
* Trades a one-time embed token for an app-user session token. Resolves to
|
|
33
41
|
* `null` when the platform refuses the token; never throws.
|
|
@@ -43,8 +51,12 @@ export declare function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl, }
|
|
|
43
51
|
/**
|
|
44
52
|
* Covers the page with a plain "session ended" notice. An embedded session can
|
|
45
53
|
* only be renewed by the host platform, so this stands in for the login
|
|
46
|
-
* redirect
|
|
47
|
-
* page: reloading the frame itself carries no token and lands here again.
|
|
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.
|
|
48
60
|
*
|
|
49
61
|
* @internal
|
|
50
62
|
*/
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** The query parameter a host platform puts the one-time token on. @internal */
|
|
13
13
|
export const EMBED_TOKEN_PARAM = "ott";
|
|
14
|
-
const
|
|
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}`;
|
|
15
17
|
const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
16
18
|
const SUBJECT_TOKEN_TYPE = "urn:base44:params:oauth:token-type:embed-ott";
|
|
17
19
|
const RATE_LIMITED = 429;
|
|
@@ -47,30 +49,49 @@ export function takeEmbedTokenFromUrl() {
|
|
|
47
49
|
return ott;
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
50
|
-
* Whether this
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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.
|
|
53
55
|
*
|
|
54
56
|
* @internal
|
|
55
57
|
*/
|
|
56
|
-
export function
|
|
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) {
|
|
57
79
|
if (typeof window === "undefined") {
|
|
58
80
|
return false;
|
|
59
81
|
}
|
|
60
82
|
// A cross-site frame is third-party storage, which some browsers block
|
|
61
83
|
// outright — every access can throw, and the marker is then unavailable.
|
|
62
84
|
try {
|
|
63
|
-
return (
|
|
64
|
-
window.sessionStorage.getItem(EMBED_TAB_KEY) === "1");
|
|
85
|
+
return (isFramed() && window.sessionStorage.getItem(embedTabKey(appId)) === "1");
|
|
65
86
|
}
|
|
66
87
|
catch (_a) {
|
|
67
88
|
return false;
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
91
|
/** @internal */
|
|
71
|
-
export function markEmbeddedTab() {
|
|
92
|
+
export function markEmbeddedTab(appId) {
|
|
72
93
|
try {
|
|
73
|
-
window.sessionStorage.setItem(
|
|
94
|
+
window.sessionStorage.setItem(embedTabKey(appId), "1");
|
|
74
95
|
}
|
|
75
96
|
catch (_a) {
|
|
76
97
|
/* storage blocked */
|
|
@@ -113,8 +134,12 @@ export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fe
|
|
|
113
134
|
/**
|
|
114
135
|
* Covers the page with a plain "session ended" notice. An embedded session can
|
|
115
136
|
* only be renewed by the host platform, so this stands in for the login
|
|
116
|
-
* redirect
|
|
117
|
-
* page: reloading the frame itself carries no token and lands here again.
|
|
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.
|
|
118
143
|
*
|
|
119
144
|
* @internal
|
|
120
145
|
*/
|
|
@@ -125,21 +150,28 @@ export function showEmbedSessionEnded() {
|
|
|
125
150
|
if (document.getElementById(SESSION_ENDED_ELEMENT_ID)) {
|
|
126
151
|
return;
|
|
127
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;}}`;
|
|
128
160
|
const overlay = document.createElement("div");
|
|
129
161
|
overlay.id = SESSION_ENDED_ELEMENT_ID;
|
|
130
162
|
overlay.setAttribute("role", "alert");
|
|
131
163
|
overlay.style.cssText =
|
|
132
164
|
"position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;" +
|
|
133
|
-
"justify-content:center;background
|
|
165
|
+
"justify-content:center;background:var(--b44-bg);color:var(--b44-fg);" +
|
|
134
166
|
"font-family:ui-sans-serif,system-ui,sans-serif;text-align:center;padding:2rem;";
|
|
135
167
|
const title = document.createElement("h1");
|
|
136
168
|
title.textContent = "Session ended";
|
|
137
169
|
title.style.cssText = "font-size:1.5rem;font-weight:700;margin:0 0 .75rem;";
|
|
138
170
|
const body = document.createElement("p");
|
|
139
171
|
body.textContent = "Reload this page in your browser to start a new session.";
|
|
140
|
-
body.style.cssText = "margin:0;color
|
|
172
|
+
body.style.cssText = "margin:0;color:var(--b44-muted);";
|
|
141
173
|
const card = document.createElement("div");
|
|
142
174
|
card.append(title, body);
|
|
143
|
-
overlay.append(card);
|
|
175
|
+
overlay.append(style, card);
|
|
144
176
|
document.body.append(overlay);
|
|
145
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
|