@mitralab.io/platform-sdk 1.0.8 → 1.1.0-beta.0
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/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +251 -108
- package/dist/index.cjs +1835 -0
- package/dist/index.d.cts +966 -0
- package/dist/index.d.ts +409 -102
- package/dist/index.js +1238 -160
- package/package.json +29 -11
- package/dist/index.d.mts +0 -659
- package/dist/index.mjs +0 -706
package/dist/index.js
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
MitraApiError: () => MitraApiError,
|
|
24
|
-
createClient: () => createClient
|
|
25
|
-
});
|
|
26
|
-
module.exports = __toCommonJS(index_exports);
|
|
1
|
+
// src/utils/url.ts
|
|
2
|
+
function stripTrailingSlashes(value) {
|
|
3
|
+
let end = value.length;
|
|
4
|
+
while (end > 0 && value.codePointAt(end - 1) === 47) end -= 1;
|
|
5
|
+
return value.slice(0, end);
|
|
6
|
+
}
|
|
27
7
|
|
|
28
8
|
// src/client.ts
|
|
29
|
-
|
|
9
|
+
import {
|
|
10
|
+
createPublicFunctionsModule,
|
|
11
|
+
encodePathSegment,
|
|
12
|
+
expectObject as expectObject2
|
|
13
|
+
} from "@mitralab.io/sdk-core";
|
|
30
14
|
|
|
31
15
|
// src/utils/http-client.ts
|
|
32
16
|
var bearerCredentialPattern = /(Bearer\s+)\S+/gi;
|
|
17
|
+
var sensitiveDetailKeyPattern = /token|authorization|password|secret|api.?key|credential|private.?key/i;
|
|
18
|
+
function isSensitiveDetailKey(key) {
|
|
19
|
+
return sensitiveDetailKeyPattern.test(key);
|
|
20
|
+
}
|
|
33
21
|
function redactText(value, currentToken) {
|
|
34
22
|
const withoutBearerCredentials = value.replace(bearerCredentialPattern, "$1[REDACTED]");
|
|
35
23
|
return currentToken ? withoutBearerCredentials.split(currentToken).join("[REDACTED]") : withoutBearerCredentials;
|
|
@@ -39,10 +27,13 @@ function redactDetails(value, currentToken) {
|
|
|
39
27
|
if (Array.isArray(value)) return value.map((item) => redactDetails(item, currentToken));
|
|
40
28
|
if (value && typeof value === "object") {
|
|
41
29
|
return Object.fromEntries(
|
|
42
|
-
Object.entries(value).map(([key, entry]) =>
|
|
43
|
-
redactText(key, currentToken)
|
|
44
|
-
|
|
45
|
-
|
|
30
|
+
Object.entries(value).map(([key, entry]) => {
|
|
31
|
+
const redactedKey = redactText(key, currentToken);
|
|
32
|
+
return [
|
|
33
|
+
redactedKey,
|
|
34
|
+
isSensitiveDetailKey(redactedKey) ? "[REDACTED]" : redactDetails(entry, currentToken)
|
|
35
|
+
];
|
|
36
|
+
})
|
|
46
37
|
);
|
|
47
38
|
}
|
|
48
39
|
return value;
|
|
@@ -54,15 +45,27 @@ function asErrorPayload(value) {
|
|
|
54
45
|
function optionalString(value) {
|
|
55
46
|
return typeof value === "string" ? value : void 0;
|
|
56
47
|
}
|
|
48
|
+
function buildRequestUrl(baseUrl, path, params) {
|
|
49
|
+
const url = `${baseUrl}${path}`;
|
|
50
|
+
if (!params) return url;
|
|
51
|
+
const searchParams = new URLSearchParams();
|
|
52
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
53
|
+
if (value !== void 0) searchParams.append(key, String(value));
|
|
54
|
+
});
|
|
55
|
+
const queryString = searchParams.toString();
|
|
56
|
+
return queryString ? `${url}?${queryString}` : url;
|
|
57
|
+
}
|
|
57
58
|
var HttpClient = class {
|
|
58
59
|
baseUrl;
|
|
59
60
|
tokenGetter;
|
|
61
|
+
beforeAuthenticatedRequest;
|
|
60
62
|
onUnauthorized;
|
|
61
63
|
onError;
|
|
62
64
|
defaultHeaders;
|
|
63
65
|
constructor(config) {
|
|
64
66
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
65
67
|
this.tokenGetter = config.getToken ?? (() => null);
|
|
68
|
+
this.beforeAuthenticatedRequest = config.beforeAuthenticatedRequest;
|
|
66
69
|
this.onUnauthorized = config.onUnauthorized;
|
|
67
70
|
this.onError = config.onError;
|
|
68
71
|
this.defaultHeaders = config.defaultHeaders ?? {};
|
|
@@ -92,18 +95,9 @@ var HttpClient = class {
|
|
|
92
95
|
*/
|
|
93
96
|
async request(path, options = {}) {
|
|
94
97
|
const { method = "GET", body, headers = {}, params, isRetry } = options;
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
Object.entries(params).forEach(([key, value]) => {
|
|
99
|
-
if (value !== void 0) {
|
|
100
|
-
searchParams.append(key, String(value));
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
const queryString = searchParams.toString();
|
|
104
|
-
if (queryString) {
|
|
105
|
-
url += `?${queryString}`;
|
|
106
|
-
}
|
|
98
|
+
const url = buildRequestUrl(this.baseUrl, path, params);
|
|
99
|
+
if (!isRetry && this.tokenGetter() && this.beforeAuthenticatedRequest) {
|
|
100
|
+
await this.beforeAuthenticatedRequest();
|
|
107
101
|
}
|
|
108
102
|
const requestHeaders = {
|
|
109
103
|
"Content-Type": "application/json",
|
|
@@ -131,27 +125,50 @@ var HttpClient = class {
|
|
|
131
125
|
}
|
|
132
126
|
if (!response.ok) {
|
|
133
127
|
if (response.status === 401 && !isRetry && this.onUnauthorized) {
|
|
134
|
-
const refreshed = await this.onUnauthorized();
|
|
128
|
+
const refreshed = await this.onUnauthorized(token);
|
|
135
129
|
if (refreshed) {
|
|
136
130
|
return this.request(path, { ...options, isRetry: true });
|
|
137
131
|
}
|
|
138
132
|
}
|
|
139
|
-
const
|
|
140
|
-
const errorPayload = asErrorPayload(errorBody);
|
|
141
|
-
const rawMessage = optionalString(errorPayload.message);
|
|
142
|
-
const rawCode = optionalString(errorPayload.error_code);
|
|
143
|
-
const error = new MitraApiError(
|
|
144
|
-
redactText(rawMessage || `Request failed with status ${response.status}`, token),
|
|
145
|
-
response.status,
|
|
146
|
-
rawCode === void 0 ? void 0 : redactText(rawCode, token),
|
|
147
|
-
redactDetails(errorBody, token)
|
|
148
|
-
);
|
|
133
|
+
const error = await this.errorFromResponse(response, token);
|
|
149
134
|
this.onError?.(error);
|
|
150
135
|
throw error;
|
|
151
136
|
}
|
|
137
|
+
return this.parseResponseBody(response, path);
|
|
138
|
+
}
|
|
139
|
+
async errorFromResponse(response, token) {
|
|
140
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
141
|
+
const errorPayload = asErrorPayload(errorBody);
|
|
142
|
+
const rawMessage = optionalString(errorPayload.message);
|
|
143
|
+
const rawCode = optionalString(errorPayload.error_code);
|
|
144
|
+
return new MitraApiError(
|
|
145
|
+
redactText(rawMessage || `Request failed with status ${response.status}`, token),
|
|
146
|
+
response.status,
|
|
147
|
+
rawCode === void 0 ? void 0 : redactText(rawCode, token),
|
|
148
|
+
redactDetails(errorBody, token)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
async parseResponseBody(response, path) {
|
|
152
152
|
if (response.status === 204) {
|
|
153
153
|
return void 0;
|
|
154
154
|
}
|
|
155
|
+
if (typeof response.text === "function") {
|
|
156
|
+
const responseText = await response.text();
|
|
157
|
+
if (responseText.length === 0) {
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(responseText);
|
|
162
|
+
} catch {
|
|
163
|
+
const error = new MitraApiError(
|
|
164
|
+
`Response from ${path} is not valid JSON`,
|
|
165
|
+
response.status,
|
|
166
|
+
"INVALID_RESPONSE"
|
|
167
|
+
);
|
|
168
|
+
this.onError?.(error);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
155
172
|
return response.json();
|
|
156
173
|
}
|
|
157
174
|
/**
|
|
@@ -231,26 +248,447 @@ var coreErrors = {
|
|
|
231
248
|
invalidResponse: (message) => new MitraApiError(message, 200, "INVALID_RESPONSE")
|
|
232
249
|
};
|
|
233
250
|
|
|
251
|
+
// src/legacy/bridge.ts
|
|
252
|
+
import { configureSdkMitra, getConfig } from "mitra-interactions-sdk";
|
|
253
|
+
|
|
254
|
+
// src/modules/auth-page-url.ts
|
|
255
|
+
function resolveAuthPageUrl(apiUrl, configuredAuthPageUrl, browserWindow = globalThis.window) {
|
|
256
|
+
const injectedAuthPageUrl = browserWindow?.__mitraEnv?.authPageUrl;
|
|
257
|
+
try {
|
|
258
|
+
const candidate = configuredAuthPageUrl ?? (typeof injectedAuthPageUrl === "string" && injectedAuthPageUrl.trim() ? injectedAuthPageUrl : new URL("/sdk-auth.html", apiUrl).toString());
|
|
259
|
+
const url = new URL(candidate);
|
|
260
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
261
|
+
throw new Error("authPageUrl must be an absolute HTTP or HTTPS URL.");
|
|
262
|
+
return url;
|
|
263
|
+
} catch {
|
|
264
|
+
throw new Error("authPageUrl must be an absolute HTTP or HTTPS URL.");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/legacy/bridge.ts
|
|
269
|
+
function stripBearer(token) {
|
|
270
|
+
return token.replace(/^\s*(?:bearer\s+)+/i, "").trim();
|
|
271
|
+
}
|
|
272
|
+
var LegacySessionBridge = class {
|
|
273
|
+
auth;
|
|
274
|
+
appId;
|
|
275
|
+
apiUrl;
|
|
276
|
+
configuredAuthPageUrl;
|
|
277
|
+
legacyUrl;
|
|
278
|
+
unsubscribeFromAuth = null;
|
|
279
|
+
constructor(auth, appId, apiUrl, authPageUrl) {
|
|
280
|
+
this.auth = auth;
|
|
281
|
+
this.appId = appId;
|
|
282
|
+
this.apiUrl = apiUrl;
|
|
283
|
+
this.configuredAuthPageUrl = authPageUrl;
|
|
284
|
+
const gatewayUrl = stripTrailingSlashes(apiUrl);
|
|
285
|
+
this.legacyUrl = gatewayUrl ? `${gatewayUrl}/legacy` : "";
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Configures the legacy SDK and hands it the session this SDK already holds.
|
|
289
|
+
*
|
|
290
|
+
* Called once per client. Failures are swallowed: an unusable legacy SDK must
|
|
291
|
+
* not stop the new client from being created.
|
|
292
|
+
*/
|
|
293
|
+
connect() {
|
|
294
|
+
this.disconnect();
|
|
295
|
+
this.syncToLegacy(this.auth.readSessionTokens());
|
|
296
|
+
this.unsubscribeFromAuth = this.auth.onSessionChange((session) => {
|
|
297
|
+
this.syncToLegacy(session);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/** Stops this bridge from changing the process-wide legacy SDK singleton. */
|
|
301
|
+
disconnect() {
|
|
302
|
+
this.unsubscribeFromAuth?.();
|
|
303
|
+
this.unsubscribeFromAuth = null;
|
|
304
|
+
}
|
|
305
|
+
syncToLegacy(session) {
|
|
306
|
+
try {
|
|
307
|
+
const authPageUrl = resolveAuthPageUrl(
|
|
308
|
+
this.apiUrl,
|
|
309
|
+
this.configuredAuthPageUrl
|
|
310
|
+
).toString();
|
|
311
|
+
configureSdkMitra({
|
|
312
|
+
baseURL: this.legacyUrl,
|
|
313
|
+
authUrl: this.legacyUrl,
|
|
314
|
+
authPageUrl,
|
|
315
|
+
projectId: this.appId,
|
|
316
|
+
onTokenRefresh: (session2) => this.adopt(session2),
|
|
317
|
+
...session.token ? { token: session.token } : {},
|
|
318
|
+
...session.refreshToken ? { refreshToken: session.refreshToken } : {}
|
|
319
|
+
});
|
|
320
|
+
const legacyConfig = getConfig();
|
|
321
|
+
if (!session.token) delete legacyConfig.token;
|
|
322
|
+
if (!session.refreshToken) delete legacyConfig.refreshToken;
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Stores a session produced by the legacy SDK. The AuthModule subscription
|
|
328
|
+
* then writes that session back to the process-wide legacy config and
|
|
329
|
+
* reinstates the refresh hook removed by legacy login.
|
|
330
|
+
*/
|
|
331
|
+
adopt(session) {
|
|
332
|
+
const adopted = this.auth.adoptSession({
|
|
333
|
+
token: stripBearer(session.token),
|
|
334
|
+
refreshToken: session.refreshToken ?? null
|
|
335
|
+
});
|
|
336
|
+
if (!adopted) this.syncToLegacy(this.auth.readSessionTokens());
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
var activeBridge = null;
|
|
340
|
+
function setActiveBridge(bridge) {
|
|
341
|
+
activeBridge?.disconnect();
|
|
342
|
+
activeBridge = bridge;
|
|
343
|
+
}
|
|
344
|
+
function adoptLegacySession(session) {
|
|
345
|
+
activeBridge?.adopt(session);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/modules/auth.ts
|
|
349
|
+
import { createAuthModule } from "@mitralab.io/sdk-core";
|
|
350
|
+
|
|
351
|
+
// src/modules/google-auth.ts
|
|
352
|
+
import { expectObject } from "@mitralab.io/sdk-core";
|
|
353
|
+
var RESULT_TYPE = "mitra-oauth-result";
|
|
354
|
+
var PROVIDER_LABELS = {
|
|
355
|
+
google: "Google",
|
|
356
|
+
microsoft: "Microsoft"
|
|
357
|
+
};
|
|
358
|
+
var POPUP_WIDTH = 480;
|
|
359
|
+
var POPUP_HEIGHT = 600;
|
|
360
|
+
var POPUP_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
361
|
+
var POPUP_CLOSED_POLL_MS = 500;
|
|
362
|
+
function expectAuthTokenResponse(value) {
|
|
363
|
+
const response = expectObject(
|
|
364
|
+
value,
|
|
365
|
+
"Authentication token response",
|
|
366
|
+
coreErrors
|
|
367
|
+
);
|
|
368
|
+
for (const field of ["accessToken", "refreshToken", "tokenType"]) {
|
|
369
|
+
if (typeof response[field] !== "string" || !response[field].trim()) {
|
|
370
|
+
throw coreErrors.invalidResponse(
|
|
371
|
+
`Authentication token response has an invalid ${field} field`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
accessToken: response.accessToken,
|
|
377
|
+
refreshToken: response.refreshToken,
|
|
378
|
+
tokenType: response.tokenType
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
var GoogleAuthFlow = class {
|
|
382
|
+
appId;
|
|
383
|
+
apiUrl;
|
|
384
|
+
configuredAuthPageUrl;
|
|
385
|
+
client;
|
|
386
|
+
provider;
|
|
387
|
+
providerLabel;
|
|
388
|
+
redirectStorageKey;
|
|
389
|
+
popupPromise = null;
|
|
390
|
+
constructor(config) {
|
|
391
|
+
this.appId = config.appId;
|
|
392
|
+
this.apiUrl = stripTrailingSlashes(config.apiUrl);
|
|
393
|
+
this.configuredAuthPageUrl = config.authPageUrl;
|
|
394
|
+
this.client = config.client;
|
|
395
|
+
this.provider = config.provider ?? "google";
|
|
396
|
+
this.providerLabel = PROVIDER_LABELS[this.provider];
|
|
397
|
+
this.redirectStorageKey = `mitra_${this.provider}_redirect_${config.appId}`;
|
|
398
|
+
}
|
|
399
|
+
signIn(options = {}) {
|
|
400
|
+
const browserWindow = this.requireBrowser();
|
|
401
|
+
if (options.mode === "redirect") {
|
|
402
|
+
return this.startRedirect(browserWindow);
|
|
403
|
+
}
|
|
404
|
+
if (options.mode !== void 0 && options.mode !== "popup") {
|
|
405
|
+
return Promise.reject(new Error(`Unsupported ${this.providerLabel} sign-in mode: ${String(options.mode)}`));
|
|
406
|
+
}
|
|
407
|
+
if (this.popupPromise) return this.popupPromise;
|
|
408
|
+
this.popupPromise = this.startPopup(browserWindow).finally(() => {
|
|
409
|
+
this.popupPromise = null;
|
|
410
|
+
});
|
|
411
|
+
return this.popupPromise;
|
|
412
|
+
}
|
|
413
|
+
async completeRedirect() {
|
|
414
|
+
const browserWindow = this.requireBrowser();
|
|
415
|
+
const params = new URLSearchParams(browserWindow.location.hash.replace(/^#/, ""));
|
|
416
|
+
const code = params.get("codeMitra");
|
|
417
|
+
const state = params.get("stateMitra");
|
|
418
|
+
const error = params.get("errorMitra");
|
|
419
|
+
if (code === null && state === null && error === null) return null;
|
|
420
|
+
const context = this.readRedirectContext(browserWindow);
|
|
421
|
+
if (!state?.trim()) {
|
|
422
|
+
throw new Error(`${this.providerLabel} sign-in redirect is missing state.`);
|
|
423
|
+
}
|
|
424
|
+
if (context?.state !== state) {
|
|
425
|
+
throw new Error(`Invalid ${this.providerLabel} sign-in state (possible CSRF).`);
|
|
426
|
+
}
|
|
427
|
+
const expectedRedirectUri = this.getRedirectUri(
|
|
428
|
+
resolveAuthPageUrl(this.apiUrl, this.configuredAuthPageUrl, browserWindow)
|
|
429
|
+
);
|
|
430
|
+
if (context.redirectUri !== expectedRedirectUri) {
|
|
431
|
+
throw new Error(`${this.providerLabel} sign-in redirect context is invalid.`);
|
|
432
|
+
}
|
|
433
|
+
this.cleanRedirectFragment(browserWindow);
|
|
434
|
+
this.clearRedirectContext(browserWindow);
|
|
435
|
+
if (code === "error" || error !== null) {
|
|
436
|
+
throw new Error(error || `${this.providerLabel} sign-in failed.`);
|
|
437
|
+
}
|
|
438
|
+
if (!code?.trim()) {
|
|
439
|
+
throw new Error(`${this.providerLabel} sign-in redirect is missing code.`);
|
|
440
|
+
}
|
|
441
|
+
return this.exchangeCode(code, context.redirectUri);
|
|
442
|
+
}
|
|
443
|
+
async startPopup(browserWindow) {
|
|
444
|
+
const state = this.generateState();
|
|
445
|
+
const authPageUrl = resolveAuthPageUrl(
|
|
446
|
+
this.apiUrl,
|
|
447
|
+
this.configuredAuthPageUrl,
|
|
448
|
+
browserWindow
|
|
449
|
+
);
|
|
450
|
+
const popup = this.openPopup(browserWindow, this.buildStartUrl(browserWindow, authPageUrl, state));
|
|
451
|
+
const result = await this.waitForPopupResult(browserWindow, popup, authPageUrl.origin, state);
|
|
452
|
+
if (result.code) return this.exchangeCode(result.code, this.getRedirectUri(authPageUrl));
|
|
453
|
+
return expectAuthTokenResponse(result.token);
|
|
454
|
+
}
|
|
455
|
+
startRedirect(browserWindow) {
|
|
456
|
+
const state = this.generateState();
|
|
457
|
+
const authPageUrl = resolveAuthPageUrl(
|
|
458
|
+
this.apiUrl,
|
|
459
|
+
this.configuredAuthPageUrl,
|
|
460
|
+
browserWindow
|
|
461
|
+
);
|
|
462
|
+
const context = {
|
|
463
|
+
state,
|
|
464
|
+
redirectUri: this.getRedirectUri(authPageUrl)
|
|
465
|
+
};
|
|
466
|
+
this.persistRedirectContext(browserWindow, context);
|
|
467
|
+
const startUrl = this.buildStartUrl(browserWindow, authPageUrl, state);
|
|
468
|
+
browserWindow.location.assign(startUrl.toString());
|
|
469
|
+
return new Promise(() => void 0);
|
|
470
|
+
}
|
|
471
|
+
async exchangeCode(code, redirectUri) {
|
|
472
|
+
const response = await this.client.post(`/api/v1/auth/${this.provider}`, {
|
|
473
|
+
appId: this.appId,
|
|
474
|
+
code,
|
|
475
|
+
redirectUri
|
|
476
|
+
});
|
|
477
|
+
return expectAuthTokenResponse(response);
|
|
478
|
+
}
|
|
479
|
+
buildStartUrl(browserWindow, authPageUrl, state) {
|
|
480
|
+
const startUrl = new URL(authPageUrl);
|
|
481
|
+
startUrl.searchParams.set("provider", this.provider);
|
|
482
|
+
startUrl.searchParams.set("state", state);
|
|
483
|
+
startUrl.searchParams.set("appId", this.appId);
|
|
484
|
+
startUrl.searchParams.set("apiUrl", this.apiUrl);
|
|
485
|
+
startUrl.searchParams.set("origin", browserWindow.location.origin);
|
|
486
|
+
startUrl.searchParams.set("responseType", "code");
|
|
487
|
+
return startUrl;
|
|
488
|
+
}
|
|
489
|
+
getRedirectUri(authPageUrl) {
|
|
490
|
+
return `${authPageUrl.origin}${authPageUrl.pathname}`;
|
|
491
|
+
}
|
|
492
|
+
generateState() {
|
|
493
|
+
if (!globalThis.crypto?.getRandomValues) {
|
|
494
|
+
throw new Error(`${this.providerLabel} sign-in requires crypto.getRandomValues.`);
|
|
495
|
+
}
|
|
496
|
+
const bytes = new Uint8Array(16);
|
|
497
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
498
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
499
|
+
}
|
|
500
|
+
openPopup(browserWindow, url) {
|
|
501
|
+
const outerWidth = browserWindow.outerWidth || browserWindow.screen.width;
|
|
502
|
+
const outerHeight = browserWindow.outerHeight || browserWindow.screen.height;
|
|
503
|
+
const left = Math.max(0, (browserWindow.screenX || 0) + (outerWidth - POPUP_WIDTH) / 2);
|
|
504
|
+
const top = Math.max(0, (browserWindow.screenY || 0) + (outerHeight - POPUP_HEIGHT) / 2);
|
|
505
|
+
const popup = browserWindow.open(
|
|
506
|
+
url.toString(),
|
|
507
|
+
"mitra-google-oauth",
|
|
508
|
+
`width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},menubar=no,toolbar=no,status=no`
|
|
509
|
+
);
|
|
510
|
+
if (!popup) {
|
|
511
|
+
throw new Error(`${this.providerLabel} sign-in popup was blocked by the browser.`);
|
|
512
|
+
}
|
|
513
|
+
return popup;
|
|
514
|
+
}
|
|
515
|
+
waitForPopupResult(browserWindow, popup, expectedOrigin, expectedState) {
|
|
516
|
+
return new Promise((resolve, reject) => {
|
|
517
|
+
const timeout = globalThis.setTimeout(() => {
|
|
518
|
+
cleanup();
|
|
519
|
+
reject(new Error(`${this.providerLabel} sign-in timed out.`));
|
|
520
|
+
}, POPUP_TIMEOUT_MS);
|
|
521
|
+
const closedPoll = globalThis.setInterval(() => {
|
|
522
|
+
if (popup.closed) {
|
|
523
|
+
cleanup();
|
|
524
|
+
reject(new Error(`${this.providerLabel} sign-in was cancelled.`));
|
|
525
|
+
}
|
|
526
|
+
}, POPUP_CLOSED_POLL_MS);
|
|
527
|
+
const onMessage = (event) => {
|
|
528
|
+
if (event.origin !== expectedOrigin || event.source !== popup) return;
|
|
529
|
+
if (!event.data || typeof event.data !== "object") return;
|
|
530
|
+
const data = event.data;
|
|
531
|
+
if (data.type !== RESULT_TYPE) return;
|
|
532
|
+
if (data.state !== expectedState) {
|
|
533
|
+
cleanup();
|
|
534
|
+
reject(new Error(`Invalid ${this.providerLabel} sign-in state (possible CSRF).`));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (data.success !== true) {
|
|
538
|
+
cleanup();
|
|
539
|
+
reject(new Error(typeof data.error === "string" && data.error.trim() ? data.error : `${this.providerLabel} sign-in failed.`));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const code = typeof data.code === "string" && data.code.trim() ? data.code : void 0;
|
|
543
|
+
if (!code && data.token === void 0) {
|
|
544
|
+
cleanup();
|
|
545
|
+
reject(new Error("Google auth page returned neither code nor token."));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
cleanup();
|
|
549
|
+
resolve({ ...code ? { code } : {}, ...data.token !== void 0 ? { token: data.token } : {} });
|
|
550
|
+
};
|
|
551
|
+
const cleanup = () => {
|
|
552
|
+
globalThis.clearTimeout(timeout);
|
|
553
|
+
globalThis.clearInterval(closedPoll);
|
|
554
|
+
browserWindow.removeEventListener("message", onMessage);
|
|
555
|
+
if (!popup.closed) popup.close();
|
|
556
|
+
};
|
|
557
|
+
browserWindow.addEventListener("message", onMessage);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
persistRedirectContext(browserWindow, context) {
|
|
561
|
+
try {
|
|
562
|
+
browserWindow.sessionStorage.setItem(this.redirectStorageKey, JSON.stringify(context));
|
|
563
|
+
} catch {
|
|
564
|
+
throw new Error(`${this.providerLabel} sign-in redirect requires sessionStorage.`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
readRedirectContext(browserWindow) {
|
|
568
|
+
try {
|
|
569
|
+
const raw = browserWindow.sessionStorage.getItem(this.redirectStorageKey);
|
|
570
|
+
if (!raw) return null;
|
|
571
|
+
const value = JSON.parse(raw);
|
|
572
|
+
if (typeof value.state !== "string" || typeof value.redirectUri !== "string") return null;
|
|
573
|
+
return {
|
|
574
|
+
state: value.state,
|
|
575
|
+
redirectUri: value.redirectUri
|
|
576
|
+
};
|
|
577
|
+
} catch {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
clearRedirectContext(browserWindow) {
|
|
582
|
+
try {
|
|
583
|
+
browserWindow.sessionStorage.removeItem(this.redirectStorageKey);
|
|
584
|
+
} catch {
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
cleanRedirectFragment(browserWindow) {
|
|
588
|
+
browserWindow.history.replaceState(
|
|
589
|
+
{},
|
|
590
|
+
"",
|
|
591
|
+
`${browserWindow.location.pathname}${browserWindow.location.search}`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
requireBrowser() {
|
|
595
|
+
if (globalThis.window === void 0) {
|
|
596
|
+
throw new Error(`${this.providerLabel} sign-in is only available in a browser.`);
|
|
597
|
+
}
|
|
598
|
+
return globalThis.window;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
234
602
|
// src/modules/auth.ts
|
|
235
|
-
var
|
|
603
|
+
var DEFAULT_TOKEN_VALIDITY_MS = 3e4;
|
|
604
|
+
var sessionPorts = /* @__PURE__ */ new WeakMap();
|
|
605
|
+
function getAuthSessionPort(auth) {
|
|
606
|
+
const port = sessionPorts.get(auth);
|
|
607
|
+
if (!port) throw new Error("Auth session port is unavailable.");
|
|
608
|
+
return port;
|
|
609
|
+
}
|
|
610
|
+
function decodeJwtPayload(token) {
|
|
611
|
+
try {
|
|
612
|
+
const part = token.replace(/^Bearer\s+/i, "").split(".")[1];
|
|
613
|
+
if (!part || typeof globalThis.atob !== "function") return null;
|
|
614
|
+
const base64 = part.replaceAll("-", "+").replaceAll("_", "/");
|
|
615
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
616
|
+
const value = JSON.parse(globalThis.atob(padded));
|
|
617
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
618
|
+
return value;
|
|
619
|
+
} catch {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function belongsToApp(token, appId) {
|
|
624
|
+
const payload = decodeJwtPayload(token);
|
|
625
|
+
if (!payload) return true;
|
|
626
|
+
return payload.app_id === appId;
|
|
627
|
+
}
|
|
628
|
+
function isTokenExpiring(token, minValidityMs) {
|
|
629
|
+
const exp = decodeJwtPayload(token)?.exp;
|
|
630
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return false;
|
|
631
|
+
return exp * 1e3 - Date.now() < minValidityMs;
|
|
632
|
+
}
|
|
633
|
+
function isDefinitiveRefreshFailure(error) {
|
|
634
|
+
return error instanceof MitraApiError && error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429;
|
|
635
|
+
}
|
|
236
636
|
var AuthModule = class {
|
|
237
637
|
appId;
|
|
238
638
|
_currentUser = null;
|
|
239
639
|
#accessToken = null;
|
|
240
640
|
#refreshToken = null;
|
|
241
|
-
|
|
641
|
+
sessionGeneration = 0;
|
|
642
|
+
refreshFlight = null;
|
|
643
|
+
transientRefreshFailureGeneration = null;
|
|
242
644
|
listeners = /* @__PURE__ */ new Set();
|
|
645
|
+
sessionListeners = /* @__PURE__ */ new Set();
|
|
243
646
|
storageKey;
|
|
244
647
|
publicClient;
|
|
245
648
|
authedClient;
|
|
246
649
|
currentUserApi;
|
|
247
|
-
|
|
650
|
+
googleAuth;
|
|
651
|
+
microsoftAuth;
|
|
652
|
+
constructor(appId, iamBaseUrl, options = {}) {
|
|
248
653
|
this.appId = appId;
|
|
654
|
+
const trimmedIamBaseUrl = stripTrailingSlashes(iamBaseUrl);
|
|
655
|
+
const apiUrl = stripTrailingSlashes(
|
|
656
|
+
options.apiUrl ?? (trimmedIamBaseUrl.endsWith("/iam") ? trimmedIamBaseUrl.slice(0, -"/iam".length) : trimmedIamBaseUrl)
|
|
657
|
+
);
|
|
249
658
|
this.storageKey = `mitra_auth_${appId}`;
|
|
250
659
|
this.publicClient = new HttpClient({ baseUrl: iamBaseUrl, getToken: () => null });
|
|
251
|
-
this.authedClient = new HttpClient({
|
|
252
|
-
|
|
660
|
+
this.authedClient = new HttpClient({
|
|
661
|
+
baseUrl: iamBaseUrl,
|
|
662
|
+
getToken: () => this.#accessToken,
|
|
663
|
+
beforeAuthenticatedRequest: () => this.ensureFreshSession().then(() => void 0),
|
|
664
|
+
onUnauthorized: (requestToken) => this.handleUnauthorized(requestToken)
|
|
665
|
+
});
|
|
666
|
+
this.currentUserApi = createAuthModule(this.authedClient, coreErrors);
|
|
667
|
+
this.googleAuth = new GoogleAuthFlow({
|
|
668
|
+
appId,
|
|
669
|
+
apiUrl,
|
|
670
|
+
authPageUrl: options.authPageUrl,
|
|
671
|
+
client: this.publicClient
|
|
672
|
+
});
|
|
673
|
+
this.microsoftAuth = new GoogleAuthFlow({
|
|
674
|
+
appId,
|
|
675
|
+
apiUrl,
|
|
676
|
+
authPageUrl: options.authPageUrl,
|
|
677
|
+
client: this.publicClient,
|
|
678
|
+
provider: "microsoft"
|
|
679
|
+
});
|
|
253
680
|
this.loadFromStorage();
|
|
681
|
+
const readAccessToken = () => this.#accessToken;
|
|
682
|
+
sessionPorts.set(this, {
|
|
683
|
+
get accessToken() {
|
|
684
|
+
return readAccessToken();
|
|
685
|
+
},
|
|
686
|
+
ensureFreshSession: (minValidityMs) => this.ensureFreshSession(minValidityMs),
|
|
687
|
+
handleUnauthorized: (requestToken) => this.handleUnauthorized(requestToken),
|
|
688
|
+
readSessionTokens: () => this.readSessionTokens(),
|
|
689
|
+
onSessionChange: (callback) => this.onSessionChange(callback),
|
|
690
|
+
adoptSession: (session) => this.adoptSession(session)
|
|
691
|
+
});
|
|
254
692
|
}
|
|
255
693
|
/** The currently authenticated user, or null. */
|
|
256
694
|
get currentUser() {
|
|
@@ -264,57 +702,94 @@ var AuthModule = class {
|
|
|
264
702
|
get isAuthenticated() {
|
|
265
703
|
return this._currentUser !== null && this.#accessToken !== null;
|
|
266
704
|
}
|
|
705
|
+
/** @deprecated Email/password authentication is not implemented by IAM. Use Google or Microsoft SSO. */
|
|
706
|
+
async signIn(_credentials) {
|
|
707
|
+
throw new MitraApiError(
|
|
708
|
+
"Email/password authentication is not available. Use signInWithGoogle() or signInWithMicrosoft().",
|
|
709
|
+
0,
|
|
710
|
+
"UNSUPPORTED_AUTH_METHOD"
|
|
711
|
+
);
|
|
712
|
+
}
|
|
267
713
|
/**
|
|
268
|
-
* Signs in
|
|
714
|
+
* Signs in with Google SSO.
|
|
715
|
+
*
|
|
716
|
+
* Popup mode is used by default. Redirect mode stores a one-time CSRF context
|
|
717
|
+
* in `sessionStorage` and navigates to the configured `sdk-auth.html` page.
|
|
718
|
+
* Call {@link completeGoogleSignInRedirect} during application startup to
|
|
719
|
+
* finish a redirect response.
|
|
269
720
|
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
721
|
+
* The auth page is resolved from `createClient({ authPageUrl })`, then
|
|
722
|
+
* `window.__mitraEnv.authPageUrl`, and finally `/sdk-auth.html` on the API
|
|
723
|
+
* gateway origin.
|
|
272
724
|
*
|
|
273
|
-
* @param
|
|
274
|
-
* @returns The authenticated user.
|
|
275
|
-
* @throws {MitraApiError}
|
|
725
|
+
* @param options - Popup or redirect mode.
|
|
726
|
+
* @returns The authenticated and hydrated user in popup mode.
|
|
727
|
+
* @throws {MitraApiError} When IAM rejects the authorization code.
|
|
728
|
+
* @throws {Error} When the browser blocks or cancels the popup, the flow times
|
|
729
|
+
* out, or the OAuth response fails origin, source, state, or shape validation.
|
|
276
730
|
*
|
|
277
731
|
* @example
|
|
278
732
|
* ```typescript
|
|
279
|
-
* const user = await mitra.auth.
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
733
|
+
* const user = await mitra.auth.signInWithGoogle();
|
|
734
|
+
* ```
|
|
735
|
+
*
|
|
736
|
+
* @example
|
|
737
|
+
* ```typescript
|
|
738
|
+
* await mitra.auth.signInWithGoogle({ mode: 'redirect' });
|
|
283
739
|
* ```
|
|
284
740
|
*/
|
|
285
|
-
async
|
|
286
|
-
|
|
287
|
-
"/api/v1/auth/login",
|
|
288
|
-
{ ...credentials, appId: this.appId }
|
|
289
|
-
);
|
|
290
|
-
this.#accessToken = tokenResponse.accessToken;
|
|
291
|
-
this.#refreshToken = tokenResponse.refreshToken;
|
|
292
|
-
const user = await this.getCurrentUser();
|
|
293
|
-
this.setAuthState(user, tokenResponse.accessToken, tokenResponse.refreshToken);
|
|
294
|
-
return user;
|
|
741
|
+
async signInWithGoogle(options = {}) {
|
|
742
|
+
return this.establishSession(await this.googleAuth.signIn(options));
|
|
295
743
|
}
|
|
296
744
|
/**
|
|
297
|
-
*
|
|
745
|
+
* Completes a Google redirect response from `#codeMitra` and `#stateMitra`.
|
|
746
|
+
*
|
|
747
|
+
* The method consumes and clears the fragment and stored CSRF context, sends
|
|
748
|
+
* the single-use code directly to IAM, persists both tokens, calls `auth.me()`,
|
|
749
|
+
* and notifies auth-state listeners. It returns `null` when the current URL is
|
|
750
|
+
* not a Google SSO redirect. Redirect errors must carry the same `stateMitra`
|
|
751
|
+
* stored at the start of the flow before their message is exposed or consumed.
|
|
298
752
|
*
|
|
299
|
-
* @
|
|
300
|
-
* @returns The newly created and authenticated user.
|
|
301
|
-
* @throws {MitraApiError} On duplicate email (409) or validation error (400).
|
|
753
|
+
* @returns The authenticated user, or `null` when no redirect result is present.
|
|
302
754
|
*
|
|
303
755
|
* @example
|
|
304
756
|
* ```typescript
|
|
305
|
-
* const
|
|
306
|
-
*
|
|
307
|
-
* password: 'securepassword',
|
|
308
|
-
* name: 'Jane Doe',
|
|
309
|
-
* });
|
|
757
|
+
* const redirectedUser = await mitra.auth.completeGoogleSignInRedirect();
|
|
758
|
+
* if (redirectedUser) console.log(redirectedUser.email);
|
|
310
759
|
* ```
|
|
311
760
|
*/
|
|
312
|
-
async
|
|
313
|
-
await this.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
761
|
+
async completeGoogleSignInRedirect() {
|
|
762
|
+
const tokenResponse = await this.googleAuth.completeRedirect();
|
|
763
|
+
return tokenResponse ? this.establishSession(tokenResponse) : null;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Signs in with Microsoft SSO: the same auth-page handshake as Google, exchanged
|
|
767
|
+
* at IAM's `/auth/microsoft`. Popup by default; redirect mode navigates away.
|
|
768
|
+
*
|
|
769
|
+
* @example
|
|
770
|
+
* ```typescript
|
|
771
|
+
* const user = await mitra.auth.signInWithMicrosoft();
|
|
772
|
+
* ```
|
|
773
|
+
*/
|
|
774
|
+
async signInWithMicrosoft(options = {}) {
|
|
775
|
+
return this.establishSession(await this.microsoftAuth.signIn(options));
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Completes a Microsoft redirect response from `#codeMitra` and `#stateMitra`.
|
|
779
|
+
* Mirrors {@link completeGoogleSignInRedirect}; returns `null` when the current
|
|
780
|
+
* URL is not a Microsoft SSO redirect.
|
|
781
|
+
*/
|
|
782
|
+
async completeMicrosoftSignInRedirect() {
|
|
783
|
+
const tokenResponse = await this.microsoftAuth.completeRedirect();
|
|
784
|
+
return tokenResponse ? this.establishSession(tokenResponse) : null;
|
|
785
|
+
}
|
|
786
|
+
/** @deprecated Email/password registration is not implemented by IAM. Use Google or Microsoft SSO. */
|
|
787
|
+
async signUp(_data) {
|
|
788
|
+
throw new MitraApiError(
|
|
789
|
+
"Email/password registration is not available. Use signInWithGoogle() or signInWithMicrosoft().",
|
|
790
|
+
0,
|
|
791
|
+
"UNSUPPORTED_AUTH_METHOD"
|
|
792
|
+
);
|
|
318
793
|
}
|
|
319
794
|
/**
|
|
320
795
|
* Signs out the current user, clearing all auth state and localStorage.
|
|
@@ -336,9 +811,9 @@ var AuthModule = class {
|
|
|
336
811
|
/**
|
|
337
812
|
* Refreshes the session using the stored refresh token.
|
|
338
813
|
*
|
|
339
|
-
* Called automatically
|
|
340
|
-
*
|
|
341
|
-
*
|
|
814
|
+
* Called automatically before requests whose JWT is close to expiry and on
|
|
815
|
+
* `401` responses. Can also be called manually. Multiple proactive, reactive,
|
|
816
|
+
* and manual calls are deduplicated into one refresh request.
|
|
342
817
|
*
|
|
343
818
|
* @returns `true` if refresh succeeded, `false` otherwise.
|
|
344
819
|
*
|
|
@@ -350,19 +825,50 @@ var AuthModule = class {
|
|
|
350
825
|
*/
|
|
351
826
|
async refreshSession() {
|
|
352
827
|
if (!this.#refreshToken) return false;
|
|
353
|
-
if (this.
|
|
354
|
-
|
|
828
|
+
if (!this.hasValidAppIdentity()) {
|
|
829
|
+
this.clearAuthState();
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
const generation = this.sessionGeneration;
|
|
833
|
+
if (this.refreshFlight?.generation === generation) {
|
|
834
|
+
return this.refreshFlight.promise;
|
|
835
|
+
}
|
|
836
|
+
this.transientRefreshFailureGeneration = null;
|
|
837
|
+
const flight = {
|
|
838
|
+
generation,
|
|
839
|
+
promise: this.doRefresh(generation, this.#refreshToken)
|
|
840
|
+
};
|
|
841
|
+
this.refreshFlight = flight;
|
|
355
842
|
try {
|
|
356
|
-
return await
|
|
843
|
+
return await flight.promise;
|
|
357
844
|
} finally {
|
|
358
|
-
this.
|
|
845
|
+
if (this.refreshFlight === flight) {
|
|
846
|
+
this.refreshFlight = null;
|
|
847
|
+
}
|
|
359
848
|
}
|
|
360
849
|
}
|
|
850
|
+
/**
|
|
851
|
+
* Resolves a 401 against the credential that actually reached the server.
|
|
852
|
+
* A newer session is retried as-is instead of being refreshed because of an
|
|
853
|
+
* older request. A signed-out session is neither refreshed nor retried.
|
|
854
|
+
*
|
|
855
|
+
* @param requestToken - Access token attached to the rejected request.
|
|
856
|
+
* @returns Whether the request should be retried once with the current token.
|
|
857
|
+
*
|
|
858
|
+
* @internal
|
|
859
|
+
*/
|
|
860
|
+
async handleUnauthorized(requestToken) {
|
|
861
|
+
const currentToken = this.#accessToken;
|
|
862
|
+
if (!currentToken) return false;
|
|
863
|
+
if (currentToken !== requestToken) return true;
|
|
864
|
+
return this.refreshSession();
|
|
865
|
+
}
|
|
361
866
|
/**
|
|
362
867
|
* Fetches the current user from the server and updates local state.
|
|
363
868
|
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
869
|
+
* Clears auth state on a definitive 401. When the request reaches 401 after
|
|
870
|
+
* IAM refresh failed due to a network error, 408, 429, or 5xx response, the
|
|
871
|
+
* retained session is preserved and this method returns null.
|
|
366
872
|
*
|
|
367
873
|
* @returns The user if authenticated, `null` otherwise.
|
|
368
874
|
*
|
|
@@ -374,14 +880,17 @@ var AuthModule = class {
|
|
|
374
880
|
*/
|
|
375
881
|
async me() {
|
|
376
882
|
if (!this.#accessToken) return null;
|
|
883
|
+
const generation = this.sessionGeneration;
|
|
377
884
|
try {
|
|
378
885
|
const user = await this.getCurrentUser();
|
|
886
|
+
if (generation !== this.sessionGeneration) return null;
|
|
379
887
|
this._currentUser = user;
|
|
888
|
+
this.transientRefreshFailureGeneration = null;
|
|
380
889
|
this.saveToStorage();
|
|
381
890
|
this.notifyListeners();
|
|
382
891
|
return user;
|
|
383
892
|
} catch (error) {
|
|
384
|
-
if (error instanceof MitraApiError && error.status === 401) {
|
|
893
|
+
if (error instanceof MitraApiError && error.status === 401 && this.#accessToken && generation === this.sessionGeneration && this.transientRefreshFailureGeneration !== this.sessionGeneration) {
|
|
385
894
|
this.clearAuthState();
|
|
386
895
|
}
|
|
387
896
|
return null;
|
|
@@ -416,10 +925,110 @@ var AuthModule = class {
|
|
|
416
925
|
* ```
|
|
417
926
|
*/
|
|
418
927
|
setToken(token, saveToStorage = true) {
|
|
928
|
+
if (!this.belongsToConfiguredApp(token)) {
|
|
929
|
+
this.clearAuthState();
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
this.invalidatePendingRefreshes();
|
|
419
933
|
this.#accessToken = token;
|
|
420
934
|
if (saveToStorage) {
|
|
421
935
|
this.saveToStorage();
|
|
422
936
|
}
|
|
937
|
+
this.notifySessionListeners();
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Reads the tokens currently held by this module.
|
|
941
|
+
*
|
|
942
|
+
* Used by the legacy session bridge to hand a session persisted by this SDK
|
|
943
|
+
* over to `mitra-interactions-sdk` on startup.
|
|
944
|
+
*
|
|
945
|
+
* @returns The access and refresh tokens, each `null` when absent.
|
|
946
|
+
*
|
|
947
|
+
* @internal
|
|
948
|
+
*/
|
|
949
|
+
readSessionTokens() {
|
|
950
|
+
return { token: this.#accessToken, refreshToken: this.#refreshToken };
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Adopts an app-scoped session received from a trusted platform boundary.
|
|
954
|
+
*
|
|
955
|
+
* This preserves the access and refresh tokens together so the normal refresh
|
|
956
|
+
* lifecycle continues after an embedded preview hands its session to the app.
|
|
957
|
+
* Call {@link checkAuth} afterward to validate the token and hydrate the user.
|
|
958
|
+
*/
|
|
959
|
+
setSession(session) {
|
|
960
|
+
return this.adoptSession({
|
|
961
|
+
token: session.accessToken,
|
|
962
|
+
refreshToken: session.refreshToken ?? null
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Ensures the current access token has enough remaining validity.
|
|
967
|
+
*
|
|
968
|
+
* JWT decoding is used only as a scheduling heuristic. Opaque tokens and JWTs
|
|
969
|
+
* without a numeric `exp` claim proceed unchanged and remain server-authoritative.
|
|
970
|
+
* Multiple proactive and reactive callers share the same refresh request.
|
|
971
|
+
* Transient refresh failures preserve the current session so the caller can
|
|
972
|
+
* continue and rely on the normal one-time `401` refresh fallback.
|
|
973
|
+
*
|
|
974
|
+
* This method is suitable for authenticated HTTP, WebSocket, and Server-Sent
|
|
975
|
+
* Events boundaries that need a fresh token before connecting.
|
|
976
|
+
*
|
|
977
|
+
* @param minValidityMs - Minimum remaining token lifetime. Defaults to 30 seconds.
|
|
978
|
+
* @returns `true` when no refresh is needed or refresh succeeds. Returns
|
|
979
|
+
* `false` when a required refresh fails, even when a transient failure keeps
|
|
980
|
+
* the current session available for a reactive server-authoritative fallback.
|
|
981
|
+
*/
|
|
982
|
+
async ensureFreshSession(minValidityMs = DEFAULT_TOKEN_VALIDITY_MS) {
|
|
983
|
+
if (!Number.isFinite(minValidityMs) || minValidityMs < 0) {
|
|
984
|
+
throw new RangeError("minValidityMs must be a finite non-negative number");
|
|
985
|
+
}
|
|
986
|
+
if (!this.#accessToken) return false;
|
|
987
|
+
if (!this.hasValidAppIdentity()) {
|
|
988
|
+
this.clearAuthState();
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
if (!isTokenExpiring(this.#accessToken, minValidityMs)) return true;
|
|
992
|
+
return this.refreshSession();
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Subscribes an internal boundary to token changes without triggering login
|
|
996
|
+
* or refresh. Unlike auth-state listeners, this callback is not invoked
|
|
997
|
+
* immediately and does not depend on the user being hydrated.
|
|
998
|
+
*
|
|
999
|
+
* @param callback - Receives the current access and refresh tokens.
|
|
1000
|
+
* @returns A function that removes the callback.
|
|
1001
|
+
*
|
|
1002
|
+
* @internal
|
|
1003
|
+
*/
|
|
1004
|
+
onSessionChange(callback) {
|
|
1005
|
+
this.sessionListeners.add(callback);
|
|
1006
|
+
return () => this.sessionListeners.delete(callback);
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Adopts a session produced outside this module, such as a legacy SSO login.
|
|
1010
|
+
*
|
|
1011
|
+
* Replaces the in-memory tokens and persists them under the same storage key
|
|
1012
|
+
* the rest of the module uses. The current user is left untouched because the
|
|
1013
|
+
* legacy SDK does not return one; call `me()` to hydrate it.
|
|
1014
|
+
*
|
|
1015
|
+
* @param session - Access token and, when the issuer returned one, refresh token.
|
|
1016
|
+
*
|
|
1017
|
+
* @internal
|
|
1018
|
+
*/
|
|
1019
|
+
adoptSession(session) {
|
|
1020
|
+
if (!this.belongsToConfiguredApp(session.token) || typeof session.refreshToken === "string" && !this.belongsToConfiguredApp(session.refreshToken)) {
|
|
1021
|
+
this.clearAuthState();
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
this.invalidatePendingRefreshes();
|
|
1025
|
+
this.#accessToken = session.token;
|
|
1026
|
+
if (session.refreshToken !== void 0) {
|
|
1027
|
+
this.#refreshToken = session.refreshToken;
|
|
1028
|
+
}
|
|
1029
|
+
this.saveToStorage();
|
|
1030
|
+
this.notifySessionListeners();
|
|
1031
|
+
return true;
|
|
423
1032
|
}
|
|
424
1033
|
/**
|
|
425
1034
|
* Redirects to `/login?returnUrl=...` for unauthenticated users.
|
|
@@ -463,27 +1072,65 @@ var AuthModule = class {
|
|
|
463
1072
|
this.listeners.delete(callback);
|
|
464
1073
|
};
|
|
465
1074
|
}
|
|
466
|
-
async doRefresh() {
|
|
1075
|
+
async doRefresh(generation, refreshToken) {
|
|
1076
|
+
let tokenResponse;
|
|
467
1077
|
try {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1078
|
+
tokenResponse = expectAuthTokenResponse(
|
|
1079
|
+
await this.publicClient.post(
|
|
1080
|
+
"/api/v1/auth/refresh-token",
|
|
1081
|
+
{ refreshToken }
|
|
1082
|
+
)
|
|
471
1083
|
);
|
|
472
|
-
|
|
473
|
-
this
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (generation !== this.sessionGeneration) return false;
|
|
1086
|
+
if (isDefinitiveRefreshFailure(error)) {
|
|
1087
|
+
this.clearAuthState();
|
|
1088
|
+
} else {
|
|
1089
|
+
this.transientRefreshFailureGeneration = generation;
|
|
1090
|
+
}
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
if (generation !== this.sessionGeneration) return false;
|
|
1094
|
+
if (!this.belongsToConfiguredApp(tokenResponse.accessToken) || !this.belongsToConfiguredApp(tokenResponse.refreshToken)) {
|
|
478
1095
|
this.clearAuthState();
|
|
479
1096
|
return false;
|
|
480
1097
|
}
|
|
1098
|
+
this.#accessToken = tokenResponse.accessToken;
|
|
1099
|
+
this.#refreshToken = tokenResponse.refreshToken;
|
|
1100
|
+
this.transientRefreshFailureGeneration = null;
|
|
1101
|
+
this.saveToStorage();
|
|
1102
|
+
this.notifySessionListeners();
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
async establishSession(tokenResponse) {
|
|
1106
|
+
if (!this.belongsToConfiguredApp(tokenResponse.accessToken) || !this.belongsToConfiguredApp(tokenResponse.refreshToken)) {
|
|
1107
|
+
this.clearAuthState();
|
|
1108
|
+
throw coreErrors.invalidResponse("Authentication returned a token for a different app");
|
|
1109
|
+
}
|
|
1110
|
+
this.invalidatePendingRefreshes();
|
|
1111
|
+
const generation = this.sessionGeneration;
|
|
1112
|
+
this.#accessToken = tokenResponse.accessToken;
|
|
1113
|
+
this.#refreshToken = tokenResponse.refreshToken;
|
|
1114
|
+
try {
|
|
1115
|
+
const user = await this.getCurrentUser();
|
|
1116
|
+
if (generation !== this.sessionGeneration) {
|
|
1117
|
+
throw new Error("Authentication session was superseded");
|
|
1118
|
+
}
|
|
1119
|
+
this.setAuthState(user, this.#accessToken, this.#refreshToken);
|
|
1120
|
+
return user;
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (generation === this.sessionGeneration) {
|
|
1123
|
+
this.clearAuthState();
|
|
1124
|
+
}
|
|
1125
|
+
throw error;
|
|
1126
|
+
}
|
|
481
1127
|
}
|
|
482
1128
|
setAuthState(user, token, refreshToken) {
|
|
483
1129
|
this._currentUser = user;
|
|
484
1130
|
this.#accessToken = token;
|
|
485
1131
|
this.#refreshToken = refreshToken;
|
|
486
1132
|
this.saveToStorage();
|
|
1133
|
+
this.notifySessionListeners();
|
|
487
1134
|
this.notifyListeners();
|
|
488
1135
|
}
|
|
489
1136
|
async getCurrentUser() {
|
|
@@ -491,11 +1138,35 @@ var AuthModule = class {
|
|
|
491
1138
|
return { ...user, tenantId: user.tenant.id };
|
|
492
1139
|
}
|
|
493
1140
|
clearAuthState() {
|
|
1141
|
+
const hadAuthState = this._currentUser !== null || this.#accessToken !== null || this.#refreshToken !== null;
|
|
1142
|
+
this.invalidatePendingRefreshes();
|
|
494
1143
|
this._currentUser = null;
|
|
495
1144
|
this.#accessToken = null;
|
|
496
1145
|
this.#refreshToken = null;
|
|
497
1146
|
this.removeFromStorage();
|
|
498
|
-
|
|
1147
|
+
if (hadAuthState) {
|
|
1148
|
+
this.notifySessionListeners();
|
|
1149
|
+
this.notifyListeners();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
invalidatePendingRefreshes() {
|
|
1153
|
+
this.sessionGeneration += 1;
|
|
1154
|
+
this.transientRefreshFailureGeneration = null;
|
|
1155
|
+
}
|
|
1156
|
+
hasValidAppIdentity() {
|
|
1157
|
+
return (!this.#accessToken || this.belongsToConfiguredApp(this.#accessToken)) && (!this.#refreshToken || this.belongsToConfiguredApp(this.#refreshToken));
|
|
1158
|
+
}
|
|
1159
|
+
belongsToConfiguredApp(token) {
|
|
1160
|
+
return belongsToApp(token, this.appId);
|
|
1161
|
+
}
|
|
1162
|
+
notifySessionListeners() {
|
|
1163
|
+
const session = this.readSessionTokens();
|
|
1164
|
+
this.sessionListeners.forEach((callback) => {
|
|
1165
|
+
try {
|
|
1166
|
+
callback(session);
|
|
1167
|
+
} catch {
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
499
1170
|
}
|
|
500
1171
|
notifyListeners() {
|
|
501
1172
|
this.listeners.forEach((callback) => {
|
|
@@ -526,9 +1197,14 @@ var AuthModule = class {
|
|
|
526
1197
|
const stored = localStorage.getItem(this.storageKey);
|
|
527
1198
|
if (stored) {
|
|
528
1199
|
const { user, token, refreshToken } = JSON.parse(stored);
|
|
1200
|
+
if (typeof token !== "string" || refreshToken !== null && refreshToken !== void 0 && typeof refreshToken !== "string" || !this.belongsToConfiguredApp(token) || typeof refreshToken === "string" && !this.belongsToConfiguredApp(refreshToken)) {
|
|
1201
|
+
this.clearAuthState();
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
529
1204
|
this._currentUser = user;
|
|
530
1205
|
this.#accessToken = token;
|
|
531
1206
|
this.#refreshToken = refreshToken ?? null;
|
|
1207
|
+
this.invalidatePendingRefreshes();
|
|
532
1208
|
}
|
|
533
1209
|
} catch {
|
|
534
1210
|
this.removeFromStorage();
|
|
@@ -544,12 +1220,13 @@ var AuthModule = class {
|
|
|
544
1220
|
};
|
|
545
1221
|
|
|
546
1222
|
// src/modules/entities.ts
|
|
547
|
-
|
|
1223
|
+
import {
|
|
1224
|
+
createEntitiesModule
|
|
1225
|
+
} from "@mitralab.io/sdk-core";
|
|
548
1226
|
var EntitiesModule = class _EntitiesModule {
|
|
549
|
-
constructor(httpClient,
|
|
1227
|
+
constructor(httpClient, _dataSourceId) {
|
|
550
1228
|
this.httpClient = httpClient;
|
|
551
|
-
|
|
552
|
-
this.core = (0, import_sdk_core2.createEntitiesModule)(httpClient, coreErrors);
|
|
1229
|
+
this.core = createEntitiesModule(httpClient, coreErrors);
|
|
553
1230
|
}
|
|
554
1231
|
core;
|
|
555
1232
|
static createProxy(httpClient, dataSourceId) {
|
|
@@ -567,9 +1244,8 @@ var EntitiesModule = class _EntitiesModule {
|
|
|
567
1244
|
* Preserved for Platform SDK 1.x compatibility.
|
|
568
1245
|
* Records now resolve the app from authenticated context instead of a data source path.
|
|
569
1246
|
*/
|
|
570
|
-
setDataSourceId(
|
|
571
|
-
|
|
572
|
-
this.core = (0, import_sdk_core2.createEntitiesModule)(this.httpClient, coreErrors);
|
|
1247
|
+
setDataSourceId(_dataSourceId) {
|
|
1248
|
+
this.core = createEntitiesModule(this.httpClient, coreErrors);
|
|
573
1249
|
}
|
|
574
1250
|
getTable(tableName) {
|
|
575
1251
|
return this.core.getTable(tableName);
|
|
@@ -577,33 +1253,53 @@ var EntitiesModule = class _EntitiesModule {
|
|
|
577
1253
|
};
|
|
578
1254
|
|
|
579
1255
|
// src/modules/functions.ts
|
|
580
|
-
|
|
1256
|
+
import {
|
|
1257
|
+
createFunctionsModule
|
|
1258
|
+
} from "@mitralab.io/sdk-core";
|
|
581
1259
|
var FunctionsModule = class {
|
|
582
1260
|
core;
|
|
583
1261
|
constructor(httpClient) {
|
|
584
|
-
this.core =
|
|
1262
|
+
this.core = createFunctionsModule(
|
|
1263
|
+
httpClient,
|
|
1264
|
+
{ emptyInput: "omit-body", executeInvocationType: "sync" },
|
|
1265
|
+
coreErrors
|
|
1266
|
+
);
|
|
585
1267
|
}
|
|
586
1268
|
/**
|
|
587
|
-
* Executes a Function
|
|
588
|
-
* The runtime SDK uses an explicit invocation header instead.
|
|
1269
|
+
* Executes a Function synchronously and waits for its terminal result.
|
|
589
1270
|
*/
|
|
590
1271
|
async execute(functionId, input) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
1272
|
+
return this.core.execute(functionId, input);
|
|
1273
|
+
}
|
|
1274
|
+
/** Queues a Function and returns its initial execution record. */
|
|
1275
|
+
async executeAsync(functionId, input) {
|
|
1276
|
+
return this.core.executeAsync(functionId, input);
|
|
1277
|
+
}
|
|
1278
|
+
/** Reads the current state of an asynchronous Function execution. */
|
|
1279
|
+
async getExecution(executionId) {
|
|
1280
|
+
return this.core.getExecution(executionId);
|
|
1281
|
+
}
|
|
1282
|
+
/** Requests cancellation of a queued or running Function execution. */
|
|
1283
|
+
cancelExecution(executionId) {
|
|
1284
|
+
return this.core.cancelExecution(executionId);
|
|
598
1285
|
}
|
|
599
1286
|
};
|
|
600
1287
|
|
|
601
1288
|
// src/modules/integration.ts
|
|
602
|
-
|
|
1289
|
+
import {
|
|
1290
|
+
createIntegrationAdminModule,
|
|
1291
|
+
createIntegrationModule
|
|
1292
|
+
} from "@mitralab.io/sdk-core";
|
|
603
1293
|
var IntegrationModule = class {
|
|
604
1294
|
core;
|
|
1295
|
+
configs;
|
|
605
1296
|
constructor(httpClient) {
|
|
606
|
-
this.core =
|
|
1297
|
+
this.core = createIntegrationModule(httpClient, coreErrors);
|
|
1298
|
+
this.configs = createIntegrationAdminModule(httpClient, coreErrors);
|
|
1299
|
+
}
|
|
1300
|
+
/** Lists the current app's integration configs without exposing admin mutations. */
|
|
1301
|
+
list(options) {
|
|
1302
|
+
return this.configs.list(options);
|
|
607
1303
|
}
|
|
608
1304
|
executeResource(resourceId, params) {
|
|
609
1305
|
return this.core.executeResource(resourceId, params);
|
|
@@ -611,34 +1307,307 @@ var IntegrationModule = class {
|
|
|
611
1307
|
execute(configId, request) {
|
|
612
1308
|
return this.core.execute(configId, request);
|
|
613
1309
|
}
|
|
1310
|
+
/** Executes a saved integration config selected by its app-scoped alias. */
|
|
1311
|
+
executeByAlias(alias, request) {
|
|
1312
|
+
return this.core.executeByAlias(alias, request);
|
|
1313
|
+
}
|
|
614
1314
|
};
|
|
615
1315
|
|
|
616
1316
|
// src/modules/queries.ts
|
|
617
|
-
|
|
1317
|
+
import {
|
|
1318
|
+
createQueriesModule
|
|
1319
|
+
} from "@mitralab.io/sdk-core";
|
|
618
1320
|
var QueriesModule = class {
|
|
619
|
-
dataSourceId = "";
|
|
620
1321
|
core;
|
|
621
1322
|
constructor(httpClient) {
|
|
622
|
-
this.core =
|
|
1323
|
+
this.core = createQueriesModule(httpClient, coreErrors);
|
|
623
1324
|
}
|
|
624
|
-
/**
|
|
625
|
-
|
|
626
|
-
|
|
1325
|
+
/**
|
|
1326
|
+
* @deprecated Preserved for Platform SDK 1.x source compatibility. Data Manager now resolves
|
|
1327
|
+
* the Data Source from the authenticated app.
|
|
1328
|
+
*/
|
|
1329
|
+
setDataSourceId(_dataSourceId) {
|
|
627
1330
|
}
|
|
628
1331
|
async execute(id, parameters) {
|
|
629
|
-
|
|
630
|
-
|
|
1332
|
+
return this.core.execute(id, parameters);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
// src/modules/agent-tasks.ts
|
|
1337
|
+
import {
|
|
1338
|
+
createAgentTaskSessionManager,
|
|
1339
|
+
createAgentTasksModule,
|
|
1340
|
+
withAgentTaskSessions
|
|
1341
|
+
} from "@mitralab.io/sdk-core";
|
|
1342
|
+
|
|
1343
|
+
// src/modules/agent-session.ts
|
|
1344
|
+
var CONNECT_TIMEOUT_MS = 15e3;
|
|
1345
|
+
function stripBearer2(token) {
|
|
1346
|
+
return token.replace(/^Bearer\s+/i, "");
|
|
1347
|
+
}
|
|
1348
|
+
function asObject(value) {
|
|
1349
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1350
|
+
}
|
|
1351
|
+
function expectEvent(value) {
|
|
1352
|
+
const event = asObject(value);
|
|
1353
|
+
if (!event || typeof event.type !== "string" || !event.type) return null;
|
|
1354
|
+
if (typeof event.timestamp !== "number" || !Number.isFinite(event.timestamp)) return null;
|
|
1355
|
+
if (event.sequence !== void 0 && (typeof event.sequence !== "number" || !Number.isSafeInteger(event.sequence) || event.sequence < 0)) return null;
|
|
1356
|
+
return {
|
|
1357
|
+
type: event.type,
|
|
1358
|
+
payload: event.payload,
|
|
1359
|
+
timestamp: event.timestamp,
|
|
1360
|
+
...typeof event.sequence === "number" ? { sequence: event.sequence } : {}
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function parseEvent(raw) {
|
|
1364
|
+
if (typeof raw !== "string") return expectEvent(raw);
|
|
1365
|
+
try {
|
|
1366
|
+
return expectEvent(JSON.parse(raw));
|
|
1367
|
+
} catch {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
function isNormalClose(event) {
|
|
1372
|
+
return event.code === 1e3 || event.code === 4409;
|
|
1373
|
+
}
|
|
1374
|
+
var BrowserAgentTaskEventSource = class {
|
|
1375
|
+
constructor(auth, apiUrl) {
|
|
1376
|
+
this.auth = auth;
|
|
1377
|
+
this.apiUrl = stripTrailingSlashes(apiUrl);
|
|
1378
|
+
}
|
|
1379
|
+
apiUrl;
|
|
1380
|
+
sseFallbackTasks = /* @__PURE__ */ new Set();
|
|
1381
|
+
async open(taskId, observer, signal, transport = "auto") {
|
|
1382
|
+
if (transport === "http") return this.openSse(taskId, observer, signal);
|
|
1383
|
+
if (transport === "websocket") return this.openWebSocket(taskId, observer, signal);
|
|
1384
|
+
if (this.sseFallbackTasks.has(taskId)) {
|
|
1385
|
+
return this.wrapAutoConnection(taskId, await this.openSse(taskId, observer, signal));
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
const connection = await this.openWebSocket(taskId, {
|
|
1389
|
+
...observer,
|
|
1390
|
+
onDisconnect: (error) => {
|
|
1391
|
+
this.sseFallbackTasks.add(taskId);
|
|
1392
|
+
observer.onDisconnect(error);
|
|
1393
|
+
}
|
|
1394
|
+
}, signal);
|
|
1395
|
+
return this.wrapAutoConnection(taskId, connection);
|
|
1396
|
+
} catch {
|
|
1397
|
+
this.sseFallbackTasks.add(taskId);
|
|
1398
|
+
return this.wrapAutoConnection(taskId, await this.openSse(taskId, observer, signal));
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
wrapAutoConnection(taskId, connection) {
|
|
1402
|
+
return {
|
|
1403
|
+
close: () => {
|
|
1404
|
+
this.sseFallbackTasks.delete(taskId);
|
|
1405
|
+
connection.close();
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
async requireFreshToken() {
|
|
1410
|
+
const fresh = await this.auth.ensureFreshSession();
|
|
1411
|
+
const token = this.auth.accessToken;
|
|
1412
|
+
if (!fresh || !token) {
|
|
1413
|
+
throw new Error("A fresh authenticated app session is required for Agent streaming.");
|
|
1414
|
+
}
|
|
1415
|
+
return token;
|
|
1416
|
+
}
|
|
1417
|
+
async openWebSocket(taskId, observer, signal) {
|
|
1418
|
+
if (typeof globalThis.WebSocket !== "function") {
|
|
1419
|
+
throw new TypeError("WebSocket is not available.");
|
|
1420
|
+
}
|
|
1421
|
+
if (signal?.aborted) throw signal.reason ?? new Error("Agent WebSocket connection aborted.");
|
|
1422
|
+
const token = await this.requireFreshToken();
|
|
1423
|
+
const url = `${this.apiUrl.replace(/^http/i, "ws")}/copilot/ws/tasks/${encodeURIComponent(taskId)}?token=${encodeURIComponent(stripBearer2(token))}`;
|
|
1424
|
+
return new Promise((resolve, reject) => {
|
|
1425
|
+
const socket = new globalThis.WebSocket(url);
|
|
1426
|
+
let opened = false;
|
|
1427
|
+
let intentionalClose = false;
|
|
1428
|
+
let settled = false;
|
|
1429
|
+
const removeAbortListener = () => signal?.removeEventListener("abort", onAbort);
|
|
1430
|
+
const clearHandshake = () => {
|
|
1431
|
+
globalThis.clearTimeout(timer);
|
|
1432
|
+
removeAbortListener();
|
|
1433
|
+
};
|
|
1434
|
+
const rejectHandshake = (error) => {
|
|
1435
|
+
if (settled) return;
|
|
1436
|
+
settled = true;
|
|
1437
|
+
intentionalClose = true;
|
|
1438
|
+
clearHandshake();
|
|
1439
|
+
socket.close();
|
|
1440
|
+
reject(error);
|
|
1441
|
+
};
|
|
1442
|
+
const close = () => {
|
|
1443
|
+
if (intentionalClose) return;
|
|
1444
|
+
intentionalClose = true;
|
|
1445
|
+
removeAbortListener();
|
|
1446
|
+
socket.close(1e3, "Client closed");
|
|
1447
|
+
};
|
|
1448
|
+
const onAbort = () => {
|
|
1449
|
+
if (!opened) {
|
|
1450
|
+
rejectHandshake(signal?.reason instanceof Error ? signal.reason : new Error("Agent WebSocket connection aborted."));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
close();
|
|
1454
|
+
};
|
|
1455
|
+
const timer = globalThis.setTimeout(() => {
|
|
1456
|
+
rejectHandshake(new Error("Timed out connecting to the Agent WebSocket."));
|
|
1457
|
+
}, CONNECT_TIMEOUT_MS);
|
|
1458
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1459
|
+
socket.onopen = () => {
|
|
1460
|
+
if (settled) return;
|
|
1461
|
+
opened = true;
|
|
1462
|
+
settled = true;
|
|
1463
|
+
globalThis.clearTimeout(timer);
|
|
1464
|
+
resolve({ close });
|
|
1465
|
+
};
|
|
1466
|
+
socket.onerror = () => {
|
|
1467
|
+
if (!opened) rejectHandshake(new Error("Failed to connect to the Agent WebSocket."));
|
|
1468
|
+
};
|
|
1469
|
+
socket.onmessage = (message) => {
|
|
1470
|
+
const event = parseEvent(message.data);
|
|
1471
|
+
if (event) observer.onEvent(event);
|
|
1472
|
+
};
|
|
1473
|
+
socket.onclose = (event) => {
|
|
1474
|
+
globalThis.clearTimeout(timer);
|
|
1475
|
+
removeAbortListener();
|
|
1476
|
+
if (!opened) {
|
|
1477
|
+
rejectHandshake(new Error(`Agent WebSocket closed during handshake (${event.code}).`));
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (!intentionalClose && !isNormalClose(event)) {
|
|
1481
|
+
observer.onDisconnect(new Error(`Agent WebSocket disconnected (${event.code}).`));
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
async openSse(taskId, observer, signal) {
|
|
1487
|
+
if (signal?.aborted) throw signal.reason ?? new Error("Agent SSE connection aborted.");
|
|
1488
|
+
const requestToken = await this.requireFreshToken();
|
|
1489
|
+
const abort = new AbortController();
|
|
1490
|
+
let intentionalClose = false;
|
|
1491
|
+
let disconnected = false;
|
|
1492
|
+
const url = `${this.apiUrl}/copilot/api/v1/tasks/${encodeURIComponent(taskId)}/events`;
|
|
1493
|
+
const request = (token) => globalThis.fetch(url, {
|
|
1494
|
+
headers: { Accept: "text/event-stream", Authorization: `Bearer ${stripBearer2(token)}` },
|
|
1495
|
+
signal: abort.signal
|
|
1496
|
+
});
|
|
1497
|
+
const onAbort = () => {
|
|
1498
|
+
intentionalClose = true;
|
|
1499
|
+
abort.abort(signal?.reason);
|
|
1500
|
+
};
|
|
1501
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1502
|
+
let response = await request(requestToken);
|
|
1503
|
+
if (response.status === 401 && await this.auth.handleUnauthorized(requestToken)) {
|
|
1504
|
+
const currentToken = this.auth.accessToken;
|
|
1505
|
+
if (currentToken) response = await request(currentToken);
|
|
1506
|
+
}
|
|
1507
|
+
if (!response.ok || !response.body) {
|
|
1508
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1509
|
+
abort.abort();
|
|
1510
|
+
throw new Error(`Agent SSE connection failed (${response.status}).`);
|
|
1511
|
+
}
|
|
1512
|
+
const disconnect = (error) => {
|
|
1513
|
+
if (disconnected || intentionalClose || signal?.aborted) return;
|
|
1514
|
+
disconnected = true;
|
|
1515
|
+
observer.onDisconnect(error);
|
|
1516
|
+
};
|
|
1517
|
+
void this.readSse(response.body, observer, abort.signal).then(() => disconnect()).catch((error) => disconnect(error)).finally(() => signal?.removeEventListener("abort", onAbort));
|
|
1518
|
+
return {
|
|
1519
|
+
close: () => {
|
|
1520
|
+
if (intentionalClose) return;
|
|
1521
|
+
intentionalClose = true;
|
|
1522
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1523
|
+
abort.abort();
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
async readSse(body, observer, signal) {
|
|
1528
|
+
const reader = body.getReader();
|
|
1529
|
+
const decoder = new TextDecoder();
|
|
1530
|
+
let buffer = "";
|
|
1531
|
+
try {
|
|
1532
|
+
while (!signal.aborted) {
|
|
1533
|
+
const { done, value } = await reader.read();
|
|
1534
|
+
if (done) break;
|
|
1535
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1536
|
+
let separator = /\r?\n\r?\n/.exec(buffer);
|
|
1537
|
+
while (separator?.index !== void 0) {
|
|
1538
|
+
const block = buffer.slice(0, separator.index);
|
|
1539
|
+
buffer = buffer.slice(separator.index + separator[0].length);
|
|
1540
|
+
const data = block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
1541
|
+
const event = data ? parseEvent(data) : null;
|
|
1542
|
+
if (event) observer.onEvent(event);
|
|
1543
|
+
separator = /\r?\n\r?\n/.exec(buffer);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
} finally {
|
|
1547
|
+
reader.releaseLock();
|
|
1548
|
+
}
|
|
631
1549
|
}
|
|
632
1550
|
};
|
|
633
1551
|
|
|
1552
|
+
// src/modules/agent-tasks.ts
|
|
1553
|
+
function createBrowserAgentTasksModule(httpClient, auth, apiUrl) {
|
|
1554
|
+
const tasks = createAgentTasksModule(httpClient, coreErrors);
|
|
1555
|
+
const manager = createAgentTaskSessionManager({
|
|
1556
|
+
tasks,
|
|
1557
|
+
eventSource: new BrowserAgentTaskEventSource(auth, apiUrl)
|
|
1558
|
+
});
|
|
1559
|
+
return withAgentTaskSessions(tasks, manager);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// src/modules/agent-credentials.ts
|
|
1563
|
+
import {
|
|
1564
|
+
createAgentCredentialsModule
|
|
1565
|
+
} from "@mitralab.io/sdk-core";
|
|
1566
|
+
function requireProvider(provider, allowed, flow) {
|
|
1567
|
+
if (!allowed.includes(provider)) {
|
|
1568
|
+
throw new TypeError(`${flow} does not support provider ${provider}.`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function createBrowserAgentCredentialsModule(httpClient) {
|
|
1572
|
+
const core = createAgentCredentialsModule(httpClient, coreErrors);
|
|
1573
|
+
return {
|
|
1574
|
+
list: () => core.list(),
|
|
1575
|
+
listModels: (agentId) => core.listModels(agentId),
|
|
1576
|
+
saveApiKey: (provider, apiKey) => {
|
|
1577
|
+
requireProvider(provider, ["ANTHROPIC", "OPENAI"], "API key authentication");
|
|
1578
|
+
return core.saveApiKey(provider, apiKey);
|
|
1579
|
+
},
|
|
1580
|
+
remove: (provider) => {
|
|
1581
|
+
requireProvider(provider, ["ANTHROPIC", "OPENAI"], "Credential removal");
|
|
1582
|
+
return core.remove(provider);
|
|
1583
|
+
},
|
|
1584
|
+
startOAuth: (provider) => {
|
|
1585
|
+
requireProvider(provider, ["ANTHROPIC"], "OAuth");
|
|
1586
|
+
return core.startOAuth(provider);
|
|
1587
|
+
},
|
|
1588
|
+
exchangeOAuth: (provider, input) => {
|
|
1589
|
+
requireProvider(provider, ["ANTHROPIC"], "OAuth");
|
|
1590
|
+
return core.exchangeOAuth(provider, input);
|
|
1591
|
+
},
|
|
1592
|
+
startDeviceAuthorization: (provider) => {
|
|
1593
|
+
requireProvider(provider, ["OPENAI"], "Device authorization");
|
|
1594
|
+
return core.startDeviceAuthorization(provider);
|
|
1595
|
+
},
|
|
1596
|
+
pollDeviceAuthorization: (provider, deviceAuthId) => {
|
|
1597
|
+
requireProvider(provider, ["OPENAI"], "Device authorization");
|
|
1598
|
+
return core.pollDeviceAuthorization(provider, deviceAuthId);
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
634
1603
|
// src/client.ts
|
|
635
1604
|
function expectAppInfoResponse(value) {
|
|
636
|
-
const response = (
|
|
1605
|
+
const response = expectObject2(
|
|
637
1606
|
value,
|
|
638
1607
|
"App info response",
|
|
639
1608
|
coreErrors
|
|
640
1609
|
);
|
|
641
|
-
if (typeof response.dataSourceId !== "string" || !response.dataSourceId.trim()) {
|
|
1610
|
+
if (response.dataSourceId !== null && (typeof response.dataSourceId !== "string" || !response.dataSourceId.trim())) {
|
|
642
1611
|
throw coreErrors.invalidResponse(
|
|
643
1612
|
"App info response has an invalid dataSourceId field"
|
|
644
1613
|
);
|
|
@@ -654,18 +1623,23 @@ function expectAppInfoResponse(value) {
|
|
|
654
1623
|
};
|
|
655
1624
|
}
|
|
656
1625
|
function createClient(config) {
|
|
657
|
-
const { appId, apiUrl, onError } = config;
|
|
658
|
-
const
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
const
|
|
664
|
-
const
|
|
1626
|
+
const { appId, apiUrl, authPageUrl, onError } = config;
|
|
1627
|
+
const gatewayUrl = stripTrailingSlashes(apiUrl);
|
|
1628
|
+
const iamUrl = `${gatewayUrl}/iam`;
|
|
1629
|
+
const dataManagerUrl = `${gatewayUrl}/data-manager`;
|
|
1630
|
+
const functionsUrl = `${gatewayUrl}/functions`;
|
|
1631
|
+
const integrationUrl = `${gatewayUrl}/integration`;
|
|
1632
|
+
const codeStudioUrl = `${gatewayUrl}/code-studio`;
|
|
1633
|
+
const copilotUrl = `${gatewayUrl}/copilot`;
|
|
1634
|
+
const authModule = new AuthModule(appId, iamUrl, { apiUrl: gatewayUrl, authPageUrl });
|
|
1635
|
+
const authSession = getAuthSessionPort(authModule);
|
|
1636
|
+
const onUnauthorized = (requestToken) => authSession.handleUnauthorized(requestToken);
|
|
1637
|
+
const beforeAuthenticatedRequest = () => authModule.ensureFreshSession().then(() => void 0);
|
|
665
1638
|
const defaultHeaders = { "X-App-Id": appId };
|
|
666
1639
|
const httpClient = new HttpClient({
|
|
667
1640
|
baseUrl: dataManagerUrl,
|
|
668
1641
|
getToken: () => authModule.accessToken,
|
|
1642
|
+
beforeAuthenticatedRequest,
|
|
669
1643
|
onUnauthorized,
|
|
670
1644
|
onError,
|
|
671
1645
|
defaultHeaders
|
|
@@ -674,20 +1648,43 @@ function createClient(config) {
|
|
|
674
1648
|
const functionsHttpClient = new HttpClient({
|
|
675
1649
|
baseUrl: functionsUrl,
|
|
676
1650
|
getToken: () => authModule.accessToken,
|
|
1651
|
+
beforeAuthenticatedRequest,
|
|
677
1652
|
onUnauthorized,
|
|
678
1653
|
onError,
|
|
679
1654
|
defaultHeaders
|
|
680
1655
|
});
|
|
681
1656
|
const functionsModule = new FunctionsModule(functionsHttpClient);
|
|
1657
|
+
const publicFunctionsModule = createPublicFunctionsModule(
|
|
1658
|
+
new HttpClient({ baseUrl: functionsUrl, getToken: () => null }),
|
|
1659
|
+
coreErrors
|
|
1660
|
+
);
|
|
1661
|
+
const copilotHttpClient = new HttpClient({
|
|
1662
|
+
baseUrl: copilotUrl,
|
|
1663
|
+
getToken: () => authModule.accessToken,
|
|
1664
|
+
beforeAuthenticatedRequest,
|
|
1665
|
+
onUnauthorized,
|
|
1666
|
+
onError,
|
|
1667
|
+
defaultHeaders
|
|
1668
|
+
});
|
|
1669
|
+
const agentTasksModule = createBrowserAgentTasksModule(
|
|
1670
|
+
copilotHttpClient,
|
|
1671
|
+
authSession,
|
|
1672
|
+
gatewayUrl
|
|
1673
|
+
);
|
|
1674
|
+
const agentCredentialsModule = createBrowserAgentCredentialsModule(copilotHttpClient);
|
|
682
1675
|
const integrationHttpClient = new HttpClient({
|
|
683
1676
|
baseUrl: integrationUrl,
|
|
684
1677
|
getToken: () => authModule.accessToken,
|
|
1678
|
+
beforeAuthenticatedRequest,
|
|
685
1679
|
onUnauthorized,
|
|
686
1680
|
onError,
|
|
687
1681
|
defaultHeaders
|
|
688
1682
|
});
|
|
689
1683
|
const integrationModule = new IntegrationModule(integrationHttpClient);
|
|
690
1684
|
const queriesModule = new QueriesModule(httpClient);
|
|
1685
|
+
const legacyBridge = new LegacySessionBridge(authSession, appId, apiUrl, authPageUrl);
|
|
1686
|
+
setActiveBridge(legacyBridge);
|
|
1687
|
+
legacyBridge.connect();
|
|
691
1688
|
let initialized = false;
|
|
692
1689
|
let allowSignup = true;
|
|
693
1690
|
async function init() {
|
|
@@ -698,11 +1695,12 @@ function createClient(config) {
|
|
|
698
1695
|
});
|
|
699
1696
|
const appInfo = expectAppInfoResponse(
|
|
700
1697
|
await publicClient.get(
|
|
701
|
-
`/api/v1/apps/${
|
|
1698
|
+
`/api/v1/apps/${encodePathSegment(appId, "appId", coreErrors)}/info`
|
|
702
1699
|
)
|
|
703
1700
|
);
|
|
704
|
-
|
|
705
|
-
|
|
1701
|
+
if (appInfo.dataSourceId) {
|
|
1702
|
+
entitiesModule.setDataSourceId(appInfo.dataSourceId);
|
|
1703
|
+
}
|
|
706
1704
|
allowSignup = appInfo.allowSignup;
|
|
707
1705
|
initialized = true;
|
|
708
1706
|
}
|
|
@@ -711,6 +1709,9 @@ function createClient(config) {
|
|
|
711
1709
|
auth: authModule,
|
|
712
1710
|
entities: entitiesModule,
|
|
713
1711
|
functions: functionsModule,
|
|
1712
|
+
publicFunctions: publicFunctionsModule,
|
|
1713
|
+
agentTasks: agentTasksModule,
|
|
1714
|
+
agentCredentials: agentCredentialsModule,
|
|
714
1715
|
integration: integrationModule,
|
|
715
1716
|
queries: queriesModule,
|
|
716
1717
|
get allowSignup() {
|
|
@@ -719,8 +1720,85 @@ function createClient(config) {
|
|
|
719
1720
|
config
|
|
720
1721
|
};
|
|
721
1722
|
}
|
|
722
|
-
|
|
723
|
-
|
|
1723
|
+
|
|
1724
|
+
// src/legacy/index.ts
|
|
1725
|
+
import {
|
|
1726
|
+
exchangeSsoCodeMitra as legacyExchangeSsoCodeMitra,
|
|
1727
|
+
loginMitra as legacyLoginMitra,
|
|
1728
|
+
loginWithGoogleMitra as legacyLoginWithGoogleMitra,
|
|
1729
|
+
loginWithMicrosoftMitra as legacyLoginWithMicrosoftMitra
|
|
1730
|
+
} from "mitra-interactions-sdk";
|
|
1731
|
+
import { configureSdkMitra as configureSdkMitra2 } from "mitra-interactions-sdk";
|
|
1732
|
+
import { createMitraInstance } from "mitra-interactions-sdk";
|
|
1733
|
+
import { getConfig as getConfig2 } from "mitra-interactions-sdk";
|
|
1734
|
+
import { resolveProjectId } from "mitra-interactions-sdk";
|
|
1735
|
+
import { refreshTokenSilently } from "mitra-interactions-sdk";
|
|
1736
|
+
import { executeServerFunctionMitra } from "mitra-interactions-sdk";
|
|
1737
|
+
import { executeServerFunctionAsyncMitra } from "mitra-interactions-sdk";
|
|
1738
|
+
import { executePublicServerFunctionMitra } from "mitra-interactions-sdk";
|
|
1739
|
+
import { executePublicServerFunctionAsyncMitra } from "mitra-interactions-sdk";
|
|
1740
|
+
import { getPublicServerFunctionExecutionMitra } from "mitra-interactions-sdk";
|
|
1741
|
+
import { stopServerFunctionExecutionMitra } from "mitra-interactions-sdk";
|
|
1742
|
+
import { callIntegrationMitra } from "mitra-interactions-sdk";
|
|
1743
|
+
import { listIntegrationsMitra } from "mitra-interactions-sdk";
|
|
1744
|
+
import { listRecordsMitra } from "mitra-interactions-sdk";
|
|
1745
|
+
import { getRecordMitra } from "mitra-interactions-sdk";
|
|
1746
|
+
import { createRecordMitra } from "mitra-interactions-sdk";
|
|
1747
|
+
import { createRecordsBatchMitra } from "mitra-interactions-sdk";
|
|
1748
|
+
import { updateRecordMitra } from "mitra-interactions-sdk";
|
|
1749
|
+
import { patchRecordMitra } from "mitra-interactions-sdk";
|
|
1750
|
+
import { deleteRecordMitra } from "mitra-interactions-sdk";
|
|
1751
|
+
import { getAgentTaskMitra } from "mitra-interactions-sdk";
|
|
1752
|
+
import { manageAgentChatMitra } from "mitra-interactions-sdk";
|
|
1753
|
+
import { manageAgentCredentialMitra } from "mitra-interactions-sdk";
|
|
1754
|
+
var loginMitra = async (method, options) => {
|
|
1755
|
+
const session = await legacyLoginMitra(method, options);
|
|
1756
|
+
adoptLegacySession(session);
|
|
1757
|
+
return session;
|
|
1758
|
+
};
|
|
1759
|
+
var loginWithGoogleMitra = async (options) => {
|
|
1760
|
+
const session = await legacyLoginWithGoogleMitra(options);
|
|
1761
|
+
adoptLegacySession(session);
|
|
1762
|
+
return session;
|
|
1763
|
+
};
|
|
1764
|
+
var loginWithMicrosoftMitra = async (options) => {
|
|
1765
|
+
const session = await legacyLoginWithMicrosoftMitra(options);
|
|
1766
|
+
adoptLegacySession(session);
|
|
1767
|
+
return session;
|
|
1768
|
+
};
|
|
1769
|
+
var exchangeSsoCodeMitra = async (options) => {
|
|
1770
|
+
const session = await legacyExchangeSsoCodeMitra(options);
|
|
1771
|
+
adoptLegacySession(session);
|
|
1772
|
+
return session;
|
|
1773
|
+
};
|
|
1774
|
+
export {
|
|
724
1775
|
MitraApiError,
|
|
725
|
-
|
|
726
|
-
|
|
1776
|
+
callIntegrationMitra,
|
|
1777
|
+
configureSdkMitra2 as configureSdkMitra,
|
|
1778
|
+
createClient,
|
|
1779
|
+
createMitraInstance,
|
|
1780
|
+
createRecordMitra,
|
|
1781
|
+
createRecordsBatchMitra,
|
|
1782
|
+
deleteRecordMitra,
|
|
1783
|
+
exchangeSsoCodeMitra,
|
|
1784
|
+
executePublicServerFunctionAsyncMitra,
|
|
1785
|
+
executePublicServerFunctionMitra,
|
|
1786
|
+
executeServerFunctionAsyncMitra,
|
|
1787
|
+
executeServerFunctionMitra,
|
|
1788
|
+
getAgentTaskMitra,
|
|
1789
|
+
getConfig2 as getConfig,
|
|
1790
|
+
getPublicServerFunctionExecutionMitra,
|
|
1791
|
+
getRecordMitra,
|
|
1792
|
+
listIntegrationsMitra,
|
|
1793
|
+
listRecordsMitra,
|
|
1794
|
+
loginMitra,
|
|
1795
|
+
loginWithGoogleMitra,
|
|
1796
|
+
loginWithMicrosoftMitra,
|
|
1797
|
+
manageAgentChatMitra,
|
|
1798
|
+
manageAgentCredentialMitra,
|
|
1799
|
+
patchRecordMitra,
|
|
1800
|
+
refreshTokenSilently,
|
|
1801
|
+
resolveProjectId,
|
|
1802
|
+
stopServerFunctionExecutionMitra,
|
|
1803
|
+
updateRecordMitra
|
|
1804
|
+
};
|