@base44-preview/sdk 0.8.48-pr.281.97e1467 → 0.8.48-pr.282.028b41c
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 +84 -36
- package/dist/client.types.d.ts +2 -1
- package/dist/modules/agents.d.ts +1 -1
- package/dist/modules/agents.js +3 -4
- package/dist/modules/agents.types.d.ts +2 -2
- package/dist/modules/ai-gateway.d.ts +1 -1
- package/dist/modules/ai-gateway.js +2 -3
- package/dist/modules/ai-gateway.types.d.ts +2 -2
- package/dist/modules/auth.js +22 -7
- package/dist/modules/auth.types.d.ts +26 -0
- package/dist/utils/auth-utils.d.ts +2 -0
- package/dist/utils/auth-utils.js +11 -0
- package/dist/utils/embed-session.d.ts +51 -0
- package/dist/utils/embed-session.js +145 -0
- package/dist/utils/socket-utils.d.ts +3 -2
- package/dist/utils/socket-utils.js +5 -11
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createAuthModule } from "./modules/auth.js";
|
|
|
5
5
|
import { createSsoModule } from "./modules/sso.js";
|
|
6
6
|
import { createConnectorsModule, createUserConnectorsModule, } from "./modules/connectors.js";
|
|
7
7
|
import { getAccessToken } from "./utils/auth-utils.js";
|
|
8
|
+
import { exchangeEmbedToken, isEmbeddedTab, markEmbeddedTab, takeEmbedTokenFromUrl, } from "./utils/embed-session.js";
|
|
8
9
|
import { createFetchWithAuth } from "./utils/fetch-with-auth.js";
|
|
9
10
|
import { createFunctionsModule } from "./modules/functions.js";
|
|
10
11
|
import { createAgentsModule } from "./modules/agents.js";
|
|
@@ -54,25 +55,16 @@ import { createActorsModule, resolveActorsHost, } from "./modules/actors.js";
|
|
|
54
55
|
*/
|
|
55
56
|
export function createClient(config) {
|
|
56
57
|
var _a, _b, _c, _d;
|
|
57
|
-
const { serverUrl = "https://base44.app", appId, analytics,
|
|
58
|
+
const { serverUrl = "https://base44.app", appId, analytics, serviceToken, requiresAuth = false, appBaseUrl, options, functionsVersion, headers: optionalHeaders, } = config;
|
|
58
59
|
// Normalize appBaseUrl to always be a string (empty if not provided or invalid)
|
|
59
60
|
const normalizedAppBaseUrl = typeof appBaseUrl === "string" ? appBaseUrl : "";
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
let socket = null;
|
|
68
|
-
const getSocket = () => {
|
|
69
|
-
if (!socket) {
|
|
70
|
-
socket = RoomsSocket({
|
|
71
|
-
config: socketConfig,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
return socket;
|
|
75
|
-
};
|
|
61
|
+
const embedOtt = takeEmbedTokenFromUrl();
|
|
62
|
+
if (embedOtt) {
|
|
63
|
+
markEmbeddedTab();
|
|
64
|
+
}
|
|
65
|
+
const embedded = Boolean(embedOtt) || isEmbeddedTab();
|
|
66
|
+
// In a frame the passed token is the OTT itself or a stale stored one, not this session.
|
|
67
|
+
const token = embedded ? undefined : config.token;
|
|
76
68
|
const headers = {
|
|
77
69
|
...optionalHeaders,
|
|
78
70
|
"X-App-Id": String(appId),
|
|
@@ -125,26 +117,84 @@ export function createClient(config) {
|
|
|
125
117
|
appBaseUrl: normalizedAppBaseUrl,
|
|
126
118
|
serverUrl,
|
|
127
119
|
token,
|
|
120
|
+
embedded,
|
|
128
121
|
});
|
|
129
122
|
// Apply the access token before any module that may issue authenticated
|
|
130
123
|
// requests during construction (notably analytics, which fires an init
|
|
131
124
|
// event whose flush calls auth.me()). Without this, the first User/me
|
|
132
125
|
// request is built before setToken runs and goes out unauthenticated.
|
|
133
|
-
|
|
126
|
+
// Skipped in a frame: the session comes from the exchange below.
|
|
127
|
+
if (typeof window !== "undefined" && !embedded) {
|
|
134
128
|
const accessToken = token || getAccessToken();
|
|
135
129
|
if (accessToken) {
|
|
136
130
|
userAuthModule.setToken(accessToken);
|
|
137
131
|
}
|
|
138
132
|
}
|
|
133
|
+
// Live token for every module; outside a frame it still falls back to storage.
|
|
134
|
+
const getToken = () => { var _a; return (_a = userAuthModule.getToken()) !== null && _a !== void 0 ? _a : (embedded ? null : getAccessToken()); };
|
|
135
|
+
const socketConfig = {
|
|
136
|
+
serverUrl,
|
|
137
|
+
mountPath: "/ws-user-apps/socket.io/",
|
|
138
|
+
transports: ["websocket"],
|
|
139
|
+
appId,
|
|
140
|
+
getToken,
|
|
141
|
+
};
|
|
142
|
+
let socket = null;
|
|
143
|
+
const getSocket = () => {
|
|
144
|
+
if (!socket) {
|
|
145
|
+
socket = RoomsSocket({ config: socketConfig });
|
|
146
|
+
}
|
|
147
|
+
return socket;
|
|
148
|
+
};
|
|
149
|
+
const applyToken = (newToken, saveToStorage) => {
|
|
150
|
+
userAuthModule.setToken(newToken, saveToStorage);
|
|
151
|
+
socket === null || socket === void 0 ? void 0 : socket.reconnect();
|
|
152
|
+
};
|
|
153
|
+
// Settles once the exchanged session is applied (memory only); at once
|
|
154
|
+
// otherwise. Never rejects: every request waits on it, so a failure here
|
|
155
|
+
// must not turn into a rejection on each of them.
|
|
156
|
+
const authReady = embedOtt
|
|
157
|
+
? exchangeEmbedToken({ serverUrl, appId, ott: embedOtt })
|
|
158
|
+
.then((sessionToken) => {
|
|
159
|
+
var _a;
|
|
160
|
+
if (sessionToken) {
|
|
161
|
+
applyToken(sessionToken, false);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// The app is about to run anonymous. Say why, once, instead of
|
|
165
|
+
// leaving only the 401s that follow.
|
|
166
|
+
const error = new Error("Base44: the embed token was refused, so this app is not signed in.");
|
|
167
|
+
console.error(error.message);
|
|
168
|
+
(_a = options === null || options === void 0 ? void 0 : options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
|
|
169
|
+
})
|
|
170
|
+
.catch((e) => {
|
|
171
|
+
console.error("Base44: applying the embedded session failed:", e);
|
|
172
|
+
})
|
|
173
|
+
: Promise.resolve();
|
|
174
|
+
if (embedOtt) {
|
|
175
|
+
// Requests issued during the exchange wait for it. Registered after createAxiosClient's
|
|
176
|
+
// interceptors so it runs first and the anonymous-visitor header sees the Authorization.
|
|
177
|
+
for (const client of [axiosClient, functionsAxiosClient]) {
|
|
178
|
+
client.interceptors.request.use(async (requestConfig) => {
|
|
179
|
+
await authReady;
|
|
180
|
+
const sessionToken = getToken();
|
|
181
|
+
if (sessionToken && !requestConfig.headers.get("Authorization")) {
|
|
182
|
+
requestConfig.headers.set("Authorization", `Bearer ${sessionToken}`);
|
|
183
|
+
}
|
|
184
|
+
return requestConfig;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
139
188
|
const actorsModule = createActorsModule({
|
|
140
189
|
appId,
|
|
141
190
|
// serverUrl is often relative/empty (same-origin app); the proxy-fallback
|
|
142
191
|
// URL needs an absolute host, so fall back to the page origin.
|
|
143
192
|
host: resolveActorsHost(serverUrl, typeof window !== "undefined" ? (_a = window.location) === null || _a === void 0 ? void 0 : _a.origin : undefined),
|
|
144
193
|
functionsVersion,
|
|
145
|
-
getAuthToken:
|
|
194
|
+
getAuthToken: getToken,
|
|
146
195
|
mintConnectionToken: async (actorName, room, connectionId) => {
|
|
147
|
-
|
|
196
|
+
await authReady;
|
|
197
|
+
const authToken = getToken();
|
|
148
198
|
return await actorsAxiosClient.post(`/apps/${appId}/actors/${encodeURIComponent(actorName)}/connection-token`, { room, connection_id: connectionId }, {
|
|
149
199
|
headers: {
|
|
150
200
|
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
@@ -171,10 +221,9 @@ export function createClient(config) {
|
|
|
171
221
|
functions: createFunctionsModule(functionsAxiosClient, appId, {
|
|
172
222
|
getAuthHeaders: () => {
|
|
173
223
|
const headers = {};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
headers["Authorization"] = `Bearer ${currentToken}`;
|
|
224
|
+
const sessionToken = getToken();
|
|
225
|
+
if (sessionToken) {
|
|
226
|
+
headers["Authorization"] = `Bearer ${sessionToken}`;
|
|
178
227
|
}
|
|
179
228
|
return headers;
|
|
180
229
|
},
|
|
@@ -185,9 +234,9 @@ export function createClient(config) {
|
|
|
185
234
|
getSocket,
|
|
186
235
|
appId,
|
|
187
236
|
serverUrl,
|
|
188
|
-
|
|
237
|
+
getToken,
|
|
189
238
|
}),
|
|
190
|
-
aiGateway: createAiGatewayModule({ serverUrl,
|
|
239
|
+
aiGateway: createAiGatewayModule({ serverUrl, getToken, appId }),
|
|
191
240
|
appLogs: createAppLogsModule(axiosClient, appId),
|
|
192
241
|
app: createAppModule(axiosClient, appId),
|
|
193
242
|
users: createUsersModule(axiosClient, appId),
|
|
@@ -232,9 +281,13 @@ export function createClient(config) {
|
|
|
232
281
|
getSocket,
|
|
233
282
|
appId,
|
|
234
283
|
serverUrl,
|
|
235
|
-
token,
|
|
284
|
+
getToken: () => token,
|
|
285
|
+
}),
|
|
286
|
+
aiGateway: createAiGatewayModule({
|
|
287
|
+
serverUrl,
|
|
288
|
+
getToken: () => serviceToken,
|
|
289
|
+
appId,
|
|
236
290
|
}),
|
|
237
|
-
aiGateway: createAiGatewayModule({ serverUrl, token: serviceToken, appId }),
|
|
238
291
|
appLogs: createAppLogsModule(serviceRoleAxiosClient, appId),
|
|
239
292
|
cleanup: () => {
|
|
240
293
|
if (socket) {
|
|
@@ -274,6 +327,7 @@ export function createClient(config) {
|
|
|
274
327
|
* Sets a new authentication token for all subsequent requests.
|
|
275
328
|
*
|
|
276
329
|
* @param newToken - The new authentication token
|
|
330
|
+
* @param saveToStorage - Whether to also keep the token in local storage. Defaults to `true`.
|
|
277
331
|
*
|
|
278
332
|
* @example
|
|
279
333
|
* ```typescript
|
|
@@ -285,14 +339,8 @@ export function createClient(config) {
|
|
|
285
339
|
* base44.setToken(access_token);
|
|
286
340
|
* ```
|
|
287
341
|
*/
|
|
288
|
-
setToken(newToken) {
|
|
289
|
-
|
|
290
|
-
if (socket) {
|
|
291
|
-
socket.updateConfig({
|
|
292
|
-
token: newToken,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
socketConfig.token = newToken;
|
|
342
|
+
setToken(newToken, saveToStorage = true) {
|
|
343
|
+
applyToken(newToken, saveToStorage);
|
|
296
344
|
},
|
|
297
345
|
/**
|
|
298
346
|
* Gets the current client configuration.
|
package/dist/client.types.d.ts
CHANGED
|
@@ -200,8 +200,9 @@ 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`.
|
|
203
204
|
*/
|
|
204
|
-
setToken(newToken: string): void;
|
|
205
|
+
setToken(newToken: string, saveToStorage?: boolean): void;
|
|
205
206
|
/**
|
|
206
207
|
* Gets the current client configuration.
|
|
207
208
|
* @internal
|
package/dist/modules/agents.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { AgentsModule, AgentsModuleConfig } from "./agents.types.js";
|
|
2
|
-
export declare function createAgentsModule({ axios, getSocket, appId, serverUrl,
|
|
2
|
+
export declare function createAgentsModule({ axios, getSocket, appId, serverUrl, getToken, }: AgentsModuleConfig): AgentsModule;
|
package/dist/modules/agents.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export function createAgentsModule({ axios, getSocket, appId, serverUrl, token, }) {
|
|
1
|
+
export function createAgentsModule({ axios, getSocket, appId, serverUrl, getToken, }) {
|
|
3
2
|
const baseURL = `/apps/${appId}/agents`;
|
|
4
3
|
// Track active conversations
|
|
5
4
|
const currentConversations = {};
|
|
@@ -56,7 +55,7 @@ export function createAgentsModule({ axios, getSocket, appId, serverUrl, token,
|
|
|
56
55
|
};
|
|
57
56
|
const getWhatsAppConnectURL = (agentName) => {
|
|
58
57
|
const baseUrl = `${serverUrl}/api/apps/${appId}/agents/${encodeURIComponent(agentName)}/whatsapp`;
|
|
59
|
-
const accessToken =
|
|
58
|
+
const accessToken = getToken();
|
|
60
59
|
if (accessToken) {
|
|
61
60
|
return `${baseUrl}?token=${accessToken}`;
|
|
62
61
|
}
|
|
@@ -67,7 +66,7 @@ export function createAgentsModule({ axios, getSocket, appId, serverUrl, token,
|
|
|
67
66
|
};
|
|
68
67
|
const getTelegramConnectURL = (agentName) => {
|
|
69
68
|
const baseUrl = `${serverUrl}/api/apps/${appId}/agents/${encodeURIComponent(agentName)}/telegram`;
|
|
70
|
-
const accessToken =
|
|
69
|
+
const accessToken = getToken();
|
|
71
70
|
if (accessToken) {
|
|
72
71
|
return `${baseUrl}?token=${accessToken}`;
|
|
73
72
|
}
|
|
@@ -160,8 +160,8 @@ export interface AgentsModuleConfig {
|
|
|
160
160
|
appId: string;
|
|
161
161
|
/** Server URL */
|
|
162
162
|
serverUrl?: string;
|
|
163
|
-
/**
|
|
164
|
-
|
|
163
|
+
/** Returns the current authentication token, if any */
|
|
164
|
+
getToken: () => string | null | undefined;
|
|
165
165
|
}
|
|
166
166
|
/**
|
|
167
167
|
* Agents module for managing AI agent conversations.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { AiGatewayModule, AiGatewayModuleConfig } from "./ai-gateway.types.js";
|
|
2
|
-
export declare function createAiGatewayModule({ serverUrl,
|
|
2
|
+
export declare function createAiGatewayModule({ serverUrl, getToken, appId, }: AiGatewayModuleConfig): AiGatewayModule;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
export function createAiGatewayModule({ serverUrl, token, appId, }) {
|
|
1
|
+
export function createAiGatewayModule({ serverUrl, getToken, appId, }) {
|
|
3
2
|
const connection = () => {
|
|
4
3
|
var _a;
|
|
5
4
|
return ({
|
|
6
5
|
baseURL: `${serverUrl}/api/apps/${appId}/ai/openai/v1`,
|
|
7
|
-
token: (_a =
|
|
6
|
+
token: (_a = getToken()) !== null && _a !== void 0 ? _a : "",
|
|
8
7
|
});
|
|
9
8
|
};
|
|
10
9
|
return {
|
|
@@ -17,8 +17,8 @@ export interface AiGatewayConnection {
|
|
|
17
17
|
export interface AiGatewayModuleConfig {
|
|
18
18
|
/** Server URL */
|
|
19
19
|
serverUrl?: string;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/** Returns the current authentication token, if any */
|
|
21
|
+
getToken: () => string | null | undefined;
|
|
22
22
|
/** Application ID */
|
|
23
23
|
appId: string;
|
|
24
24
|
}
|
package/dist/modules/auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resetAnalyticsSessionContext } from "./analytics.js";
|
|
2
|
+
import { showEmbedSessionEnded } from "../utils/embed-session.js";
|
|
2
3
|
function isInsideIframe() {
|
|
3
4
|
if (typeof window === "undefined")
|
|
4
5
|
return false;
|
|
@@ -81,10 +82,16 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
81
82
|
// Tracked here rather than read off `axios.defaults` so the answer stays tied
|
|
82
83
|
// to the identity transitions below (`setToken`, `logout`) instead of to the
|
|
83
84
|
// header a caller may have set on the instance directly.
|
|
84
|
-
let
|
|
85
|
+
let accessToken = options.token || null;
|
|
85
86
|
return {
|
|
86
87
|
hasToken() {
|
|
87
|
-
return
|
|
88
|
+
return accessToken !== null;
|
|
89
|
+
},
|
|
90
|
+
getToken() {
|
|
91
|
+
return accessToken;
|
|
92
|
+
},
|
|
93
|
+
isEmbedded() {
|
|
94
|
+
return Boolean(options.embedded);
|
|
88
95
|
},
|
|
89
96
|
// Get current user information
|
|
90
97
|
async me() {
|
|
@@ -109,6 +116,12 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
109
116
|
if (typeof window === "undefined") {
|
|
110
117
|
throw new Error("Login method can only be used in a browser environment");
|
|
111
118
|
}
|
|
119
|
+
// An embedded session can only be renewed by the host platform; the
|
|
120
|
+
// login round trip cannot complete inside its frame.
|
|
121
|
+
if (options.embedded) {
|
|
122
|
+
showEmbedSessionEnded();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
112
125
|
// If nextUrl is not provided, use the current URL
|
|
113
126
|
const redirectUrl = nextUrl
|
|
114
127
|
? new URL(nextUrl, window.location.origin).toString()
|
|
@@ -151,7 +164,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
151
164
|
// flight would otherwise resolve into callers that run after the logout.
|
|
152
165
|
clearPendingMe();
|
|
153
166
|
resetAnalyticsSessionContext();
|
|
154
|
-
|
|
167
|
+
accessToken = null;
|
|
155
168
|
// Only do the rest if in a browser environment
|
|
156
169
|
if (typeof window !== "undefined") {
|
|
157
170
|
// Remove token from localStorage
|
|
@@ -176,18 +189,20 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
|
|
|
176
189
|
setToken(token, saveToStorage = true) {
|
|
177
190
|
if (!token)
|
|
178
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;
|
|
179
196
|
// Same reasoning as in `logout`: the identity changes here, so anything
|
|
180
197
|
// resolved for the previous one must not be handed to later callers.
|
|
181
198
|
clearPendingMe();
|
|
182
199
|
resetAnalyticsSessionContext();
|
|
183
|
-
|
|
200
|
+
accessToken = token;
|
|
184
201
|
// handle token change for axios clients
|
|
185
202
|
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
186
203
|
functionsAxiosClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
187
204
|
// Save token to localStorage if requested
|
|
188
|
-
if (
|
|
189
|
-
typeof window !== "undefined" &&
|
|
190
|
-
window.localStorage) {
|
|
205
|
+
if (persist && typeof window !== "undefined" && window.localStorage) {
|
|
191
206
|
try {
|
|
192
207
|
window.localStorage.setItem("base44_access_token", token);
|
|
193
208
|
// Set "token" that is set by the built-in SDK of platform version 2
|
|
@@ -98,6 +98,11 @@ export interface AuthModuleOptions {
|
|
|
98
98
|
* which is how the server-side SDK reports a token it never sets explicitly.
|
|
99
99
|
*/
|
|
100
100
|
token?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Whether this client runs embedded in a host platform's frame, where a
|
|
103
|
+
* session is minted by the platform and cannot be renewed by a login redirect.
|
|
104
|
+
*/
|
|
105
|
+
embedded?: boolean;
|
|
101
106
|
}
|
|
102
107
|
/**
|
|
103
108
|
* Authentication module for managing user authentication and authorization. The module automatically stores tokens in local storage when available and manages authorization headers for API requests.
|
|
@@ -179,6 +184,22 @@ export interface AuthModule {
|
|
|
179
184
|
* ```
|
|
180
185
|
*/
|
|
181
186
|
redirectToLogin(nextUrl: string): void;
|
|
187
|
+
/**
|
|
188
|
+
* Whether the app is running embedded in a host platform.
|
|
189
|
+
*
|
|
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()} shows 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.
|
|
191
|
+
*
|
|
192
|
+
* @returns `true` when the app was embedded by a host platform, `false` otherwise.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* // Show your own message instead of a login screen
|
|
197
|
+
* if (!user && base44.auth.isEmbedded()) {
|
|
198
|
+
* return <SessionEnded />;
|
|
199
|
+
* }
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
isEmbedded(): boolean;
|
|
182
203
|
/**
|
|
183
204
|
* Redirects the user to a third-party authentication provider's login page.
|
|
184
205
|
*
|
|
@@ -539,4 +560,9 @@ export interface InternalAuthModule extends AuthModule {
|
|
|
539
560
|
* could not succeed without a session, not to decide that one is valid.
|
|
540
561
|
*/
|
|
541
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;
|
|
542
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.
|
package/dist/utils/auth-utils.js
CHANGED
|
@@ -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);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sessions for apps embedded in a host platform.
|
|
3
|
+
*
|
|
4
|
+
* The platform's server mints a one-time token for one of its users and puts
|
|
5
|
+
* it on the iframe URL as `?ott=`. The client takes it off the URL as it is
|
|
6
|
+
* created, trades it for an app-user session through the OAuth token-exchange
|
|
7
|
+
* grant (RFC 8693), and keeps the result in memory only — never in storage — so
|
|
8
|
+
* the session lives and dies with the frame.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
/** The query parameter a host platform puts the one-time token on. @internal */
|
|
13
|
+
export declare const EMBED_TOKEN_PARAM = "ott";
|
|
14
|
+
/**
|
|
15
|
+
* Reads the one-time token off the current URL and strips it, so it is never
|
|
16
|
+
* left in the address bar, in history, or in a shared link.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare function takeEmbedTokenFromUrl(): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Whether this tab previously redeemed an embed token. Counts only inside a
|
|
23
|
+
* frame: a top-level tab that once carried a token must fall back to the
|
|
24
|
+
* regular login, or it could never sign in again.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function isEmbeddedTab(): boolean;
|
|
29
|
+
/** @internal */
|
|
30
|
+
export declare function markEmbeddedTab(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Trades a one-time embed token for an app-user session token. Resolves to
|
|
33
|
+
* `null` when the platform refuses the token; never throws.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl, }: {
|
|
38
|
+
serverUrl: string;
|
|
39
|
+
appId: string;
|
|
40
|
+
ott: string;
|
|
41
|
+
fetchImpl?: typeof fetch;
|
|
42
|
+
}): Promise<string | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Covers the page with a plain "session ended" notice. An embedded session can
|
|
45
|
+
* only be renewed by the host platform, so this stands in for the login
|
|
46
|
+
* redirect, which cannot complete inside a frame. The copy points at the host
|
|
47
|
+
* page: reloading the frame itself carries no token and lands here again.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function showEmbedSessionEnded(): void;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sessions for apps embedded in a host platform.
|
|
3
|
+
*
|
|
4
|
+
* The platform's server mints a one-time token for one of its users and puts
|
|
5
|
+
* it on the iframe URL as `?ott=`. The client takes it off the URL as it is
|
|
6
|
+
* created, trades it for an app-user session through the OAuth token-exchange
|
|
7
|
+
* grant (RFC 8693), and keeps the result in memory only — never in storage — so
|
|
8
|
+
* the session lives and dies with the frame.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
/** The query parameter a host platform puts the one-time token on. @internal */
|
|
13
|
+
export const EMBED_TOKEN_PARAM = "ott";
|
|
14
|
+
const EMBED_TAB_KEY = "base44_embed_session";
|
|
15
|
+
const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
16
|
+
const SUBJECT_TOKEN_TYPE = "urn:base44:params:oauth:token-type:embed-ott";
|
|
17
|
+
const RATE_LIMITED = 429;
|
|
18
|
+
const RATE_LIMIT_RETRY_MS = 1000;
|
|
19
|
+
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
|
+
*/
|
|
26
|
+
export function takeEmbedTokenFromUrl() {
|
|
27
|
+
if (typeof window === "undefined" || !window.location) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
let ott = null;
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(window.location.href);
|
|
33
|
+
ott = url.searchParams.get(EMBED_TOKEN_PARAM);
|
|
34
|
+
if (!ott) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
url.searchParams.delete(EMBED_TOKEN_PARAM);
|
|
38
|
+
window.history.replaceState(window.history.state, "", url.toString());
|
|
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.
|
|
45
|
+
console.error("Error retrieving embed token from URL:", e);
|
|
46
|
+
}
|
|
47
|
+
return ott;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether this tab previously redeemed an embed token. Counts only inside a
|
|
51
|
+
* frame: a top-level tab that once carried a token must fall back to the
|
|
52
|
+
* regular login, or it could never sign in again.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export function isEmbeddedTab() {
|
|
57
|
+
if (typeof window === "undefined") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
// A cross-site frame is third-party storage, which some browsers block
|
|
61
|
+
// outright — every access can throw, and the marker is then unavailable.
|
|
62
|
+
try {
|
|
63
|
+
return (window.self !== window.top &&
|
|
64
|
+
window.sessionStorage.getItem(EMBED_TAB_KEY) === "1");
|
|
65
|
+
}
|
|
66
|
+
catch (_a) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** @internal */
|
|
71
|
+
export function markEmbeddedTab() {
|
|
72
|
+
try {
|
|
73
|
+
window.sessionStorage.setItem(EMBED_TAB_KEY, "1");
|
|
74
|
+
}
|
|
75
|
+
catch (_a) {
|
|
76
|
+
/* storage blocked */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Trades a one-time embed token for an app-user session token. Resolves to
|
|
81
|
+
* `null` when the platform refuses the token; never throws.
|
|
82
|
+
*
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
export async function exchangeEmbedToken({ serverUrl, appId, ott, fetchImpl = fetch, }) {
|
|
86
|
+
const post = () => fetchImpl(`${serverUrl}/api/apps/${appId}/auth/embed/token`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "X-App-Id": String(appId) },
|
|
89
|
+
body: new URLSearchParams({
|
|
90
|
+
grant_type: GRANT_TYPE,
|
|
91
|
+
subject_token: ott,
|
|
92
|
+
subject_token_type: SUBJECT_TOKEN_TYPE,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
let response = await post();
|
|
97
|
+
// The limiter refuses before the token is redeemed, so it is still valid.
|
|
98
|
+
if (response.status === RATE_LIMITED) {
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_RETRY_MS));
|
|
100
|
+
response = await post();
|
|
101
|
+
}
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const { access_token: token } = (await response.json());
|
|
106
|
+
return token || null;
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
console.error("Embed token exchange failed:", e);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Covers the page with a plain "session ended" notice. An embedded session can
|
|
115
|
+
* only be renewed by the host platform, so this stands in for the login
|
|
116
|
+
* redirect, which cannot complete inside a frame. The copy points at the host
|
|
117
|
+
* page: reloading the frame itself carries no token and lands here again.
|
|
118
|
+
*
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export function showEmbedSessionEnded() {
|
|
122
|
+
if (typeof document === "undefined" || !document.body) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (document.getElementById(SESSION_ENDED_ELEMENT_ID)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const overlay = document.createElement("div");
|
|
129
|
+
overlay.id = SESSION_ENDED_ELEMENT_ID;
|
|
130
|
+
overlay.setAttribute("role", "alert");
|
|
131
|
+
overlay.style.cssText =
|
|
132
|
+
"position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;" +
|
|
133
|
+
"justify-content:center;background:#fff;color:#0f172a;" +
|
|
134
|
+
"font-family:ui-sans-serif,system-ui,sans-serif;text-align:center;padding:2rem;";
|
|
135
|
+
const title = document.createElement("h1");
|
|
136
|
+
title.textContent = "Session ended";
|
|
137
|
+
title.style.cssText = "font-size:1.5rem;font-weight:700;margin:0 0 .75rem;";
|
|
138
|
+
const body = document.createElement("p");
|
|
139
|
+
body.textContent = "Reload this page in your browser to start a new session.";
|
|
140
|
+
body.style.cssText = "margin:0;color:#475569;";
|
|
141
|
+
const card = document.createElement("div");
|
|
142
|
+
card.append(title, body);
|
|
143
|
+
overlay.append(card);
|
|
144
|
+
document.body.append(overlay);
|
|
145
|
+
}
|
|
@@ -4,7 +4,8 @@ export interface RoomsSocketConfig {
|
|
|
4
4
|
mountPath: string;
|
|
5
5
|
transports: string[];
|
|
6
6
|
appId: string;
|
|
7
|
-
|
|
7
|
+
/** Asked on every connect, so the socket always carries the current session. */
|
|
8
|
+
getToken: () => string | null;
|
|
8
9
|
}
|
|
9
10
|
export type TSocketRoom = string;
|
|
10
11
|
export type TJsonStr = string;
|
|
@@ -40,7 +41,7 @@ export declare function RoomsSocket({ config }: {
|
|
|
40
41
|
leave: (room: string) => void;
|
|
41
42
|
}>;
|
|
42
43
|
subscribeToRoom: (room: TSocketRoom, handlers: Partial<{ [k in TEvent]: THandler<k>; }>) => () => void;
|
|
43
|
-
|
|
44
|
+
reconnect: () => void;
|
|
44
45
|
updateModel: (room: string, data: any) => Promise<void>;
|
|
45
46
|
disconnect: () => void;
|
|
46
47
|
};
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { io } from "socket.io-client";
|
|
2
|
-
import { getAccessToken } from "./auth-utils.js";
|
|
3
2
|
import { getAnalyticsSessionId } from "../modules/analytics.js";
|
|
4
3
|
const ROOM_LEAVE_GRACE_MS = 250;
|
|
5
4
|
function initializeSocket(config, handlers) {
|
|
6
|
-
var _a;
|
|
7
5
|
// On unauthenticated clients, send a stable anonymous visitor id on the
|
|
8
6
|
// handshake so the backend can verify room access for anonymous agent
|
|
9
7
|
// conversations (mirrors the X-Base44-Anonymous-Id HTTP header). Authenticated
|
|
10
8
|
// clients are identified by their token instead.
|
|
11
|
-
const resolvedToken =
|
|
9
|
+
const resolvedToken = config.getToken();
|
|
12
10
|
const query = {
|
|
13
11
|
app_id: config.appId,
|
|
14
12
|
token: resolvedToken,
|
|
@@ -42,7 +40,6 @@ function initializeSocket(config, handlers) {
|
|
|
42
40
|
return socket;
|
|
43
41
|
}
|
|
44
42
|
export function RoomsSocket({ config }) {
|
|
45
|
-
let currentConfig = { ...config };
|
|
46
43
|
const roomsToListeners = {};
|
|
47
44
|
const pendingRoomLeaves = {};
|
|
48
45
|
const handlers = {
|
|
@@ -84,13 +81,10 @@ export function RoomsSocket({ config }) {
|
|
|
84
81
|
socket.disconnect();
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
|
-
|
|
84
|
+
/** Drops the connection and opens a new one with the current token. */
|
|
85
|
+
function reconnect() {
|
|
88
86
|
cleanup();
|
|
89
|
-
|
|
90
|
-
...currentConfig,
|
|
91
|
-
...config,
|
|
92
|
-
};
|
|
93
|
-
socket = initializeSocket(currentConfig, handlers);
|
|
87
|
+
socket = initializeSocket(config, handlers);
|
|
94
88
|
}
|
|
95
89
|
function joinRoom(room) {
|
|
96
90
|
socket.emit("join", room);
|
|
@@ -163,7 +157,7 @@ export function RoomsSocket({ config }) {
|
|
|
163
157
|
return {
|
|
164
158
|
socket,
|
|
165
159
|
subscribeToRoom,
|
|
166
|
-
|
|
160
|
+
reconnect,
|
|
167
161
|
updateModel,
|
|
168
162
|
disconnect,
|
|
169
163
|
};
|