@mitralab.io/platform-sdk 1.0.9 → 1.1.0-beta.1

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/index.js CHANGED
@@ -1,8 +1,23 @@
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
+ }
7
+
1
8
  // src/client.ts
2
- import { encodePathSegment, expectObject } from "@mitralab.io/sdk-core";
9
+ import {
10
+ createPublicFunctionsModule,
11
+ encodePathSegment,
12
+ expectObject as expectObject2
13
+ } from "@mitralab.io/sdk-core";
3
14
 
4
15
  // src/utils/http-client.ts
5
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
+ }
6
21
  function redactText(value, currentToken) {
7
22
  const withoutBearerCredentials = value.replace(bearerCredentialPattern, "$1[REDACTED]");
8
23
  return currentToken ? withoutBearerCredentials.split(currentToken).join("[REDACTED]") : withoutBearerCredentials;
@@ -12,10 +27,13 @@ function redactDetails(value, currentToken) {
12
27
  if (Array.isArray(value)) return value.map((item) => redactDetails(item, currentToken));
13
28
  if (value && typeof value === "object") {
14
29
  return Object.fromEntries(
15
- Object.entries(value).map(([key, entry]) => [
16
- redactText(key, currentToken),
17
- redactDetails(entry, currentToken)
18
- ])
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
+ })
19
37
  );
20
38
  }
21
39
  return value;
@@ -40,12 +58,14 @@ function buildRequestUrl(baseUrl, path, params) {
40
58
  var HttpClient = class {
41
59
  baseUrl;
42
60
  tokenGetter;
61
+ beforeAuthenticatedRequest;
43
62
  onUnauthorized;
44
63
  onError;
45
64
  defaultHeaders;
46
65
  constructor(config) {
47
66
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
48
67
  this.tokenGetter = config.getToken ?? (() => null);
68
+ this.beforeAuthenticatedRequest = config.beforeAuthenticatedRequest;
49
69
  this.onUnauthorized = config.onUnauthorized;
50
70
  this.onError = config.onError;
51
71
  this.defaultHeaders = config.defaultHeaders ?? {};
@@ -76,6 +96,9 @@ var HttpClient = class {
76
96
  async request(path, options = {}) {
77
97
  const { method = "GET", body, headers = {}, params, isRetry } = options;
78
98
  const url = buildRequestUrl(this.baseUrl, path, params);
99
+ if (!isRetry && this.tokenGetter() && this.beforeAuthenticatedRequest) {
100
+ await this.beforeAuthenticatedRequest();
101
+ }
79
102
  const requestHeaders = {
80
103
  "Content-Type": "application/json",
81
104
  ...this.defaultHeaders,
@@ -102,27 +125,50 @@ var HttpClient = class {
102
125
  }
103
126
  if (!response.ok) {
104
127
  if (response.status === 401 && !isRetry && this.onUnauthorized) {
105
- const refreshed = await this.onUnauthorized();
128
+ const refreshed = await this.onUnauthorized(token);
106
129
  if (refreshed) {
107
130
  return this.request(path, { ...options, isRetry: true });
108
131
  }
109
132
  }
110
- const errorBody = await response.json().catch(() => ({}));
111
- const errorPayload = asErrorPayload(errorBody);
112
- const rawMessage = optionalString(errorPayload.message);
113
- const rawCode = optionalString(errorPayload.error_code);
114
- const error = new MitraApiError(
115
- redactText(rawMessage || `Request failed with status ${response.status}`, token),
116
- response.status,
117
- rawCode === void 0 ? void 0 : redactText(rawCode, token),
118
- redactDetails(errorBody, token)
119
- );
133
+ const error = await this.errorFromResponse(response, token);
120
134
  this.onError?.(error);
121
135
  throw error;
122
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) {
123
152
  if (response.status === 204) {
124
153
  return void 0;
125
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
+ }
126
172
  return response.json();
127
173
  }
128
174
  /**
@@ -202,26 +248,447 @@ var coreErrors = {
202
248
  invalidResponse: (message) => new MitraApiError(message, 200, "INVALID_RESPONSE")
203
249
  };
204
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
+
205
348
  // src/modules/auth.ts
206
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
+
602
+ // src/modules/auth.ts
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
+ }
207
636
  var AuthModule = class {
208
637
  appId;
209
638
  _currentUser = null;
210
639
  #accessToken = null;
211
640
  #refreshToken = null;
212
- refreshPromise = null;
641
+ sessionGeneration = 0;
642
+ refreshFlight = null;
643
+ transientRefreshFailureGeneration = null;
213
644
  listeners = /* @__PURE__ */ new Set();
645
+ sessionListeners = /* @__PURE__ */ new Set();
214
646
  storageKey;
215
647
  publicClient;
216
648
  authedClient;
217
649
  currentUserApi;
218
- constructor(appId, iamBaseUrl) {
650
+ googleAuth;
651
+ microsoftAuth;
652
+ constructor(appId, iamBaseUrl, options = {}) {
219
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
+ );
220
658
  this.storageKey = `mitra_auth_${appId}`;
221
659
  this.publicClient = new HttpClient({ baseUrl: iamBaseUrl, getToken: () => null });
222
- this.authedClient = new HttpClient({ baseUrl: iamBaseUrl, getToken: () => this.#accessToken });
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
+ });
223
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
+ });
224
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
+ });
225
692
  }
226
693
  /** The currently authenticated user, or null. */
227
694
  get currentUser() {
@@ -235,57 +702,94 @@ var AuthModule = class {
235
702
  get isAuthenticated() {
236
703
  return this._currentUser !== null && this.#accessToken !== null;
237
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
+ }
238
713
  /**
239
- * Signs in a user with email and password.
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.
240
720
  *
241
- * On success, stores access token, refresh token, and user data.
242
- * Subsequent API requests use the token automatically.
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.
243
724
  *
244
- * @param credentials - Email and password.
245
- * @returns The authenticated user.
246
- * @throws {MitraApiError} On invalid credentials (401).
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.
247
730
  *
248
731
  * @example
249
732
  * ```typescript
250
- * const user = await mitra.auth.signIn({
251
- * email: 'user@example.com',
252
- * password: 'password123',
253
- * });
733
+ * const user = await mitra.auth.signInWithGoogle();
734
+ * ```
735
+ *
736
+ * @example
737
+ * ```typescript
738
+ * await mitra.auth.signInWithGoogle({ mode: 'redirect' });
254
739
  * ```
255
740
  */
256
- async signIn(credentials) {
257
- const tokenResponse = await this.publicClient.post(
258
- "/api/v1/auth/login",
259
- { ...credentials, appId: this.appId }
260
- );
261
- this.#accessToken = tokenResponse.accessToken;
262
- this.#refreshToken = tokenResponse.refreshToken;
263
- const user = await this.getCurrentUser();
264
- this.setAuthState(user, tokenResponse.accessToken, tokenResponse.refreshToken);
265
- return user;
741
+ async signInWithGoogle(options = {}) {
742
+ return this.establishSession(await this.googleAuth.signIn(options));
266
743
  }
267
744
  /**
268
- * Registers a new user and signs them in automatically.
745
+ * Completes a Google redirect response from `#codeMitra` and `#stateMitra`.
269
746
  *
270
- * @param data - Email, password, and optional name.
271
- * @returns The newly created and authenticated user.
272
- * @throws {MitraApiError} On duplicate email (409) or validation error (400).
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.
752
+ *
753
+ * @returns The authenticated user, or `null` when no redirect result is present.
273
754
  *
274
755
  * @example
275
756
  * ```typescript
276
- * const user = await mitra.auth.signUp({
277
- * email: 'new@example.com',
278
- * password: 'securepassword',
279
- * name: 'Jane Doe',
280
- * });
757
+ * const redirectedUser = await mitra.auth.completeGoogleSignInRedirect();
758
+ * if (redirectedUser) console.log(redirectedUser.email);
281
759
  * ```
282
760
  */
283
- async signUp(data) {
284
- await this.publicClient.post("/api/v1/auth/register", {
285
- ...data,
286
- appId: this.appId
287
- });
288
- return this.signIn({ email: data.email, password: data.password });
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
+ );
289
793
  }
290
794
  /**
291
795
  * Signs out the current user, clearing all auth state and localStorage.
@@ -307,9 +811,9 @@ var AuthModule = class {
307
811
  /**
308
812
  * Refreshes the session using the stored refresh token.
309
813
  *
310
- * Called automatically by the SDK on 401 responses. Can also be called
311
- * manually. Multiple concurrent calls are deduplicated (only one refresh
312
- * request is made).
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.
313
817
  *
314
818
  * @returns `true` if refresh succeeded, `false` otherwise.
315
819
  *
@@ -321,19 +825,50 @@ var AuthModule = class {
321
825
  */
322
826
  async refreshSession() {
323
827
  if (!this.#refreshToken) return false;
324
- if (this.refreshPromise) return this.refreshPromise;
325
- this.refreshPromise = this.doRefresh();
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;
326
842
  try {
327
- return await this.refreshPromise;
843
+ return await flight.promise;
328
844
  } finally {
329
- this.refreshPromise = null;
845
+ if (this.refreshFlight === flight) {
846
+ this.refreshFlight = null;
847
+ }
330
848
  }
331
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
+ }
332
866
  /**
333
867
  * Fetches the current user from the server and updates local state.
334
868
  *
335
- * Only clears auth state on 401 (expired/invalid token).
336
- * Transient errors (500, network) return null without clearing the session.
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.
337
872
  *
338
873
  * @returns The user if authenticated, `null` otherwise.
339
874
  *
@@ -345,14 +880,17 @@ var AuthModule = class {
345
880
  */
346
881
  async me() {
347
882
  if (!this.#accessToken) return null;
883
+ const generation = this.sessionGeneration;
348
884
  try {
349
885
  const user = await this.getCurrentUser();
886
+ if (generation !== this.sessionGeneration) return null;
350
887
  this._currentUser = user;
888
+ this.transientRefreshFailureGeneration = null;
351
889
  this.saveToStorage();
352
890
  this.notifyListeners();
353
891
  return user;
354
892
  } catch (error) {
355
- if (error instanceof MitraApiError && error.status === 401) {
893
+ if (error instanceof MitraApiError && error.status === 401 && this.#accessToken && generation === this.sessionGeneration && this.transientRefreshFailureGeneration !== this.sessionGeneration) {
356
894
  this.clearAuthState();
357
895
  }
358
896
  return null;
@@ -387,10 +925,110 @@ var AuthModule = class {
387
925
  * ```
388
926
  */
389
927
  setToken(token, saveToStorage = true) {
928
+ if (!this.belongsToConfiguredApp(token)) {
929
+ this.clearAuthState();
930
+ return;
931
+ }
932
+ this.invalidatePendingRefreshes();
390
933
  this.#accessToken = token;
391
934
  if (saveToStorage) {
392
935
  this.saveToStorage();
393
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;
394
1032
  }
395
1033
  /**
396
1034
  * Redirects to `/login?returnUrl=...` for unauthenticated users.
@@ -434,27 +1072,65 @@ var AuthModule = class {
434
1072
  this.listeners.delete(callback);
435
1073
  };
436
1074
  }
437
- async doRefresh() {
1075
+ async doRefresh(generation, refreshToken) {
1076
+ let tokenResponse;
438
1077
  try {
439
- const tokenResponse = await this.publicClient.post(
440
- "/api/v1/auth/refresh-token",
441
- { refreshToken: this.#refreshToken }
1078
+ tokenResponse = expectAuthTokenResponse(
1079
+ await this.publicClient.post(
1080
+ "/api/v1/auth/refresh-token",
1081
+ { refreshToken }
1082
+ )
442
1083
  );
443
- this.#accessToken = tokenResponse.accessToken;
444
- this.#refreshToken = tokenResponse.refreshToken;
445
- const user = await this.getCurrentUser();
446
- this.setAuthState(user, tokenResponse.accessToken, tokenResponse.refreshToken);
447
- return true;
448
- } catch {
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)) {
449
1095
  this.clearAuthState();
450
1096
  return false;
451
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
+ }
452
1127
  }
453
1128
  setAuthState(user, token, refreshToken) {
454
1129
  this._currentUser = user;
455
1130
  this.#accessToken = token;
456
1131
  this.#refreshToken = refreshToken;
457
1132
  this.saveToStorage();
1133
+ this.notifySessionListeners();
458
1134
  this.notifyListeners();
459
1135
  }
460
1136
  async getCurrentUser() {
@@ -462,11 +1138,35 @@ var AuthModule = class {
462
1138
  return { ...user, tenantId: user.tenant.id };
463
1139
  }
464
1140
  clearAuthState() {
1141
+ const hadAuthState = this._currentUser !== null || this.#accessToken !== null || this.#refreshToken !== null;
1142
+ this.invalidatePendingRefreshes();
465
1143
  this._currentUser = null;
466
1144
  this.#accessToken = null;
467
1145
  this.#refreshToken = null;
468
1146
  this.removeFromStorage();
469
- this.notifyListeners();
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
+ });
470
1170
  }
471
1171
  notifyListeners() {
472
1172
  this.listeners.forEach((callback) => {
@@ -497,9 +1197,14 @@ var AuthModule = class {
497
1197
  const stored = localStorage.getItem(this.storageKey);
498
1198
  if (stored) {
499
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
+ }
500
1204
  this._currentUser = user;
501
1205
  this.#accessToken = token;
502
1206
  this.#refreshToken = refreshToken ?? null;
1207
+ this.invalidatePendingRefreshes();
503
1208
  }
504
1209
  } catch {
505
1210
  this.removeFromStorage();
@@ -554,31 +1259,47 @@ import {
554
1259
  var FunctionsModule = class {
555
1260
  core;
556
1261
  constructor(httpClient) {
557
- this.core = createFunctionsModule(httpClient, { emptyInput: "omit-body" }, coreErrors);
1262
+ this.core = createFunctionsModule(
1263
+ httpClient,
1264
+ { emptyInput: "omit-body", executeInvocationType: "sync" },
1265
+ coreErrors
1266
+ );
558
1267
  }
559
1268
  /**
560
- * Executes a Function using the Platform SDK 1.x server-default invocation semantics.
561
- * The runtime SDK uses an explicit invocation header instead.
1269
+ * Executes a Function synchronously and waits for its terminal result.
562
1270
  */
563
1271
  async execute(functionId, input) {
564
- const execution = await this.core.execute(functionId, input);
565
- if (execution.input === null) {
566
- throw coreErrors.invalidResponse(
567
- "Function execution response has an invalid input field"
568
- );
569
- }
570
- return { ...execution, input: execution.input };
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);
571
1285
  }
572
1286
  };
573
1287
 
574
1288
  // src/modules/integration.ts
575
1289
  import {
1290
+ createIntegrationAdminModule,
576
1291
  createIntegrationModule
577
1292
  } from "@mitralab.io/sdk-core";
578
1293
  var IntegrationModule = class {
579
1294
  core;
1295
+ configs;
580
1296
  constructor(httpClient) {
581
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);
582
1303
  }
583
1304
  executeResource(resourceId, params) {
584
1305
  return this.core.executeResource(resourceId, params);
@@ -586,6 +1307,10 @@ var IntegrationModule = class {
586
1307
  execute(configId, request) {
587
1308
  return this.core.execute(configId, request);
588
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
+ }
589
1314
  };
590
1315
 
591
1316
  // src/modules/queries.ts
@@ -593,29 +1318,296 @@ import {
593
1318
  createQueriesModule
594
1319
  } from "@mitralab.io/sdk-core";
595
1320
  var QueriesModule = class {
596
- dataSourceId = "";
597
1321
  core;
598
1322
  constructor(httpClient) {
599
- this.core = createQueriesModule(httpClient, () => this.dataSourceId, coreErrors);
1323
+ this.core = createQueriesModule(httpClient, coreErrors);
600
1324
  }
601
- /** Called by `client.init()` to set the app's resolved data source. */
602
- setDataSourceId(dataSourceId) {
603
- this.dataSourceId = dataSourceId;
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) {
604
1330
  }
605
1331
  async execute(id, parameters) {
606
- const result = await this.core.execute(id, parameters);
607
- return { ...result, affectedRows: result.affectedRows ?? null };
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
+ }
608
1549
  }
609
1550
  };
610
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
+
611
1603
  // src/client.ts
612
1604
  function expectAppInfoResponse(value) {
613
- const response = expectObject(
1605
+ const response = expectObject2(
614
1606
  value,
615
1607
  "App info response",
616
1608
  coreErrors
617
1609
  );
618
- if (typeof response.dataSourceId !== "string" || !response.dataSourceId.trim()) {
1610
+ if (response.dataSourceId !== null && (typeof response.dataSourceId !== "string" || !response.dataSourceId.trim())) {
619
1611
  throw coreErrors.invalidResponse(
620
1612
  "App info response has an invalid dataSourceId field"
621
1613
  );
@@ -631,18 +1623,23 @@ function expectAppInfoResponse(value) {
631
1623
  };
632
1624
  }
633
1625
  function createClient(config) {
634
- const { appId, apiUrl, onError } = config;
635
- const iamUrl = `${apiUrl}/iam`;
636
- const dataManagerUrl = `${apiUrl}/data-manager`;
637
- const functionsUrl = `${apiUrl}/functions`;
638
- const integrationUrl = `${apiUrl}/integration`;
639
- const codeStudioUrl = `${apiUrl}/code-studio`;
640
- const authModule = new AuthModule(appId, iamUrl);
641
- const onUnauthorized = () => authModule.refreshSession();
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);
642
1638
  const defaultHeaders = { "X-App-Id": appId };
643
1639
  const httpClient = new HttpClient({
644
1640
  baseUrl: dataManagerUrl,
645
1641
  getToken: () => authModule.accessToken,
1642
+ beforeAuthenticatedRequest,
646
1643
  onUnauthorized,
647
1644
  onError,
648
1645
  defaultHeaders
@@ -651,20 +1648,43 @@ function createClient(config) {
651
1648
  const functionsHttpClient = new HttpClient({
652
1649
  baseUrl: functionsUrl,
653
1650
  getToken: () => authModule.accessToken,
1651
+ beforeAuthenticatedRequest,
654
1652
  onUnauthorized,
655
1653
  onError,
656
1654
  defaultHeaders
657
1655
  });
658
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);
659
1675
  const integrationHttpClient = new HttpClient({
660
1676
  baseUrl: integrationUrl,
661
1677
  getToken: () => authModule.accessToken,
1678
+ beforeAuthenticatedRequest,
662
1679
  onUnauthorized,
663
1680
  onError,
664
1681
  defaultHeaders
665
1682
  });
666
1683
  const integrationModule = new IntegrationModule(integrationHttpClient);
667
1684
  const queriesModule = new QueriesModule(httpClient);
1685
+ const legacyBridge = new LegacySessionBridge(authSession, appId, apiUrl, authPageUrl);
1686
+ setActiveBridge(legacyBridge);
1687
+ legacyBridge.connect();
668
1688
  let initialized = false;
669
1689
  let allowSignup = true;
670
1690
  async function init() {
@@ -678,8 +1698,9 @@ function createClient(config) {
678
1698
  `/api/v1/apps/${encodePathSegment(appId, "appId", coreErrors)}/info`
679
1699
  )
680
1700
  );
681
- entitiesModule.setDataSourceId(appInfo.dataSourceId);
682
- queriesModule.setDataSourceId(appInfo.dataSourceId);
1701
+ if (appInfo.dataSourceId) {
1702
+ entitiesModule.setDataSourceId(appInfo.dataSourceId);
1703
+ }
683
1704
  allowSignup = appInfo.allowSignup;
684
1705
  initialized = true;
685
1706
  }
@@ -688,6 +1709,9 @@ function createClient(config) {
688
1709
  auth: authModule,
689
1710
  entities: entitiesModule,
690
1711
  functions: functionsModule,
1712
+ publicFunctions: publicFunctionsModule,
1713
+ agentTasks: agentTasksModule,
1714
+ agentCredentials: agentCredentialsModule,
691
1715
  integration: integrationModule,
692
1716
  queries: queriesModule,
693
1717
  get allowSignup() {
@@ -696,7 +1720,85 @@ function createClient(config) {
696
1720
  config
697
1721
  };
698
1722
  }
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
+ };
699
1774
  export {
700
1775
  MitraApiError,
701
- createClient
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
702
1804
  };