@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.cjs CHANGED
@@ -434,16 +434,37 @@ async function resolveApiKeySession(publicClient, appId, apiKey) {
434
434
  );
435
435
  }
436
436
 
437
- // src/modules/google-auth.ts
437
+ // src/modules/auth-page-flow.ts
438
438
  var import_sdk_core2 = require("@mitralab.io/sdk-core");
439
439
  var RESULT_TYPE = "mitra-oauth-result";
440
- var PROVIDER_LABELS = {
441
- google: "Google",
442
- microsoft: "Microsoft"
440
+ var FIVE_MINUTES_MS = 5 * 60 * 1e3;
441
+ var TEN_MINUTES_MS = 10 * 60 * 1e3;
442
+ var PROVIDERS = {
443
+ google: {
444
+ label: "Google",
445
+ exchangePath: "/api/v1/auth/google",
446
+ sendsRedirectUri: true,
447
+ redirectStorage: "sessionStorage",
448
+ popupTimeoutMs: FIVE_MINUTES_MS
449
+ },
450
+ microsoft: {
451
+ label: "Microsoft",
452
+ exchangePath: "/api/v1/auth/microsoft",
453
+ sendsRedirectUri: true,
454
+ redirectStorage: "sessionStorage",
455
+ popupTimeoutMs: FIVE_MINUTES_MS
456
+ },
457
+ email: {
458
+ label: "Email",
459
+ exchangePath: "/api/v1/auth/magic-link/exchange",
460
+ sendsRedirectUri: false,
461
+ redirectStorage: "localStorage",
462
+ pendingRequestTtlMs: TEN_MINUTES_MS,
463
+ popupTimeoutMs: TEN_MINUTES_MS
464
+ }
443
465
  };
444
466
  var POPUP_WIDTH = 480;
445
467
  var POPUP_HEIGHT = 600;
446
- var POPUP_TIMEOUT_MS = 5 * 60 * 1e3;
447
468
  var POPUP_CLOSED_POLL_MS = 500;
448
469
  function expectAuthTokenResponse(value) {
449
470
  const response = (0, import_sdk_core2.expectObject)(
@@ -464,12 +485,13 @@ function expectAuthTokenResponse(value) {
464
485
  tokenType: response.tokenType
465
486
  };
466
487
  }
467
- var GoogleAuthFlow = class {
488
+ var AuthPageFlow = class {
468
489
  appId;
469
490
  apiUrl;
470
491
  configuredAuthPageUrl;
471
492
  client;
472
493
  provider;
494
+ profile;
473
495
  providerLabel;
474
496
  redirectStorageKey;
475
497
  popupPromise = null;
@@ -479,7 +501,8 @@ var GoogleAuthFlow = class {
479
501
  this.configuredAuthPageUrl = config.authPageUrl;
480
502
  this.client = config.client;
481
503
  this.provider = config.provider ?? "google";
482
- this.providerLabel = PROVIDER_LABELS[this.provider];
504
+ this.profile = PROVIDERS[this.provider];
505
+ this.providerLabel = this.profile.label;
483
506
  this.redirectStorageKey = `mitra_${this.provider}_redirect_${config.appId}`;
484
507
  }
485
508
  signIn(options = {}) {
@@ -496,6 +519,19 @@ var GoogleAuthFlow = class {
496
519
  });
497
520
  return this.popupPromise;
498
521
  }
522
+ /**
523
+ * Finishes a redirect this provider started, or returns `null` when the URL
524
+ * carries no result or carries one that belongs to another provider's flow.
525
+ *
526
+ * The state generated at the start of every flow is prefixed with the provider
527
+ * name, and the auth page echoes it verbatim, so a fragment identifies its own
528
+ * flow. An application that offers several methods can call every completion
529
+ * at startup, in any order, even while other methods have requests pending.
530
+ *
531
+ * A fragment of this flow is always consumed, including when it cannot be
532
+ * completed, so a failure is reported once instead of on every reload. A
533
+ * fragment of another flow is left exactly as it was found.
534
+ */
499
535
  async completeRedirect() {
500
536
  const browserWindow = this.requireBrowser();
501
537
  const params = new URLSearchParams(browserWindow.location.hash.replace(/^#/, ""));
@@ -504,17 +540,36 @@ var GoogleAuthFlow = class {
504
540
  const error = params.get("errorMitra");
505
541
  if (code === null && state === null && error === null) return null;
506
542
  const context = this.readRedirectContext(browserWindow);
507
- if (!state?.trim()) {
508
- throw new Error(`${this.providerLabel} sign-in redirect is missing state.`);
543
+ const hasState = state !== null && state.trim() !== "";
544
+ if (hasState && !this.ownsState(state)) return null;
545
+ if (!hasState && !context) return null;
546
+ if (!hasState) {
547
+ throw this.discardOwnRedirect(
548
+ browserWindow,
549
+ `${this.providerLabel} sign-in redirect is missing state.`
550
+ );
551
+ }
552
+ if (context && this.hasExpired(context)) {
553
+ this.clearRedirectContext(browserWindow);
554
+ throw this.discardOwnRedirect(
555
+ browserWindow,
556
+ `${this.providerLabel} sign-in request expired before it was completed.`
557
+ );
509
558
  }
510
559
  if (context?.state !== state) {
511
- throw new Error(`Invalid ${this.providerLabel} sign-in state (possible CSRF).`);
560
+ throw this.discardOwnRedirect(
561
+ browserWindow,
562
+ `Invalid ${this.providerLabel} sign-in state (possible CSRF).`
563
+ );
512
564
  }
513
565
  const expectedRedirectUri = this.getRedirectUri(
514
566
  resolveAuthPageUrl(this.apiUrl, this.configuredAuthPageUrl, browserWindow)
515
567
  );
516
568
  if (context.redirectUri !== expectedRedirectUri) {
517
- throw new Error(`${this.providerLabel} sign-in redirect context is invalid.`);
569
+ throw this.discardOwnRedirect(
570
+ browserWindow,
571
+ `${this.providerLabel} sign-in redirect context is invalid.`
572
+ );
518
573
  }
519
574
  this.cleanRedirectFragment(browserWindow);
520
575
  this.clearRedirectContext(browserWindow);
@@ -533,8 +588,13 @@ var GoogleAuthFlow = class {
533
588
  this.configuredAuthPageUrl,
534
589
  browserWindow
535
590
  );
591
+ const completedInAnotherTab = this.profile.pendingRequestTtlMs !== void 0;
592
+ if (completedInAnotherTab) {
593
+ this.writeRedirectContext(browserWindow, this.newRedirectContext(state, authPageUrl));
594
+ }
536
595
  const popup = this.openPopup(browserWindow, this.buildStartUrl(browserWindow, authPageUrl, state));
537
596
  const result = await this.waitForPopupResult(browserWindow, popup, authPageUrl.origin, state);
597
+ if (completedInAnotherTab) this.clearRedirectContext(browserWindow);
538
598
  if (result.code) return this.exchangeCode(result.code, this.getRedirectUri(authPageUrl));
539
599
  return expectAuthTokenResponse(result.token);
540
600
  }
@@ -545,20 +605,20 @@ var GoogleAuthFlow = class {
545
605
  this.configuredAuthPageUrl,
546
606
  browserWindow
547
607
  );
548
- const context = {
549
- state,
550
- redirectUri: this.getRedirectUri(authPageUrl)
551
- };
552
- this.persistRedirectContext(browserWindow, context);
608
+ if (!this.writeRedirectContext(browserWindow, this.newRedirectContext(state, authPageUrl))) {
609
+ throw new Error(
610
+ `${this.providerLabel} sign-in redirect requires ${this.profile.redirectStorage}.`
611
+ );
612
+ }
553
613
  const startUrl = this.buildStartUrl(browserWindow, authPageUrl, state);
554
614
  browserWindow.location.assign(startUrl.toString());
555
615
  return new Promise(() => void 0);
556
616
  }
557
617
  async exchangeCode(code, redirectUri) {
558
- const response = await this.client.post(`/api/v1/auth/${this.provider}`, {
618
+ const response = await this.client.post(this.profile.exchangePath, {
559
619
  appId: this.appId,
560
620
  code,
561
- redirectUri
621
+ ...this.profile.sendsRedirectUri ? { redirectUri } : {}
562
622
  });
563
623
  return expectAuthTokenResponse(response);
564
624
  }
@@ -575,13 +635,41 @@ var GoogleAuthFlow = class {
575
635
  getRedirectUri(authPageUrl) {
576
636
  return `${authPageUrl.origin}${authPageUrl.pathname}`;
577
637
  }
638
+ newRedirectContext(state, authPageUrl) {
639
+ return {
640
+ state,
641
+ redirectUri: this.getRedirectUri(authPageUrl),
642
+ createdAt: Date.now()
643
+ };
644
+ }
645
+ /**
646
+ * Reports a fragment of this flow that cannot be completed, dropping it from the
647
+ * URL first. Nobody else claims a fragment that names this flow, so leaving it
648
+ * there would make the application fail again on every reload.
649
+ */
650
+ discardOwnRedirect(browserWindow, message) {
651
+ this.cleanRedirectFragment(browserWindow);
652
+ return new Error(message);
653
+ }
654
+ /** Whether a state echoed by the auth page was generated by this provider's flow. */
655
+ ownsState(state) {
656
+ return state.startsWith(`${this.provider}.`);
657
+ }
658
+ hasExpired(context) {
659
+ const ttlMs = this.profile.pendingRequestTtlMs;
660
+ if (ttlMs === void 0) return false;
661
+ if (!Number.isFinite(context.createdAt)) return true;
662
+ return Date.now() - context.createdAt > ttlMs;
663
+ }
664
+ /** A one-time state that names the flow that created it, so its fragment is recognizable. */
578
665
  generateState() {
579
666
  if (!globalThis.crypto?.getRandomValues) {
580
667
  throw new Error(`${this.providerLabel} sign-in requires crypto.getRandomValues.`);
581
668
  }
582
669
  const bytes = new Uint8Array(16);
583
670
  globalThis.crypto.getRandomValues(bytes);
584
- return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
671
+ const random = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
672
+ return `${this.provider}.${random}`;
585
673
  }
586
674
  openPopup(browserWindow, url) {
587
675
  const outerWidth = browserWindow.outerWidth || browserWindow.screen.width;
@@ -590,7 +678,7 @@ var GoogleAuthFlow = class {
590
678
  const top = Math.max(0, (browserWindow.screenY || 0) + (outerHeight - POPUP_HEIGHT) / 2);
591
679
  const popup = browserWindow.open(
592
680
  url.toString(),
593
- "mitra-google-oauth",
681
+ `mitra-${this.provider}-auth`,
594
682
  `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},menubar=no,toolbar=no,status=no`
595
683
  );
596
684
  if (!popup) {
@@ -603,7 +691,7 @@ var GoogleAuthFlow = class {
603
691
  const timeout = globalThis.setTimeout(() => {
604
692
  cleanup();
605
693
  reject(new Error(`${this.providerLabel} sign-in timed out.`));
606
- }, POPUP_TIMEOUT_MS);
694
+ }, this.profile.popupTimeoutMs);
607
695
  const closedPoll = globalThis.setInterval(() => {
608
696
  if (popup.closed) {
609
697
  cleanup();
@@ -628,7 +716,7 @@ var GoogleAuthFlow = class {
628
716
  const code = typeof data.code === "string" && data.code.trim() ? data.code : void 0;
629
717
  if (!code && data.token === void 0) {
630
718
  cleanup();
631
- reject(new Error("Google auth page returned neither code nor token."));
719
+ reject(new Error(`${this.providerLabel} auth page returned neither code nor token.`));
632
720
  return;
633
721
  }
634
722
  cleanup();
@@ -643,22 +731,28 @@ var GoogleAuthFlow = class {
643
731
  browserWindow.addEventListener("message", onMessage);
644
732
  });
645
733
  }
646
- persistRedirectContext(browserWindow, context) {
734
+ /** Writes the pending request, reporting whether storage accepted it. */
735
+ writeRedirectContext(browserWindow, context) {
647
736
  try {
648
- browserWindow.sessionStorage.setItem(this.redirectStorageKey, JSON.stringify(context));
737
+ browserWindow[this.profile.redirectStorage].setItem(
738
+ this.redirectStorageKey,
739
+ JSON.stringify(context)
740
+ );
741
+ return true;
649
742
  } catch {
650
- throw new Error(`${this.providerLabel} sign-in redirect requires sessionStorage.`);
743
+ return false;
651
744
  }
652
745
  }
653
746
  readRedirectContext(browserWindow) {
654
747
  try {
655
- const raw = browserWindow.sessionStorage.getItem(this.redirectStorageKey);
748
+ const raw = browserWindow[this.profile.redirectStorage].getItem(this.redirectStorageKey);
656
749
  if (!raw) return null;
657
750
  const value = JSON.parse(raw);
658
751
  if (typeof value.state !== "string" || typeof value.redirectUri !== "string") return null;
659
752
  return {
660
753
  state: value.state,
661
- redirectUri: value.redirectUri
754
+ redirectUri: value.redirectUri,
755
+ createdAt: typeof value.createdAt === "number" ? value.createdAt : Number.NaN
662
756
  };
663
757
  } catch {
664
758
  return null;
@@ -666,7 +760,7 @@ var GoogleAuthFlow = class {
666
760
  }
667
761
  clearRedirectContext(browserWindow) {
668
762
  try {
669
- browserWindow.sessionStorage.removeItem(this.redirectStorageKey);
763
+ browserWindow[this.profile.redirectStorage].removeItem(this.redirectStorageKey);
670
764
  } catch {
671
765
  }
672
766
  }
@@ -736,6 +830,7 @@ var AuthModule = class {
736
830
  currentUserApi;
737
831
  googleAuth;
738
832
  microsoftAuth;
833
+ emailAuth;
739
834
  constructor(appId, iamBaseUrl, options = {}) {
740
835
  this.appId = appId;
741
836
  const trimmedIamBaseUrl = stripTrailingSlashes(iamBaseUrl);
@@ -752,19 +847,26 @@ var AuthModule = class {
752
847
  onUnauthorized: (requestToken) => this.handleUnauthorized(requestToken)
753
848
  });
754
849
  this.currentUserApi = (0, import_sdk_core3.createAuthModule)(this.authedClient, coreErrors);
755
- this.googleAuth = new GoogleAuthFlow({
850
+ this.googleAuth = new AuthPageFlow({
756
851
  appId,
757
852
  apiUrl,
758
853
  authPageUrl: options.authPageUrl,
759
854
  client: this.publicClient
760
855
  });
761
- this.microsoftAuth = new GoogleAuthFlow({
856
+ this.microsoftAuth = new AuthPageFlow({
762
857
  appId,
763
858
  apiUrl,
764
859
  authPageUrl: options.authPageUrl,
765
860
  client: this.publicClient,
766
861
  provider: "microsoft"
767
862
  });
863
+ this.emailAuth = new AuthPageFlow({
864
+ appId,
865
+ apiUrl,
866
+ authPageUrl: options.authPageUrl,
867
+ client: this.publicClient,
868
+ provider: "email"
869
+ });
768
870
  this.loadFromStorage();
769
871
  const readAccessToken = () => this.#accessToken;
770
872
  sessionPorts.set(this, {
@@ -790,10 +892,10 @@ var AuthModule = class {
790
892
  get isAuthenticated() {
791
893
  return this._currentUser !== null && this.#accessToken !== null;
792
894
  }
793
- /** @deprecated Email/password authentication is not implemented by IAM. Use Google or Microsoft SSO. */
895
+ /** @deprecated Email/password authentication is not implemented by IAM. Use signInWithEmail() or SSO. */
794
896
  async signIn(_credentials) {
795
897
  throw new MitraApiError(
796
- "Email/password authentication is not available. Use signInWithGoogle() or signInWithMicrosoft().",
898
+ "Email/password authentication is not available. Use signInWithEmail(), signInWithGoogle() or signInWithMicrosoft().",
797
899
  0,
798
900
  "UNSUPPORTED_AUTH_METHOD"
799
901
  );
@@ -879,8 +981,10 @@ var AuthModule = class {
879
981
  *
880
982
  * The method consumes and clears the fragment and stored CSRF context, sends
881
983
  * the single-use code directly to IAM, persists both tokens, calls `auth.me()`,
882
- * and notifies auth-state listeners. It returns `null` when the current URL is
883
- * not a Google SSO redirect. Redirect errors must carry the same `stateMitra`
984
+ * and notifies auth-state listeners. It returns `null` when the current URL
985
+ * carries no redirect result, and also when it carries one this flow never
986
+ * started, so an application that offers several methods can call every
987
+ * completion at startup. Redirect errors must carry the same `stateMitra`
884
988
  * stored at the start of the flow before their message is exposed or consumed.
885
989
  *
886
990
  * @returns The authenticated user, or `null` when no redirect result is present.
@@ -916,10 +1020,54 @@ var AuthModule = class {
916
1020
  const tokenResponse = await this.microsoftAuth.completeRedirect();
917
1021
  return tokenResponse ? this.establishSession(tokenResponse) : null;
918
1022
  }
919
- /** @deprecated Email/password registration is not implemented by IAM. Use Google or Microsoft SSO. */
1023
+ /**
1024
+ * Signs in with a one-time code sent by email: the same auth-page handshake as
1025
+ * SSO, where the platform page collects the address and the code, and IAM
1026
+ * hands back a single-use exchange code redeemed at `/auth/magic-link/exchange`.
1027
+ * Popup by default; redirect mode navigates away.
1028
+ *
1029
+ * The message also carries a link. Because that link opens a new tab, the
1030
+ * pending request is kept in `localStorage` for 10 minutes - the one-time state
1031
+ * and the auth page URL, never a token - so
1032
+ * {@link completeEmailSignInRedirect} can finish the flow in that tab.
1033
+ *
1034
+ * @param options - Popup or redirect mode.
1035
+ * @returns The authenticated and hydrated user in popup mode.
1036
+ * @throws {MitraApiError} When IAM rejects the exchange code.
1037
+ * @throws {Error} When the browser blocks or cancels the popup, the flow times
1038
+ * out, or the response fails origin, source, state, or shape validation. A
1039
+ * person who finishes through the link instead of the popup leaves this call
1040
+ * to time out, which the application should treat as a cancelled popup.
1041
+ *
1042
+ * @example
1043
+ * ```typescript
1044
+ * const user = await mitra.auth.signInWithEmail();
1045
+ * ```
1046
+ */
1047
+ async signInWithEmail(options = {}) {
1048
+ return this.establishSession(await this.emailAuth.signIn(options));
1049
+ }
1050
+ /**
1051
+ * Completes an email sign-in from `#codeMitra` and `#stateMitra`, during
1052
+ * application startup like {@link completeGoogleSignInRedirect}.
1053
+ *
1054
+ * It covers both ways the flow comes back, and they are the same check: a
1055
+ * redirect in the tab that started it, and the tab the link in the message
1056
+ * opened. Both carry the one-time state this SDK generated, which the second
1057
+ * tab matches against the pending request kept in `localStorage`. A request
1058
+ * older than 10 minutes is discarded instead of completed.
1059
+ *
1060
+ * @returns The authenticated user, or `null` when the URL carries no result or
1061
+ * carries one that belongs to another sign-in method's flow.
1062
+ */
1063
+ async completeEmailSignInRedirect() {
1064
+ const tokenResponse = await this.emailAuth.completeRedirect();
1065
+ return tokenResponse ? this.establishSession(tokenResponse) : null;
1066
+ }
1067
+ /** @deprecated Email/password registration is not implemented by IAM. Use signInWithEmail() or SSO. */
920
1068
  async signUp(_data) {
921
1069
  throw new MitraApiError(
922
- "Email/password registration is not available. Use signInWithGoogle() or signInWithMicrosoft().",
1070
+ "Email/password registration is not available. Use signInWithEmail(), signInWithGoogle() or signInWithMicrosoft().",
923
1071
  0,
924
1072
  "UNSUPPORTED_AUTH_METHOD"
925
1073
  );
@@ -1660,11 +1808,10 @@ function holdSendsWhileOffline(session, outbox) {
1660
1808
 
1661
1809
  // src/modules/agent-session.ts
1662
1810
  var CONNECT_TIMEOUT_MS = 15e3;
1663
- var CHANNEL_BOOT_TIMEOUT_MS = 9e4;
1664
- var CHANNEL_BOOT_RETRY_MS = 2e3;
1665
1811
  var SILENCE_TIMEOUT_MS = 6e4;
1666
1812
  var RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 16e3];
1667
1813
  var SUPERSEDED_CLOSE_CODE = 4409;
1814
+ var SOCKET_OPEN = 1;
1668
1815
  var SilenceWatchdog = class {
1669
1816
  constructor(onSilence) {
1670
1817
  this.onSilence = onSilence;
@@ -1780,6 +1927,8 @@ var BrowserAgentTaskEventSource = class {
1780
1927
  */
1781
1928
  boxCursors = /* @__PURE__ */ new Map();
1782
1929
  boxAddresses = /* @__PURE__ */ new Map();
1930
+ /** The box socket a task is talking on right now; absent while it is lost or being redialed. */
1931
+ directSockets = /* @__PURE__ */ new Map();
1783
1932
  async open(taskId, observer, signal, transport = "auto") {
1784
1933
  if (transport === "http") return this.openSse(taskId, observer, signal);
1785
1934
  if (transport === "websocket") return this.openWebSocket(taskId, observer, signal);
@@ -1811,6 +1960,23 @@ var BrowserAgentTaskEventSource = class {
1811
1960
  }
1812
1961
  };
1813
1962
  }
1963
+ /**
1964
+ * Writes a message or an interrupt on the box socket when the task is on the direct channel
1965
+ * and that socket is open right now. The box accepts the core's input as its own inbound
1966
+ * frame, asks the copilot for admission itself and answers on this same socket with the
1967
+ * frames the observer already reads, so the copilot's host socket is off the message path.
1968
+ * False sends the caller to REST: no direct channel, a socket lost or mid-redial, or an
1969
+ * approval, which stays on REST as it is.
1970
+ *
1971
+ * A frame written on a socket that closes before the box acknowledges it is not sent again
1972
+ * over REST: the box may have admitted the turn already, and a second copy would start it
1973
+ * twice. The redial replays the box log, which is the same recovery a REST 202 gets when its
1974
+ * turn is lost.
1975
+ */
1976
+ sendOnChannel(taskId, input) {
1977
+ if (input.type === "approval_response") return false;
1978
+ return this.directSockets.get(taskId)?.send(JSON.stringify(input)) ?? false;
1979
+ }
1814
1980
  async requireFreshToken() {
1815
1981
  const fresh = await this.auth.ensureFreshSession();
1816
1982
  const token = this.auth.accessToken;
@@ -1827,28 +1993,28 @@ var BrowserAgentTaskEventSource = class {
1827
1993
  requestDirectChannel(taskId, token, signal) {
1828
1994
  return this.askDirectChannel(taskId, token, signal).catch(() => null);
1829
1995
  }
1830
- /** Like `requestDirectChannel`, but a request the network lost is thrown, not a refusal. */
1996
+ /**
1997
+ * Like `requestDirectChannel`, but a request the network lost is thrown, not a refusal.
1998
+ *
1999
+ * One request. The copilot holds it while the box boots and answers 200 with the channel, or
2000
+ * an error status once the box cannot be had. A 202 comes only from a copilot older than that
2001
+ * contract, which used to mean "still booting, ask again": there is no channel to open, and
2002
+ * the conversation follows the copilot socket. Polling here again would put the wait back on
2003
+ * the client that the server now owns.
2004
+ */
1831
2005
  async askDirectChannel(taskId, token, signal) {
2006
+ if (signal?.aborted) return null;
1832
2007
  const url = `${this.apiUrl}/copilot/api/v1/tasks/${encodeURIComponent(taskId)}/channel`;
1833
- const deadline = Date.now() + CHANNEL_BOOT_TIMEOUT_MS;
1834
- for (; ; ) {
1835
- if (signal?.aborted) return null;
1836
- const response = await globalThis.fetch(url, {
1837
- method: "POST",
1838
- headers: { Authorization: `Bearer ${stripBearer2(token)}` },
1839
- ...signal ? { signal } : {}
1840
- });
1841
- if (response.status === 202) {
1842
- if (Date.now() >= deadline) return null;
1843
- await sleep(CHANNEL_BOOT_RETRY_MS, signal);
1844
- continue;
1845
- }
1846
- if (!response.ok) return null;
1847
- try {
1848
- return toDirectChannel(await response.json(), this.apiUrl);
1849
- } catch {
1850
- return null;
1851
- }
2008
+ const response = await globalThis.fetch(url, {
2009
+ method: "POST",
2010
+ headers: { Authorization: `Bearer ${stripBearer2(token)}` },
2011
+ ...signal ? { signal } : {}
2012
+ });
2013
+ if (response.status === 202 || !response.ok) return null;
2014
+ try {
2015
+ return toDirectChannel(await response.json(), this.apiUrl);
2016
+ } catch {
2017
+ return null;
1852
2018
  }
1853
2019
  }
1854
2020
  async openWebSocket(taskId, observer, signal) {
@@ -1897,6 +2063,11 @@ var BrowserAgentTaskEventSource = class {
1897
2063
  signal?.addEventListener("abort", onAbort, { once: true });
1898
2064
  let inTurn = false;
1899
2065
  let current = null;
2066
+ const track = (socket) => {
2067
+ if (socket) this.directSockets.set(taskId, socket);
2068
+ else if (current && this.directSockets.get(taskId) === current) this.directSockets.delete(taskId);
2069
+ current = socket;
2070
+ };
1900
2071
  const onFrame = (event) => {
1901
2072
  if (typeof event.sequence === "number") {
1902
2073
  const seen = this.boxCursors.get(taskId) ?? 0;
@@ -1906,22 +2077,21 @@ var BrowserAgentTaskEventSource = class {
1906
2077
  observer.onEvent(event);
1907
2078
  };
1908
2079
  const onLost = (error, code) => {
1909
- current = null;
2080
+ track(null);
1910
2081
  if (link.signal.aborted) return;
1911
2082
  if (!inTurn || code === SUPERSEDED_CLOSE_CODE) {
1912
2083
  observer.onDisconnect(error);
1913
2084
  return;
1914
2085
  }
1915
- void this.redial(taskId, error, link.signal, observer, { onFrame, onLost }).then((socket) => {
1916
- current = socket;
1917
- });
2086
+ void this.redial(taskId, error, link.signal, observer, { onFrame, onLost }).then(track);
1918
2087
  };
1919
- current = await this.dial(channel.wsUrl, { signal: link.signal, onFrame, onLost });
2088
+ track(await this.dial(channel.wsUrl, { signal: link.signal, onFrame, onLost }));
1920
2089
  return {
1921
2090
  close: () => {
1922
2091
  signal?.removeEventListener("abort", onAbort);
1923
2092
  link.abort();
1924
2093
  current?.close();
2094
+ track(null);
1925
2095
  }
1926
2096
  };
1927
2097
  }
@@ -2006,6 +2176,15 @@ var BrowserAgentTaskEventSource = class {
2006
2176
  removeAbortListener();
2007
2177
  socket.close(1e3, "Client closed");
2008
2178
  };
2179
+ const send = (frame) => {
2180
+ if (intentionalClose || socket.readyState !== SOCKET_OPEN) return false;
2181
+ try {
2182
+ socket.send(frame);
2183
+ return true;
2184
+ } catch {
2185
+ return false;
2186
+ }
2187
+ };
2009
2188
  const onAbort = () => {
2010
2189
  if (!opened) {
2011
2190
  rejectHandshake(signal?.reason instanceof Error ? signal.reason : new Error("Agent WebSocket connection aborted."));
@@ -2029,7 +2208,7 @@ var BrowserAgentTaskEventSource = class {
2029
2208
  } catch {
2030
2209
  }
2031
2210
  }
2032
- resolve({ close });
2211
+ resolve({ close, send });
2033
2212
  };
2034
2213
  socket.onerror = () => {
2035
2214
  if (!opened) rejectHandshake(new Error("Failed to connect to the Agent WebSocket."));
@@ -2136,6 +2315,10 @@ var BrowserAgentTaskEventSource = class {
2136
2315
  };
2137
2316
 
2138
2317
  // src/modules/agent-tasks.ts
2318
+ function bornOnBox(options) {
2319
+ if (!("create" in options) || options.runtime || options.transport === "http") return options;
2320
+ return { ...options, runtime: "T3" };
2321
+ }
2139
2322
  function createBrowserAgentTasksModule(httpClient, auth, apiUrl) {
2140
2323
  const tasks = (0, import_sdk_core8.createAgentTasksModule)(httpClient, coreErrors);
2141
2324
  const source = new BrowserAgentTaskEventSource(auth, apiUrl);
@@ -2161,11 +2344,14 @@ function createBrowserAgentTasksModule(httpClient, auth, apiUrl) {
2161
2344
  }
2162
2345
  };
2163
2346
  const manager = (0, import_sdk_core8.createAgentTaskSessionManager)({
2164
- tasks: { ...tasks, sendInput: (taskId, input) => outbox.sendInput(taskId, input) },
2347
+ tasks: {
2348
+ ...tasks,
2349
+ sendInput: (taskId, input) => source.sendOnChannel(taskId, input) ? Promise.resolve() : outbox.sendInput(taskId, input)
2350
+ },
2165
2351
  eventSource
2166
2352
  });
2167
2353
  return (0, import_sdk_core8.withAgentTaskSessions)(tasks, {
2168
- session: (options) => holdSendsWhileOffline(manager.session(options), outbox)
2354
+ session: (options) => holdSendsWhileOffline(manager.session(bornOnBox(options)), outbox)
2169
2355
  });
2170
2356
  }
2171
2357
 
@@ -2227,7 +2413,12 @@ function expectAppInfoResponse(value) {
2227
2413
  }
2228
2414
  return {
2229
2415
  dataSourceId: response.dataSourceId,
2230
- allowSignup: response.allowSignup
2416
+ allowSignup: response.allowSignup,
2417
+ // Unlike the fields above, a missing or non-boolean value is read as `false`
2418
+ // instead of failing `init()`: a Code Studio older than the email login gate
2419
+ // answers without this field, and an app that cannot prove it is enabled
2420
+ // should stop offering the option, not stop starting.
2421
+ emailLoginEnabled: response.emailLoginEnabled === true
2231
2422
  };
2232
2423
  }
2233
2424
  function createClient(config) {
@@ -2295,6 +2486,7 @@ function createClient(config) {
2295
2486
  legacyBridge.connect();
2296
2487
  let initialized = false;
2297
2488
  let allowSignup = true;
2489
+ let emailLoginEnabled = false;
2298
2490
  async function init() {
2299
2491
  if (initialized) return;
2300
2492
  const publicClient = new HttpClient({
@@ -2310,6 +2502,7 @@ function createClient(config) {
2310
2502
  entitiesModule.setDataSourceId(appInfo.dataSourceId);
2311
2503
  }
2312
2504
  allowSignup = appInfo.allowSignup;
2505
+ emailLoginEnabled = appInfo.emailLoginEnabled;
2313
2506
  initialized = true;
2314
2507
  }
2315
2508
  return {
@@ -2325,6 +2518,9 @@ function createClient(config) {
2325
2518
  get allowSignup() {
2326
2519
  return allowSignup;
2327
2520
  },
2521
+ get emailLoginEnabled() {
2522
+ return emailLoginEnabled;
2523
+ },
2328
2524
  config
2329
2525
  };
2330
2526
  }