@mitralab.io/platform-sdk 1.1.2 → 1.1.4-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/dist/index.js CHANGED
@@ -384,16 +384,37 @@ async function resolveApiKeySession(publicClient, appId, apiKey) {
384
384
  );
385
385
  }
386
386
 
387
- // src/modules/google-auth.ts
387
+ // src/modules/auth-page-flow.ts
388
388
  import { expectObject } from "@mitralab.io/sdk-core";
389
389
  var RESULT_TYPE = "mitra-oauth-result";
390
- var PROVIDER_LABELS = {
391
- google: "Google",
392
- microsoft: "Microsoft"
390
+ var FIVE_MINUTES_MS = 5 * 60 * 1e3;
391
+ var TEN_MINUTES_MS = 10 * 60 * 1e3;
392
+ var PROVIDERS = {
393
+ google: {
394
+ label: "Google",
395
+ exchangePath: "/api/v1/auth/google",
396
+ sendsRedirectUri: true,
397
+ redirectStorage: "sessionStorage",
398
+ popupTimeoutMs: FIVE_MINUTES_MS
399
+ },
400
+ microsoft: {
401
+ label: "Microsoft",
402
+ exchangePath: "/api/v1/auth/microsoft",
403
+ sendsRedirectUri: true,
404
+ redirectStorage: "sessionStorage",
405
+ popupTimeoutMs: FIVE_MINUTES_MS
406
+ },
407
+ email: {
408
+ label: "Email",
409
+ exchangePath: "/api/v1/auth/magic-link/exchange",
410
+ sendsRedirectUri: false,
411
+ redirectStorage: "localStorage",
412
+ pendingRequestTtlMs: TEN_MINUTES_MS,
413
+ popupTimeoutMs: TEN_MINUTES_MS
414
+ }
393
415
  };
394
416
  var POPUP_WIDTH = 480;
395
417
  var POPUP_HEIGHT = 600;
396
- var POPUP_TIMEOUT_MS = 5 * 60 * 1e3;
397
418
  var POPUP_CLOSED_POLL_MS = 500;
398
419
  function expectAuthTokenResponse(value) {
399
420
  const response = expectObject(
@@ -414,12 +435,13 @@ function expectAuthTokenResponse(value) {
414
435
  tokenType: response.tokenType
415
436
  };
416
437
  }
417
- var GoogleAuthFlow = class {
438
+ var AuthPageFlow = class {
418
439
  appId;
419
440
  apiUrl;
420
441
  configuredAuthPageUrl;
421
442
  client;
422
443
  provider;
444
+ profile;
423
445
  providerLabel;
424
446
  redirectStorageKey;
425
447
  popupPromise = null;
@@ -429,7 +451,8 @@ var GoogleAuthFlow = class {
429
451
  this.configuredAuthPageUrl = config.authPageUrl;
430
452
  this.client = config.client;
431
453
  this.provider = config.provider ?? "google";
432
- this.providerLabel = PROVIDER_LABELS[this.provider];
454
+ this.profile = PROVIDERS[this.provider];
455
+ this.providerLabel = this.profile.label;
433
456
  this.redirectStorageKey = `mitra_${this.provider}_redirect_${config.appId}`;
434
457
  }
435
458
  signIn(options = {}) {
@@ -446,6 +469,19 @@ var GoogleAuthFlow = class {
446
469
  });
447
470
  return this.popupPromise;
448
471
  }
472
+ /**
473
+ * Finishes a redirect this provider started, or returns `null` when the URL
474
+ * carries no result or carries one that belongs to another provider's flow.
475
+ *
476
+ * The state generated at the start of every flow is prefixed with the provider
477
+ * name, and the auth page echoes it verbatim, so a fragment identifies its own
478
+ * flow. An application that offers several methods can call every completion
479
+ * at startup, in any order, even while other methods have requests pending.
480
+ *
481
+ * A fragment of this flow is always consumed, including when it cannot be
482
+ * completed, so a failure is reported once instead of on every reload. A
483
+ * fragment of another flow is left exactly as it was found.
484
+ */
449
485
  async completeRedirect() {
450
486
  const browserWindow = this.requireBrowser();
451
487
  const params = new URLSearchParams(browserWindow.location.hash.replace(/^#/, ""));
@@ -454,17 +490,36 @@ var GoogleAuthFlow = class {
454
490
  const error = params.get("errorMitra");
455
491
  if (code === null && state === null && error === null) return null;
456
492
  const context = this.readRedirectContext(browserWindow);
457
- if (!state?.trim()) {
458
- throw new Error(`${this.providerLabel} sign-in redirect is missing state.`);
493
+ const hasState = state !== null && state.trim() !== "";
494
+ if (hasState && !this.ownsState(state)) return null;
495
+ if (!hasState && !context) return null;
496
+ if (!hasState) {
497
+ throw this.discardOwnRedirect(
498
+ browserWindow,
499
+ `${this.providerLabel} sign-in redirect is missing state.`
500
+ );
501
+ }
502
+ if (context && this.hasExpired(context)) {
503
+ this.clearRedirectContext(browserWindow);
504
+ throw this.discardOwnRedirect(
505
+ browserWindow,
506
+ `${this.providerLabel} sign-in request expired before it was completed.`
507
+ );
459
508
  }
460
509
  if (context?.state !== state) {
461
- throw new Error(`Invalid ${this.providerLabel} sign-in state (possible CSRF).`);
510
+ throw this.discardOwnRedirect(
511
+ browserWindow,
512
+ `Invalid ${this.providerLabel} sign-in state (possible CSRF).`
513
+ );
462
514
  }
463
515
  const expectedRedirectUri = this.getRedirectUri(
464
516
  resolveAuthPageUrl(this.apiUrl, this.configuredAuthPageUrl, browserWindow)
465
517
  );
466
518
  if (context.redirectUri !== expectedRedirectUri) {
467
- throw new Error(`${this.providerLabel} sign-in redirect context is invalid.`);
519
+ throw this.discardOwnRedirect(
520
+ browserWindow,
521
+ `${this.providerLabel} sign-in redirect context is invalid.`
522
+ );
468
523
  }
469
524
  this.cleanRedirectFragment(browserWindow);
470
525
  this.clearRedirectContext(browserWindow);
@@ -483,8 +538,13 @@ var GoogleAuthFlow = class {
483
538
  this.configuredAuthPageUrl,
484
539
  browserWindow
485
540
  );
541
+ const completedInAnotherTab = this.profile.pendingRequestTtlMs !== void 0;
542
+ if (completedInAnotherTab) {
543
+ this.writeRedirectContext(browserWindow, this.newRedirectContext(state, authPageUrl));
544
+ }
486
545
  const popup = this.openPopup(browserWindow, this.buildStartUrl(browserWindow, authPageUrl, state));
487
546
  const result = await this.waitForPopupResult(browserWindow, popup, authPageUrl.origin, state);
547
+ if (completedInAnotherTab) this.clearRedirectContext(browserWindow);
488
548
  if (result.code) return this.exchangeCode(result.code, this.getRedirectUri(authPageUrl));
489
549
  return expectAuthTokenResponse(result.token);
490
550
  }
@@ -495,20 +555,20 @@ var GoogleAuthFlow = class {
495
555
  this.configuredAuthPageUrl,
496
556
  browserWindow
497
557
  );
498
- const context = {
499
- state,
500
- redirectUri: this.getRedirectUri(authPageUrl)
501
- };
502
- this.persistRedirectContext(browserWindow, context);
558
+ if (!this.writeRedirectContext(browserWindow, this.newRedirectContext(state, authPageUrl))) {
559
+ throw new Error(
560
+ `${this.providerLabel} sign-in redirect requires ${this.profile.redirectStorage}.`
561
+ );
562
+ }
503
563
  const startUrl = this.buildStartUrl(browserWindow, authPageUrl, state);
504
564
  browserWindow.location.assign(startUrl.toString());
505
565
  return new Promise(() => void 0);
506
566
  }
507
567
  async exchangeCode(code, redirectUri) {
508
- const response = await this.client.post(`/api/v1/auth/${this.provider}`, {
568
+ const response = await this.client.post(this.profile.exchangePath, {
509
569
  appId: this.appId,
510
570
  code,
511
- redirectUri
571
+ ...this.profile.sendsRedirectUri ? { redirectUri } : {}
512
572
  });
513
573
  return expectAuthTokenResponse(response);
514
574
  }
@@ -525,13 +585,41 @@ var GoogleAuthFlow = class {
525
585
  getRedirectUri(authPageUrl) {
526
586
  return `${authPageUrl.origin}${authPageUrl.pathname}`;
527
587
  }
588
+ newRedirectContext(state, authPageUrl) {
589
+ return {
590
+ state,
591
+ redirectUri: this.getRedirectUri(authPageUrl),
592
+ createdAt: Date.now()
593
+ };
594
+ }
595
+ /**
596
+ * Reports a fragment of this flow that cannot be completed, dropping it from the
597
+ * URL first. Nobody else claims a fragment that names this flow, so leaving it
598
+ * there would make the application fail again on every reload.
599
+ */
600
+ discardOwnRedirect(browserWindow, message) {
601
+ this.cleanRedirectFragment(browserWindow);
602
+ return new Error(message);
603
+ }
604
+ /** Whether a state echoed by the auth page was generated by this provider's flow. */
605
+ ownsState(state) {
606
+ return state.startsWith(`${this.provider}.`);
607
+ }
608
+ hasExpired(context) {
609
+ const ttlMs = this.profile.pendingRequestTtlMs;
610
+ if (ttlMs === void 0) return false;
611
+ if (!Number.isFinite(context.createdAt)) return true;
612
+ return Date.now() - context.createdAt > ttlMs;
613
+ }
614
+ /** A one-time state that names the flow that created it, so its fragment is recognizable. */
528
615
  generateState() {
529
616
  if (!globalThis.crypto?.getRandomValues) {
530
617
  throw new Error(`${this.providerLabel} sign-in requires crypto.getRandomValues.`);
531
618
  }
532
619
  const bytes = new Uint8Array(16);
533
620
  globalThis.crypto.getRandomValues(bytes);
534
- return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
621
+ const random = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
622
+ return `${this.provider}.${random}`;
535
623
  }
536
624
  openPopup(browserWindow, url) {
537
625
  const outerWidth = browserWindow.outerWidth || browserWindow.screen.width;
@@ -540,7 +628,7 @@ var GoogleAuthFlow = class {
540
628
  const top = Math.max(0, (browserWindow.screenY || 0) + (outerHeight - POPUP_HEIGHT) / 2);
541
629
  const popup = browserWindow.open(
542
630
  url.toString(),
543
- "mitra-google-oauth",
631
+ `mitra-${this.provider}-auth`,
544
632
  `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},menubar=no,toolbar=no,status=no`
545
633
  );
546
634
  if (!popup) {
@@ -553,7 +641,7 @@ var GoogleAuthFlow = class {
553
641
  const timeout = globalThis.setTimeout(() => {
554
642
  cleanup();
555
643
  reject(new Error(`${this.providerLabel} sign-in timed out.`));
556
- }, POPUP_TIMEOUT_MS);
644
+ }, this.profile.popupTimeoutMs);
557
645
  const closedPoll = globalThis.setInterval(() => {
558
646
  if (popup.closed) {
559
647
  cleanup();
@@ -578,7 +666,7 @@ var GoogleAuthFlow = class {
578
666
  const code = typeof data.code === "string" && data.code.trim() ? data.code : void 0;
579
667
  if (!code && data.token === void 0) {
580
668
  cleanup();
581
- reject(new Error("Google auth page returned neither code nor token."));
669
+ reject(new Error(`${this.providerLabel} auth page returned neither code nor token.`));
582
670
  return;
583
671
  }
584
672
  cleanup();
@@ -593,22 +681,28 @@ var GoogleAuthFlow = class {
593
681
  browserWindow.addEventListener("message", onMessage);
594
682
  });
595
683
  }
596
- persistRedirectContext(browserWindow, context) {
684
+ /** Writes the pending request, reporting whether storage accepted it. */
685
+ writeRedirectContext(browserWindow, context) {
597
686
  try {
598
- browserWindow.sessionStorage.setItem(this.redirectStorageKey, JSON.stringify(context));
687
+ browserWindow[this.profile.redirectStorage].setItem(
688
+ this.redirectStorageKey,
689
+ JSON.stringify(context)
690
+ );
691
+ return true;
599
692
  } catch {
600
- throw new Error(`${this.providerLabel} sign-in redirect requires sessionStorage.`);
693
+ return false;
601
694
  }
602
695
  }
603
696
  readRedirectContext(browserWindow) {
604
697
  try {
605
- const raw = browserWindow.sessionStorage.getItem(this.redirectStorageKey);
698
+ const raw = browserWindow[this.profile.redirectStorage].getItem(this.redirectStorageKey);
606
699
  if (!raw) return null;
607
700
  const value = JSON.parse(raw);
608
701
  if (typeof value.state !== "string" || typeof value.redirectUri !== "string") return null;
609
702
  return {
610
703
  state: value.state,
611
- redirectUri: value.redirectUri
704
+ redirectUri: value.redirectUri,
705
+ createdAt: typeof value.createdAt === "number" ? value.createdAt : Number.NaN
612
706
  };
613
707
  } catch {
614
708
  return null;
@@ -616,7 +710,7 @@ var GoogleAuthFlow = class {
616
710
  }
617
711
  clearRedirectContext(browserWindow) {
618
712
  try {
619
- browserWindow.sessionStorage.removeItem(this.redirectStorageKey);
713
+ browserWindow[this.profile.redirectStorage].removeItem(this.redirectStorageKey);
620
714
  } catch {
621
715
  }
622
716
  }
@@ -686,6 +780,7 @@ var AuthModule = class {
686
780
  currentUserApi;
687
781
  googleAuth;
688
782
  microsoftAuth;
783
+ emailAuth;
689
784
  constructor(appId, iamBaseUrl, options = {}) {
690
785
  this.appId = appId;
691
786
  const trimmedIamBaseUrl = stripTrailingSlashes(iamBaseUrl);
@@ -702,19 +797,26 @@ var AuthModule = class {
702
797
  onUnauthorized: (requestToken) => this.handleUnauthorized(requestToken)
703
798
  });
704
799
  this.currentUserApi = createAuthModule(this.authedClient, coreErrors);
705
- this.googleAuth = new GoogleAuthFlow({
800
+ this.googleAuth = new AuthPageFlow({
706
801
  appId,
707
802
  apiUrl,
708
803
  authPageUrl: options.authPageUrl,
709
804
  client: this.publicClient
710
805
  });
711
- this.microsoftAuth = new GoogleAuthFlow({
806
+ this.microsoftAuth = new AuthPageFlow({
712
807
  appId,
713
808
  apiUrl,
714
809
  authPageUrl: options.authPageUrl,
715
810
  client: this.publicClient,
716
811
  provider: "microsoft"
717
812
  });
813
+ this.emailAuth = new AuthPageFlow({
814
+ appId,
815
+ apiUrl,
816
+ authPageUrl: options.authPageUrl,
817
+ client: this.publicClient,
818
+ provider: "email"
819
+ });
718
820
  this.loadFromStorage();
719
821
  const readAccessToken = () => this.#accessToken;
720
822
  sessionPorts.set(this, {
@@ -740,10 +842,10 @@ var AuthModule = class {
740
842
  get isAuthenticated() {
741
843
  return this._currentUser !== null && this.#accessToken !== null;
742
844
  }
743
- /** @deprecated Email/password authentication is not implemented by IAM. Use Google or Microsoft SSO. */
845
+ /** @deprecated Email/password authentication is not implemented by IAM. Use signInWithEmail() or SSO. */
744
846
  async signIn(_credentials) {
745
847
  throw new MitraApiError(
746
- "Email/password authentication is not available. Use signInWithGoogle() or signInWithMicrosoft().",
848
+ "Email/password authentication is not available. Use signInWithEmail(), signInWithGoogle() or signInWithMicrosoft().",
747
849
  0,
748
850
  "UNSUPPORTED_AUTH_METHOD"
749
851
  );
@@ -829,8 +931,10 @@ var AuthModule = class {
829
931
  *
830
932
  * The method consumes and clears the fragment and stored CSRF context, sends
831
933
  * the single-use code directly to IAM, persists both tokens, calls `auth.me()`,
832
- * and notifies auth-state listeners. It returns `null` when the current URL is
833
- * not a Google SSO redirect. Redirect errors must carry the same `stateMitra`
934
+ * and notifies auth-state listeners. It returns `null` when the current URL
935
+ * carries no redirect result, and also when it carries one this flow never
936
+ * started, so an application that offers several methods can call every
937
+ * completion at startup. Redirect errors must carry the same `stateMitra`
834
938
  * stored at the start of the flow before their message is exposed or consumed.
835
939
  *
836
940
  * @returns The authenticated user, or `null` when no redirect result is present.
@@ -866,10 +970,54 @@ var AuthModule = class {
866
970
  const tokenResponse = await this.microsoftAuth.completeRedirect();
867
971
  return tokenResponse ? this.establishSession(tokenResponse) : null;
868
972
  }
869
- /** @deprecated Email/password registration is not implemented by IAM. Use Google or Microsoft SSO. */
973
+ /**
974
+ * Signs in with a one-time code sent by email: the same auth-page handshake as
975
+ * SSO, where the platform page collects the address and the code, and IAM
976
+ * hands back a single-use exchange code redeemed at `/auth/magic-link/exchange`.
977
+ * Popup by default; redirect mode navigates away.
978
+ *
979
+ * The message also carries a link. Because that link opens a new tab, the
980
+ * pending request is kept in `localStorage` for 10 minutes - the one-time state
981
+ * and the auth page URL, never a token - so
982
+ * {@link completeEmailSignInRedirect} can finish the flow in that tab.
983
+ *
984
+ * @param options - Popup or redirect mode.
985
+ * @returns The authenticated and hydrated user in popup mode.
986
+ * @throws {MitraApiError} When IAM rejects the exchange code.
987
+ * @throws {Error} When the browser blocks or cancels the popup, the flow times
988
+ * out, or the response fails origin, source, state, or shape validation. A
989
+ * person who finishes through the link instead of the popup leaves this call
990
+ * to time out, which the application should treat as a cancelled popup.
991
+ *
992
+ * @example
993
+ * ```typescript
994
+ * const user = await mitra.auth.signInWithEmail();
995
+ * ```
996
+ */
997
+ async signInWithEmail(options = {}) {
998
+ return this.establishSession(await this.emailAuth.signIn(options));
999
+ }
1000
+ /**
1001
+ * Completes an email sign-in from `#codeMitra` and `#stateMitra`, during
1002
+ * application startup like {@link completeGoogleSignInRedirect}.
1003
+ *
1004
+ * It covers both ways the flow comes back, and they are the same check: a
1005
+ * redirect in the tab that started it, and the tab the link in the message
1006
+ * opened. Both carry the one-time state this SDK generated, which the second
1007
+ * tab matches against the pending request kept in `localStorage`. A request
1008
+ * older than 10 minutes is discarded instead of completed.
1009
+ *
1010
+ * @returns The authenticated user, or `null` when the URL carries no result or
1011
+ * carries one that belongs to another sign-in method's flow.
1012
+ */
1013
+ async completeEmailSignInRedirect() {
1014
+ const tokenResponse = await this.emailAuth.completeRedirect();
1015
+ return tokenResponse ? this.establishSession(tokenResponse) : null;
1016
+ }
1017
+ /** @deprecated Email/password registration is not implemented by IAM. Use signInWithEmail() or SSO. */
870
1018
  async signUp(_data) {
871
1019
  throw new MitraApiError(
872
- "Email/password registration is not available. Use signInWithGoogle() or signInWithMicrosoft().",
1020
+ "Email/password registration is not available. Use signInWithEmail(), signInWithGoogle() or signInWithMicrosoft().",
873
1021
  0,
874
1022
  "UNSUPPORTED_AUTH_METHOD"
875
1023
  );
@@ -1623,11 +1771,10 @@ function holdSendsWhileOffline(session, outbox) {
1623
1771
 
1624
1772
  // src/modules/agent-session.ts
1625
1773
  var CONNECT_TIMEOUT_MS = 15e3;
1626
- var CHANNEL_BOOT_TIMEOUT_MS = 9e4;
1627
- var CHANNEL_BOOT_RETRY_MS = 2e3;
1628
1774
  var SILENCE_TIMEOUT_MS = 6e4;
1629
1775
  var RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 16e3];
1630
1776
  var SUPERSEDED_CLOSE_CODE = 4409;
1777
+ var SOCKET_OPEN = 1;
1631
1778
  var SilenceWatchdog = class {
1632
1779
  constructor(onSilence) {
1633
1780
  this.onSilence = onSilence;
@@ -1743,6 +1890,8 @@ var BrowserAgentTaskEventSource = class {
1743
1890
  */
1744
1891
  boxCursors = /* @__PURE__ */ new Map();
1745
1892
  boxAddresses = /* @__PURE__ */ new Map();
1893
+ /** The box socket a task is talking on right now; absent while it is lost or being redialed. */
1894
+ directSockets = /* @__PURE__ */ new Map();
1746
1895
  async open(taskId, observer, signal, transport = "auto") {
1747
1896
  if (transport === "http") return this.openSse(taskId, observer, signal);
1748
1897
  if (transport === "websocket") return this.openWebSocket(taskId, observer, signal);
@@ -1774,6 +1923,23 @@ var BrowserAgentTaskEventSource = class {
1774
1923
  }
1775
1924
  };
1776
1925
  }
1926
+ /**
1927
+ * Writes a message or an interrupt on the box socket when the task is on the direct channel
1928
+ * and that socket is open right now. The box accepts the core's input as its own inbound
1929
+ * frame, asks the copilot for admission itself and answers on this same socket with the
1930
+ * frames the observer already reads, so the copilot's host socket is off the message path.
1931
+ * False sends the caller to REST: no direct channel, a socket lost or mid-redial, or an
1932
+ * approval, which stays on REST as it is.
1933
+ *
1934
+ * A frame written on a socket that closes before the box acknowledges it is not sent again
1935
+ * over REST: the box may have admitted the turn already, and a second copy would start it
1936
+ * twice. The redial replays the box log, which is the same recovery a REST 202 gets when its
1937
+ * turn is lost.
1938
+ */
1939
+ sendOnChannel(taskId, input) {
1940
+ if (input.type === "approval_response") return false;
1941
+ return this.directSockets.get(taskId)?.send(JSON.stringify(input)) ?? false;
1942
+ }
1777
1943
  async requireFreshToken() {
1778
1944
  const fresh = await this.auth.ensureFreshSession();
1779
1945
  const token = this.auth.accessToken;
@@ -1790,28 +1956,28 @@ var BrowserAgentTaskEventSource = class {
1790
1956
  requestDirectChannel(taskId, token, signal) {
1791
1957
  return this.askDirectChannel(taskId, token, signal).catch(() => null);
1792
1958
  }
1793
- /** Like `requestDirectChannel`, but a request the network lost is thrown, not a refusal. */
1959
+ /**
1960
+ * Like `requestDirectChannel`, but a request the network lost is thrown, not a refusal.
1961
+ *
1962
+ * One request. The copilot holds it while the box boots and answers 200 with the channel, or
1963
+ * an error status once the box cannot be had. A 202 comes only from a copilot older than that
1964
+ * contract, which used to mean "still booting, ask again": there is no channel to open, and
1965
+ * the conversation follows the copilot socket. Polling here again would put the wait back on
1966
+ * the client that the server now owns.
1967
+ */
1794
1968
  async askDirectChannel(taskId, token, signal) {
1969
+ if (signal?.aborted) return null;
1795
1970
  const url = `${this.apiUrl}/copilot/api/v1/tasks/${encodeURIComponent(taskId)}/channel`;
1796
- const deadline = Date.now() + CHANNEL_BOOT_TIMEOUT_MS;
1797
- for (; ; ) {
1798
- if (signal?.aborted) return null;
1799
- const response = await globalThis.fetch(url, {
1800
- method: "POST",
1801
- headers: { Authorization: `Bearer ${stripBearer2(token)}` },
1802
- ...signal ? { signal } : {}
1803
- });
1804
- if (response.status === 202) {
1805
- if (Date.now() >= deadline) return null;
1806
- await sleep(CHANNEL_BOOT_RETRY_MS, signal);
1807
- continue;
1808
- }
1809
- if (!response.ok) return null;
1810
- try {
1811
- return toDirectChannel(await response.json(), this.apiUrl);
1812
- } catch {
1813
- return null;
1814
- }
1971
+ const response = await globalThis.fetch(url, {
1972
+ method: "POST",
1973
+ headers: { Authorization: `Bearer ${stripBearer2(token)}` },
1974
+ ...signal ? { signal } : {}
1975
+ });
1976
+ if (response.status === 202 || !response.ok) return null;
1977
+ try {
1978
+ return toDirectChannel(await response.json(), this.apiUrl);
1979
+ } catch {
1980
+ return null;
1815
1981
  }
1816
1982
  }
1817
1983
  async openWebSocket(taskId, observer, signal) {
@@ -1860,6 +2026,11 @@ var BrowserAgentTaskEventSource = class {
1860
2026
  signal?.addEventListener("abort", onAbort, { once: true });
1861
2027
  let inTurn = false;
1862
2028
  let current = null;
2029
+ const track = (socket) => {
2030
+ if (socket) this.directSockets.set(taskId, socket);
2031
+ else if (current && this.directSockets.get(taskId) === current) this.directSockets.delete(taskId);
2032
+ current = socket;
2033
+ };
1863
2034
  const onFrame = (event) => {
1864
2035
  if (typeof event.sequence === "number") {
1865
2036
  const seen = this.boxCursors.get(taskId) ?? 0;
@@ -1869,22 +2040,21 @@ var BrowserAgentTaskEventSource = class {
1869
2040
  observer.onEvent(event);
1870
2041
  };
1871
2042
  const onLost = (error, code) => {
1872
- current = null;
2043
+ track(null);
1873
2044
  if (link.signal.aborted) return;
1874
2045
  if (!inTurn || code === SUPERSEDED_CLOSE_CODE) {
1875
2046
  observer.onDisconnect(error);
1876
2047
  return;
1877
2048
  }
1878
- void this.redial(taskId, error, link.signal, observer, { onFrame, onLost }).then((socket) => {
1879
- current = socket;
1880
- });
2049
+ void this.redial(taskId, error, link.signal, observer, { onFrame, onLost }).then(track);
1881
2050
  };
1882
- current = await this.dial(channel.wsUrl, { signal: link.signal, onFrame, onLost });
2051
+ track(await this.dial(channel.wsUrl, { signal: link.signal, onFrame, onLost }));
1883
2052
  return {
1884
2053
  close: () => {
1885
2054
  signal?.removeEventListener("abort", onAbort);
1886
2055
  link.abort();
1887
2056
  current?.close();
2057
+ track(null);
1888
2058
  }
1889
2059
  };
1890
2060
  }
@@ -1969,6 +2139,15 @@ var BrowserAgentTaskEventSource = class {
1969
2139
  removeAbortListener();
1970
2140
  socket.close(1e3, "Client closed");
1971
2141
  };
2142
+ const send = (frame) => {
2143
+ if (intentionalClose || socket.readyState !== SOCKET_OPEN) return false;
2144
+ try {
2145
+ socket.send(frame);
2146
+ return true;
2147
+ } catch {
2148
+ return false;
2149
+ }
2150
+ };
1972
2151
  const onAbort = () => {
1973
2152
  if (!opened) {
1974
2153
  rejectHandshake(signal?.reason instanceof Error ? signal.reason : new Error("Agent WebSocket connection aborted."));
@@ -1992,7 +2171,7 @@ var BrowserAgentTaskEventSource = class {
1992
2171
  } catch {
1993
2172
  }
1994
2173
  }
1995
- resolve({ close });
2174
+ resolve({ close, send });
1996
2175
  };
1997
2176
  socket.onerror = () => {
1998
2177
  if (!opened) rejectHandshake(new Error("Failed to connect to the Agent WebSocket."));
@@ -2099,6 +2278,10 @@ var BrowserAgentTaskEventSource = class {
2099
2278
  };
2100
2279
 
2101
2280
  // src/modules/agent-tasks.ts
2281
+ function bornOnBox(options) {
2282
+ if (!("create" in options) || options.runtime || options.transport === "http") return options;
2283
+ return { ...options, runtime: "T3" };
2284
+ }
2102
2285
  function createBrowserAgentTasksModule(httpClient, auth, apiUrl) {
2103
2286
  const tasks = createAgentTasksModule(httpClient, coreErrors);
2104
2287
  const source = new BrowserAgentTaskEventSource(auth, apiUrl);
@@ -2124,11 +2307,14 @@ function createBrowserAgentTasksModule(httpClient, auth, apiUrl) {
2124
2307
  }
2125
2308
  };
2126
2309
  const manager = createAgentTaskSessionManager({
2127
- tasks: { ...tasks, sendInput: (taskId, input) => outbox.sendInput(taskId, input) },
2310
+ tasks: {
2311
+ ...tasks,
2312
+ sendInput: (taskId, input) => source.sendOnChannel(taskId, input) ? Promise.resolve() : outbox.sendInput(taskId, input)
2313
+ },
2128
2314
  eventSource
2129
2315
  });
2130
2316
  return withAgentTaskSessions(tasks, {
2131
- session: (options) => holdSendsWhileOffline(manager.session(options), outbox)
2317
+ session: (options) => holdSendsWhileOffline(manager.session(bornOnBox(options)), outbox)
2132
2318
  });
2133
2319
  }
2134
2320
 
@@ -2192,7 +2378,12 @@ function expectAppInfoResponse(value) {
2192
2378
  }
2193
2379
  return {
2194
2380
  dataSourceId: response.dataSourceId,
2195
- allowSignup: response.allowSignup
2381
+ allowSignup: response.allowSignup,
2382
+ // Unlike the fields above, a missing or non-boolean value is read as `false`
2383
+ // instead of failing `init()`: a Code Studio older than the email login gate
2384
+ // answers without this field, and an app that cannot prove it is enabled
2385
+ // should stop offering the option, not stop starting.
2386
+ emailLoginEnabled: response.emailLoginEnabled === true
2196
2387
  };
2197
2388
  }
2198
2389
  function createClient(config) {
@@ -2260,6 +2451,7 @@ function createClient(config) {
2260
2451
  legacyBridge.connect();
2261
2452
  let initialized = false;
2262
2453
  let allowSignup = true;
2454
+ let emailLoginEnabled = false;
2263
2455
  async function init() {
2264
2456
  if (initialized) return;
2265
2457
  const publicClient = new HttpClient({
@@ -2275,6 +2467,7 @@ function createClient(config) {
2275
2467
  entitiesModule.setDataSourceId(appInfo.dataSourceId);
2276
2468
  }
2277
2469
  allowSignup = appInfo.allowSignup;
2470
+ emailLoginEnabled = appInfo.emailLoginEnabled;
2278
2471
  initialized = true;
2279
2472
  }
2280
2473
  return {
@@ -2290,6 +2483,9 @@ function createClient(config) {
2290
2483
  get allowSignup() {
2291
2484
  return allowSignup;
2292
2485
  },
2486
+ get emailLoginEnabled() {
2487
+ return emailLoginEnabled;
2488
+ },
2293
2489
  config
2294
2490
  };
2295
2491
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mitralab.io/platform-sdk",
3
- "version": "1.1.2",
3
+ "version": "1.1.4-beta.0",
4
4
  "description": "JavaScript/TypeScript SDK for building apps on the Mitra Platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -62,7 +62,7 @@
62
62
  "url": "https://github.com/mitralab-dev/mitra-platform-sdk/issues"
63
63
  },
64
64
  "dependencies": {
65
- "@mitralab.io/sdk-core": "0.2.2",
65
+ "@mitralab.io/sdk-core": "0.2.4",
66
66
  "mitra-interactions-sdk": "1.0.60-beta.48"
67
67
  },
68
68
  "devDependencies": {