@mitralab.io/platform-sdk 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +71 -3
- package/dist/index.cjs +209 -59
- package/dist/index.d.cts +62 -11
- package/dist/index.d.ts +62 -11
- package/dist/index.js +209 -59
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -384,16 +384,37 @@ async function resolveApiKeySession(publicClient, appId, apiKey) {
|
|
|
384
384
|
);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
// src/modules/
|
|
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
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
458
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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(
|
|
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
|
-
|
|
684
|
+
/** Writes the pending request, reporting whether storage accepted it. */
|
|
685
|
+
writeRedirectContext(browserWindow, context) {
|
|
597
686
|
try {
|
|
598
|
-
browserWindow.
|
|
687
|
+
browserWindow[this.profile.redirectStorage].setItem(
|
|
688
|
+
this.redirectStorageKey,
|
|
689
|
+
JSON.stringify(context)
|
|
690
|
+
);
|
|
691
|
+
return true;
|
|
599
692
|
} catch {
|
|
600
|
-
|
|
693
|
+
return false;
|
|
601
694
|
}
|
|
602
695
|
}
|
|
603
696
|
readRedirectContext(browserWindow) {
|
|
604
697
|
try {
|
|
605
|
-
const raw = browserWindow.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
833
|
-
*
|
|
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
|
-
/**
|
|
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,8 +1771,6 @@ 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;
|
|
@@ -1790,28 +1936,28 @@ var BrowserAgentTaskEventSource = class {
|
|
|
1790
1936
|
requestDirectChannel(taskId, token, signal) {
|
|
1791
1937
|
return this.askDirectChannel(taskId, token, signal).catch(() => null);
|
|
1792
1938
|
}
|
|
1793
|
-
/**
|
|
1939
|
+
/**
|
|
1940
|
+
* Like `requestDirectChannel`, but a request the network lost is thrown, not a refusal.
|
|
1941
|
+
*
|
|
1942
|
+
* One request. The copilot holds it while the box boots and answers 200 with the channel, or
|
|
1943
|
+
* an error status once the box cannot be had. A 202 comes only from a copilot older than that
|
|
1944
|
+
* contract, which used to mean "still booting, ask again": there is no channel to open, and
|
|
1945
|
+
* the conversation follows the copilot socket. Polling here again would put the wait back on
|
|
1946
|
+
* the client that the server now owns.
|
|
1947
|
+
*/
|
|
1794
1948
|
async askDirectChannel(taskId, token, signal) {
|
|
1949
|
+
if (signal?.aborted) return null;
|
|
1795
1950
|
const url = `${this.apiUrl}/copilot/api/v1/tasks/${encodeURIComponent(taskId)}/channel`;
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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
|
-
}
|
|
1951
|
+
const response = await globalThis.fetch(url, {
|
|
1952
|
+
method: "POST",
|
|
1953
|
+
headers: { Authorization: `Bearer ${stripBearer2(token)}` },
|
|
1954
|
+
...signal ? { signal } : {}
|
|
1955
|
+
});
|
|
1956
|
+
if (response.status === 202 || !response.ok) return null;
|
|
1957
|
+
try {
|
|
1958
|
+
return toDirectChannel(await response.json(), this.apiUrl);
|
|
1959
|
+
} catch {
|
|
1960
|
+
return null;
|
|
1815
1961
|
}
|
|
1816
1962
|
}
|
|
1817
1963
|
async openWebSocket(taskId, observer, signal) {
|
|
@@ -2099,6 +2245,10 @@ var BrowserAgentTaskEventSource = class {
|
|
|
2099
2245
|
};
|
|
2100
2246
|
|
|
2101
2247
|
// src/modules/agent-tasks.ts
|
|
2248
|
+
function bornOnBox(options) {
|
|
2249
|
+
if (!("create" in options) || options.runtime || options.transport === "http") return options;
|
|
2250
|
+
return { ...options, runtime: "T3" };
|
|
2251
|
+
}
|
|
2102
2252
|
function createBrowserAgentTasksModule(httpClient, auth, apiUrl) {
|
|
2103
2253
|
const tasks = createAgentTasksModule(httpClient, coreErrors);
|
|
2104
2254
|
const source = new BrowserAgentTaskEventSource(auth, apiUrl);
|
|
@@ -2128,7 +2278,7 @@ function createBrowserAgentTasksModule(httpClient, auth, apiUrl) {
|
|
|
2128
2278
|
eventSource
|
|
2129
2279
|
});
|
|
2130
2280
|
return withAgentTaskSessions(tasks, {
|
|
2131
|
-
session: (options) => holdSendsWhileOffline(manager.session(options), outbox)
|
|
2281
|
+
session: (options) => holdSendsWhileOffline(manager.session(bornOnBox(options)), outbox)
|
|
2132
2282
|
});
|
|
2133
2283
|
}
|
|
2134
2284
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mitralab.io/platform-sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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.
|
|
65
|
+
"@mitralab.io/sdk-core": "0.2.4",
|
|
66
66
|
"mitra-interactions-sdk": "1.0.60-beta.48"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|