@oxyhq/core 5.5.0 → 7.0.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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +7 -4
- package/dist/cjs/OxyServices.base.js +7 -102
- package/dist/cjs/boot/coldBootV2.js +350 -0
- package/dist/cjs/boot/deviceBootReturn.js +152 -0
- package/dist/cjs/crypto/keyManager.js +95 -0
- package/dist/cjs/i18n/locales/en-US.json +13 -1
- package/dist/cjs/i18n/locales/es-ES.json +13 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +13 -1
- package/dist/cjs/i18n/locales/locales/es-ES.json +13 -1
- package/dist/cjs/index.js +54 -45
- package/dist/cjs/mixins/OxyServices.accounts.js +13 -26
- package/dist/cjs/mixins/OxyServices.auth.js +66 -201
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
- package/dist/cjs/mixins/index.js +8 -17
- package/dist/cjs/server/index.js +2 -2
- package/dist/cjs/session/SessionClient.js +40 -1
- package/dist/cjs/session/authStateStore.js +284 -0
- package/dist/cjs/session/refresh.js +264 -0
- package/dist/cjs/shared/utils/debugUtils.js +3 -3
- package/dist/cjs/utils/accountUtils.js +1 -55
- package/dist/cjs/utils/authWebUrl.js +10 -39
- package/dist/cjs/utils/coldBoot.js +10 -8
- package/dist/cjs/utils/platform.js +19 -0
- package/dist/cjs/utils/registrableApex.js +49 -0
- package/dist/cjs/utils/ssoBounce.js +15 -362
- package/dist/cjs/utils/validationUtils.js +57 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +7 -4
- package/dist/esm/OxyServices.base.js +7 -102
- package/dist/esm/boot/coldBootV2.js +344 -0
- package/dist/esm/boot/deviceBootReturn.js +146 -0
- package/dist/esm/crypto/keyManager.js +95 -0
- package/dist/esm/i18n/locales/en-US.json +13 -1
- package/dist/esm/i18n/locales/es-ES.json +13 -1
- package/dist/esm/i18n/locales/locales/en-US.json +13 -1
- package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
- package/dist/esm/index.js +34 -17
- package/dist/esm/mixins/OxyServices.accounts.js +13 -26
- package/dist/esm/mixins/OxyServices.auth.js +66 -201
- package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
- package/dist/esm/mixins/index.js +8 -17
- package/dist/esm/server/index.js +1 -1
- package/dist/esm/session/SessionClient.js +40 -1
- package/dist/esm/session/authStateStore.js +278 -0
- package/dist/esm/session/refresh.js +257 -0
- package/dist/esm/shared/utils/debugUtils.js +3 -3
- package/dist/esm/utils/accountUtils.js +0 -53
- package/dist/esm/utils/authWebUrl.js +9 -37
- package/dist/esm/utils/coldBoot.js +10 -8
- package/dist/esm/utils/platform.js +18 -0
- package/dist/esm/utils/registrableApex.js +46 -0
- package/dist/esm/utils/ssoBounce.js +14 -345
- package/dist/esm/utils/validationUtils.js +56 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +14 -1
- package/dist/types/OxyServices.base.d.ts +0 -52
- package/dist/types/OxyServices.d.ts +0 -25
- package/dist/types/boot/coldBootV2.d.ts +76 -0
- package/dist/types/boot/deviceBootReturn.d.ts +83 -0
- package/dist/types/crypto/keyManager.d.ts +21 -0
- package/dist/types/index.d.ts +14 -21
- package/dist/types/mixins/OxyServices.accounts.d.ts +24 -29
- package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
- package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
- package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
- package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
- package/dist/types/mixins/OxyServices.features.d.ts +0 -2
- package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
- package/dist/types/mixins/OxyServices.language.d.ts +0 -2
- package/dist/types/mixins/OxyServices.links.d.ts +0 -2
- package/dist/types/mixins/OxyServices.location.d.ts +0 -2
- package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
- package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
- package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
- package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
- package/dist/types/mixins/OxyServices.security.d.ts +0 -2
- package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.user.d.ts +0 -2
- package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
- package/dist/types/mixins/index.d.ts +5 -9
- package/dist/types/models/interfaces.d.ts +0 -67
- package/dist/types/models/session.d.ts +4 -5
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/session/SessionClient.d.ts +26 -1
- package/dist/types/session/authStateStore.d.ts +119 -0
- package/dist/types/session/refresh.d.ts +93 -0
- package/dist/types/shared/utils/debugUtils.d.ts +3 -3
- package/dist/types/utils/accountUtils.d.ts +2 -17
- package/dist/types/utils/authWebUrl.d.ts +9 -35
- package/dist/types/utils/coldBoot.d.ts +17 -14
- package/dist/types/utils/platform.d.ts +14 -0
- package/dist/types/utils/registrableApex.d.ts +31 -0
- package/dist/types/utils/ssoBounce.d.ts +14 -280
- package/dist/types/utils/validationUtils.d.ts +15 -0
- package/package.json +2 -2
- package/src/HttpService.ts +20 -4
- package/src/OxyServices.base.ts +7 -112
- package/src/OxyServices.ts +0 -38
- package/src/boot/__tests__/coldBootV2.test.ts +317 -0
- package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
- package/src/boot/coldBootV2.ts +426 -0
- package/src/boot/deviceBootReturn.ts +195 -0
- package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
- package/src/crypto/keyManager.ts +101 -0
- package/src/i18n/locales/en-US.json +13 -1
- package/src/i18n/locales/es-ES.json +13 -1
- package/src/index.ts +75 -65
- package/src/mixins/OxyServices.accounts.ts +27 -39
- package/src/mixins/OxyServices.auth.ts +78 -253
- package/src/mixins/OxyServices.deviceBoot.ts +146 -0
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
- package/src/mixins/__tests__/accounts.test.ts +17 -44
- package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
- package/src/mixins/index.ts +10 -23
- package/src/models/interfaces.ts +0 -79
- package/src/models/session.ts +4 -5
- package/src/server/index.ts +1 -1
- package/src/session/SessionClient.ts +53 -2
- package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
- package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
- package/src/session/__tests__/SessionClient.state.test.ts +18 -5
- package/src/session/__tests__/authStateStore.test.ts +209 -0
- package/src/session/__tests__/refresh.test.ts +256 -0
- package/src/session/authStateStore.ts +335 -0
- package/src/session/refresh.ts +334 -0
- package/src/shared/utils/debugUtils.ts +3 -3
- package/src/utils/__tests__/authWebUrl.test.ts +5 -40
- package/src/utils/__tests__/registrableApex.test.ts +62 -0
- package/src/utils/__tests__/validationUtils.test.ts +30 -0
- package/src/utils/accountUtils.ts +2 -65
- package/src/utils/authWebUrl.ts +9 -39
- package/src/utils/coldBoot.ts +17 -14
- package/src/utils/platform.ts +21 -0
- package/src/utils/registrableApex.ts +45 -0
- package/src/utils/ssoBounce.ts +14 -393
- package/src/utils/validationUtils.ts +62 -0
- package/dist/cjs/AuthManager.js +0 -1110
- package/dist/cjs/AuthManagerTypes.js +0 -13
- package/dist/cjs/CrossDomainAuth.js +0 -206
- package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
- package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
- package/dist/cjs/mixins/OxyServices.silent.js +0 -204
- package/dist/cjs/mixins/OxyServices.sso.js +0 -208
- package/dist/cjs/utils/fapiAutoDetect.js +0 -99
- package/dist/cjs/utils/ssoEstablish.js +0 -110
- package/dist/cjs/utils/ssoReturn.js +0 -275
- package/dist/esm/AuthManager.js +0 -1105
- package/dist/esm/AuthManagerTypes.js +0 -12
- package/dist/esm/CrossDomainAuth.js +0 -201
- package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
- package/dist/esm/mixins/OxyServices.redirect.js +0 -92
- package/dist/esm/mixins/OxyServices.silent.js +0 -202
- package/dist/esm/mixins/OxyServices.sso.js +0 -204
- package/dist/esm/utils/fapiAutoDetect.js +0 -95
- package/dist/esm/utils/ssoEstablish.js +0 -107
- package/dist/esm/utils/ssoReturn.js +0 -271
- package/dist/types/AuthManager.d.ts +0 -380
- package/dist/types/AuthManagerTypes.d.ts +0 -81
- package/dist/types/CrossDomainAuth.d.ts +0 -164
- package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
- package/dist/types/mixins/OxyServices.redirect.d.ts +0 -92
- package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
- package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
- package/dist/types/utils/fapiAutoDetect.d.ts +0 -56
- package/dist/types/utils/ssoEstablish.d.ts +0 -85
- package/dist/types/utils/ssoReturn.d.ts +0 -156
- package/src/AuthManager.ts +0 -1269
- package/src/AuthManagerTypes.ts +0 -86
- package/src/CrossDomainAuth.ts +0 -243
- package/src/__tests__/authManager.cookiePath.test.ts +0 -390
- package/src/__tests__/authManager.security.test.ts +0 -377
- package/src/__tests__/crossDomainAuth.test.ts +0 -116
- package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
- package/src/mixins/OxyServices.fedcm.ts +0 -1026
- package/src/mixins/OxyServices.redirect.ts +0 -122
- package/src/mixins/OxyServices.silent.ts +0 -272
- package/src/mixins/OxyServices.sso.ts +0 -261
- package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
- package/src/mixins/__tests__/fedcm.test.ts +0 -667
- package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
- package/src/mixins/__tests__/silent.test.ts +0 -102
- package/src/mixins/__tests__/sso.test.ts +0 -228
- package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
- package/src/utils/__tests__/fapiAutoDetect.test.ts +0 -183
- package/src/utils/__tests__/ssoBounce.test.ts +0 -219
- package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
- package/src/utils/__tests__/ssoReturn.test.ts +0 -276
- package/src/utils/fapiAutoDetect.ts +0 -91
- package/src/utils/ssoEstablish.ts +0 -174
- package/src/utils/ssoReturn.ts +0 -389
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ServiceCredentialMismatchError = void 0;
|
|
4
4
|
exports.OxyServicesAuthMixin = OxyServicesAuthMixin;
|
|
5
|
+
const contracts_1 = require("@oxyhq/contracts");
|
|
5
6
|
const OxyServices_errors_1 = require("../OxyServices.errors");
|
|
6
7
|
const keyManager_1 = require("../crypto/keyManager");
|
|
7
8
|
const signatureService_1 = require("../crypto/signatureService");
|
|
@@ -618,207 +619,6 @@ function OxyServicesAuthMixin(Base) {
|
|
|
618
619
|
throw this.handleError(error);
|
|
619
620
|
}
|
|
620
621
|
}
|
|
621
|
-
/**
|
|
622
|
-
* Refresh every device-local refresh-cookie slot in a single round trip
|
|
623
|
-
* (Google-style multi-account rebuild).
|
|
624
|
-
*
|
|
625
|
-
* Calls `POST {sessionBaseUrl}/auth/refresh-all` with `credentials: 'include'`
|
|
626
|
-
* and NO bearer. The browser attaches every indexed `oxy_rt_${authuser}`
|
|
627
|
-
* cookie it has; the server rotates each in parallel and returns one entry
|
|
628
|
-
* per VALID account.
|
|
629
|
-
*
|
|
630
|
-
* Failure handling:
|
|
631
|
-
* - 401 → no signed-in accounts on this device → returns `{ accounts: [] }`
|
|
632
|
-
* (NOT an error; this is the cold-boot "not signed in" path).
|
|
633
|
-
* - 404 → endpoint not deployed → returns `{ accounts: [] }`.
|
|
634
|
-
* - Any other non-2xx → throws via `handleError`.
|
|
635
|
-
*
|
|
636
|
-
* The refresh cookie itself never enters JS — only the rotated access
|
|
637
|
-
* tokens do. Each access token still needs to be planted via
|
|
638
|
-
* `setTokens(...)` (or per-account in-memory storage) at the consumer.
|
|
639
|
-
*/
|
|
640
|
-
async refreshAllSessions(options = {}) {
|
|
641
|
-
const url = `${this.getSessionBaseUrl().replace(/\/$/, '')}/auth/refresh-all`;
|
|
642
|
-
// Optional bounded abort (see `RefreshAllOptions.timeout`). A positive
|
|
643
|
-
// timeout arms an `AbortController` that aborts the in-flight request; an
|
|
644
|
-
// abort is treated as "no signed-in accounts on this device" — the same
|
|
645
|
-
// outcome as a 401 — so a cross-domain stall falls through cleanly instead
|
|
646
|
-
// of hanging the cold boot.
|
|
647
|
-
const timeout = typeof options.timeout === 'number' && options.timeout > 0 ? options.timeout : undefined;
|
|
648
|
-
const controller = timeout !== undefined ? new AbortController() : undefined;
|
|
649
|
-
const timeoutId = timeout !== undefined && controller
|
|
650
|
-
? setTimeout(() => controller.abort(), timeout)
|
|
651
|
-
: undefined;
|
|
652
|
-
let response;
|
|
653
|
-
try {
|
|
654
|
-
response = await fetch(url, {
|
|
655
|
-
method: 'POST',
|
|
656
|
-
credentials: 'include',
|
|
657
|
-
headers: { Accept: 'application/json' },
|
|
658
|
-
signal: controller?.signal,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
catch (error) {
|
|
662
|
-
// A bounded-timeout abort is the "not signed in / cross-domain stall"
|
|
663
|
-
// path, NOT an error. The browser raises a DOMException named
|
|
664
|
-
// 'AbortError' (some runtimes use a generic Error); match on the name so
|
|
665
|
-
// we never throw the timeout into the cold-boot error handler.
|
|
666
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
667
|
-
return { accounts: [] };
|
|
668
|
-
}
|
|
669
|
-
throw this.handleError(error);
|
|
670
|
-
}
|
|
671
|
-
finally {
|
|
672
|
-
if (timeoutId !== undefined) {
|
|
673
|
-
clearTimeout(timeoutId);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
if (response.status === 401) {
|
|
677
|
-
return { accounts: [] };
|
|
678
|
-
}
|
|
679
|
-
if (response.status === 404) {
|
|
680
|
-
return { accounts: [] };
|
|
681
|
-
}
|
|
682
|
-
if (!response.ok) {
|
|
683
|
-
throw this.handleError(new Error(`Refresh-all failed with HTTP ${response.status}`));
|
|
684
|
-
}
|
|
685
|
-
const payload = (await response.json());
|
|
686
|
-
const raw = Array.isArray(payload.accounts) ? payload.accounts : [];
|
|
687
|
-
const accounts = [];
|
|
688
|
-
for (const entry of raw) {
|
|
689
|
-
if (entry === null || typeof entry !== 'object') {
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
const e = entry;
|
|
693
|
-
if (!e.accessToken || !e.expiresAt || !e.sessionId || !e.user) {
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
const userId = e.user.id ?? e.user._id;
|
|
697
|
-
// `name.displayName` is optional on the contract — do NOT drop no-name
|
|
698
|
-
// accounts. Only an absent userId or username makes the entry unusable.
|
|
699
|
-
if (!userId || !e.user.username) {
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
if (typeof e.authuser !== 'number') {
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
accounts.push({
|
|
706
|
-
authuser: e.authuser,
|
|
707
|
-
accessToken: e.accessToken,
|
|
708
|
-
expiresAt: e.expiresAt,
|
|
709
|
-
sessionId: e.sessionId,
|
|
710
|
-
user: {
|
|
711
|
-
id: userId,
|
|
712
|
-
username: e.user.username,
|
|
713
|
-
// `name.displayName` is optional; carry whatever structured name the
|
|
714
|
-
// server sent (possibly an empty object) and let consumers fall back.
|
|
715
|
-
name: e.user.name ?? {},
|
|
716
|
-
avatar: e.user.avatar ?? null,
|
|
717
|
-
email: e.user.email,
|
|
718
|
-
color: e.user.color ?? null,
|
|
719
|
-
},
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
return { accounts };
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* Rotate a single refresh-cookie slot and return the fresh access token.
|
|
726
|
-
*
|
|
727
|
-
* When `authuser` is provided, the server rotates ONLY that slot
|
|
728
|
-
* (`oxy_rt_${authuser}`) — sibling accounts on the same device stay
|
|
729
|
-
* untouched. When omitted, the server picks the lowest indexed slot present.
|
|
730
|
-
* The refresh cookie itself never enters JS.
|
|
731
|
-
*
|
|
732
|
-
* Returns `null` on 401 (no cookie / expired / reused) so the caller can
|
|
733
|
-
* fall through cleanly to the unauthenticated path.
|
|
734
|
-
*/
|
|
735
|
-
async refreshTokenViaCookie(opts = {}) {
|
|
736
|
-
const result = await this._refreshCookieRaw(opts.authuser);
|
|
737
|
-
return result;
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* Sign out a single device-local account by its authuser slot index.
|
|
741
|
-
*
|
|
742
|
-
* Revokes that slot's refresh-token family and deactivates its session;
|
|
743
|
-
* sibling indexed slots stay signed in. The browser-side `oxy_rt_${n}`
|
|
744
|
-
* cookie is cleared by the server's `Set-Cookie` response header.
|
|
745
|
-
*/
|
|
746
|
-
async logoutSessionByAuthuser(authuser) {
|
|
747
|
-
const url = `${this.getSessionBaseUrl().replace(/\/$/, '')}/auth/logout?authuser=${encodeURIComponent(String(authuser))}`;
|
|
748
|
-
try {
|
|
749
|
-
const response = await fetch(url, {
|
|
750
|
-
method: 'POST',
|
|
751
|
-
credentials: 'include',
|
|
752
|
-
headers: { Accept: 'application/json' },
|
|
753
|
-
});
|
|
754
|
-
if (!response.ok && response.status !== 401) {
|
|
755
|
-
throw new Error(`Logout (authuser=${authuser}) failed with HTTP ${response.status}`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
catch (error) {
|
|
759
|
-
throw this.handleError(error);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Sign out EVERY device-local account on this device by clearing every
|
|
764
|
-
* presented refresh-cookie slot at once. Revokes every family + clears
|
|
765
|
-
* every slot. Always succeeds (idempotent on unknown/garbage tokens).
|
|
766
|
-
*/
|
|
767
|
-
async logoutAllSessionsViaCookie() {
|
|
768
|
-
const url = `${this.getSessionBaseUrl().replace(/\/$/, '')}/auth/logout`;
|
|
769
|
-
try {
|
|
770
|
-
const response = await fetch(url, {
|
|
771
|
-
method: 'POST',
|
|
772
|
-
credentials: 'include',
|
|
773
|
-
headers: { Accept: 'application/json' },
|
|
774
|
-
});
|
|
775
|
-
if (!response.ok && response.status !== 401) {
|
|
776
|
-
throw new Error(`Logout-all failed with HTTP ${response.status}`);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
catch (error) {
|
|
780
|
-
throw this.handleError(error);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Internal: raw `POST /auth/refresh[?authuser=N]` call returning the
|
|
785
|
-
* minted access token. Returns `null` on 401 / non-2xx.
|
|
786
|
-
*
|
|
787
|
-
* @internal
|
|
788
|
-
*/
|
|
789
|
-
async _refreshCookieRaw(authuser) {
|
|
790
|
-
const base = this.getSessionBaseUrl().replace(/\/$/, '');
|
|
791
|
-
const url = typeof authuser === 'number'
|
|
792
|
-
? `${base}/auth/refresh?authuser=${encodeURIComponent(String(authuser))}`
|
|
793
|
-
: `${base}/auth/refresh`;
|
|
794
|
-
let response;
|
|
795
|
-
try {
|
|
796
|
-
response = await fetch(url, {
|
|
797
|
-
method: 'POST',
|
|
798
|
-
credentials: 'include',
|
|
799
|
-
headers: { Accept: 'application/json' },
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
catch (error) {
|
|
803
|
-
throw this.handleError(error);
|
|
804
|
-
}
|
|
805
|
-
if (!response.ok) {
|
|
806
|
-
return null;
|
|
807
|
-
}
|
|
808
|
-
const payload = (await response.json());
|
|
809
|
-
if (typeof payload.accessToken !== 'string' || !payload.accessToken) {
|
|
810
|
-
return null;
|
|
811
|
-
}
|
|
812
|
-
const expiresAt = typeof payload.expiresAt === 'string' ? payload.expiresAt : '';
|
|
813
|
-
if (typeof payload.authuser !== 'number') {
|
|
814
|
-
return null;
|
|
815
|
-
}
|
|
816
|
-
return {
|
|
817
|
-
accessToken: payload.accessToken,
|
|
818
|
-
expiresAt,
|
|
819
|
-
authuser: payload.authuser,
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
622
|
/**
|
|
823
623
|
* Internal: decode (without verifying) the `sessionId` claim from a
|
|
824
624
|
* server-signed access token. The server already verified the signature;
|
|
@@ -988,5 +788,70 @@ function OxyServicesAuthMixin(Base) {
|
|
|
988
788
|
async signInWithEmail(email, password, deviceName, deviceFingerprint) {
|
|
989
789
|
return this.signIn(email, password, deviceName, deviceFingerprint);
|
|
990
790
|
}
|
|
791
|
+
/**
|
|
792
|
+
* Device-first password sign-in. Unlike the legacy {@link signIn} (which
|
|
793
|
+
* assumes a one-step session and is kept intact for existing callers until
|
|
794
|
+
* the F4 cutover), this returns the FULL `POST /auth/login` contract — the
|
|
795
|
+
* discriminated {@link LoginResult}: either a 2FA challenge
|
|
796
|
+
* (`{ twoFactorRequired, loginToken }`) to complete via
|
|
797
|
+
* {@link completeTwoFactorSignIn}, or a session arm.
|
|
798
|
+
*
|
|
799
|
+
* `options.deviceToken` attributes the new session to the caller's existing
|
|
800
|
+
* device set (so an in-app login from a cross-apex, cookie-less web app
|
|
801
|
+
* still joins the same DeviceSession). On the session arm, a returned access
|
|
802
|
+
* token is planted immediately (mirroring {@link verifyChallenge}), so the
|
|
803
|
+
* caller has an authenticated client without a second round-trip.
|
|
804
|
+
*/
|
|
805
|
+
async passwordSignIn(identifier, password, options = {}) {
|
|
806
|
+
try {
|
|
807
|
+
const res = await this.makeRequest('POST', '/auth/login', {
|
|
808
|
+
identifier,
|
|
809
|
+
password,
|
|
810
|
+
deviceName: options.deviceName,
|
|
811
|
+
deviceFingerprint: options.deviceFingerprint,
|
|
812
|
+
deviceToken: options.deviceToken,
|
|
813
|
+
}, { cache: false });
|
|
814
|
+
const parsed = (0, contracts_1.safeParseContract)(contracts_1.loginResultSchema, res);
|
|
815
|
+
if (!parsed) {
|
|
816
|
+
throw new Error('auth/login returned an unexpected response shape');
|
|
817
|
+
}
|
|
818
|
+
if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
|
|
819
|
+
this.setTokens(parsed.accessToken);
|
|
820
|
+
}
|
|
821
|
+
return parsed;
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
throw this.handleError(error);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Complete a 2FA-gated sign-in started by {@link passwordSignIn}. Presents
|
|
829
|
+
* the short-lived `loginToken` with either a TOTP `token` or a `backupCode`
|
|
830
|
+
* to `POST /security/2fa/verify-login`, which must resolve to the session
|
|
831
|
+
* arm of {@link LoginResult} (a second 2FA challenge here is a protocol
|
|
832
|
+
* error). A returned access token is planted immediately.
|
|
833
|
+
*/
|
|
834
|
+
async completeTwoFactorSignIn(params) {
|
|
835
|
+
try {
|
|
836
|
+
const res = await this.makeRequest('POST', '/security/2fa/verify-login', {
|
|
837
|
+
loginToken: params.loginToken,
|
|
838
|
+
token: params.token,
|
|
839
|
+
backupCode: params.backupCode,
|
|
840
|
+
deviceToken: params.deviceToken,
|
|
841
|
+
deviceName: params.deviceName,
|
|
842
|
+
}, { cache: false });
|
|
843
|
+
const parsed = (0, contracts_1.safeParseContract)(contracts_1.loginResultSchema, res);
|
|
844
|
+
if (!parsed || 'twoFactorRequired' in parsed) {
|
|
845
|
+
throw new Error('security/2fa/verify-login returned an unexpected response shape');
|
|
846
|
+
}
|
|
847
|
+
if (parsed.accessToken) {
|
|
848
|
+
this.setTokens(parsed.accessToken);
|
|
849
|
+
}
|
|
850
|
+
return parsed;
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
throw this.handleError(error);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
991
856
|
};
|
|
992
857
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OxyServicesDeviceBootMixin = OxyServicesDeviceBootMixin;
|
|
4
|
+
/**
|
|
5
|
+
* Device-first bootstrap mixin (auth centralization, wave 1).
|
|
6
|
+
*
|
|
7
|
+
* The client half of the new device-first session bootstrap: the four network
|
|
8
|
+
* calls the cold boot (`coldBootV2`) and the unified refresh handler
|
|
9
|
+
* (`refresh.ts`) make, plus the URL builder for the cross-apex bootstrap hop.
|
|
10
|
+
* Every response is validated against the `@oxyhq/contracts` `deviceBoot`
|
|
11
|
+
* schemas via `safeParseContract`, so producer (oxy-api) and consumer cannot
|
|
12
|
+
* drift — an unexpected shape throws here rather than silently corrupting the
|
|
13
|
+
* persisted store.
|
|
14
|
+
*
|
|
15
|
+
* These methods are ADDITIVE and carry NO persistence or token-planting side
|
|
16
|
+
* effects of their own (except the bearer-authenticated calls that naturally
|
|
17
|
+
* flow through `HttpService`). The cold boot / refresh handler own persistence
|
|
18
|
+
* and `setTokens`, so the same network primitive can be reused from either
|
|
19
|
+
* without double-planting.
|
|
20
|
+
*/
|
|
21
|
+
const contracts_1 = require("@oxyhq/contracts");
|
|
22
|
+
function OxyServicesDeviceBootMixin(Base) {
|
|
23
|
+
return class extends Base {
|
|
24
|
+
/**
|
|
25
|
+
* Exchange a single-use boot `code` (from the `#oxy_boot` return fragment)
|
|
26
|
+
* for a token bundle. Origin-bound + GETDEL-burned server-side.
|
|
27
|
+
*
|
|
28
|
+
* @throws if the response does not match {@link authTokenBundleSchema}.
|
|
29
|
+
*/
|
|
30
|
+
async exchangeBootCode(code) {
|
|
31
|
+
try {
|
|
32
|
+
const res = await this.makeRequest('POST', '/auth/device/exchange', { code }, { cache: false });
|
|
33
|
+
const parsed = (0, contracts_1.safeParseContract)(contracts_1.authTokenBundleSchema, res);
|
|
34
|
+
if (!parsed) {
|
|
35
|
+
throw new Error('device/exchange returned an unexpected response shape');
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw this.handleError(error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Same-site fast path (`*.oxy.so` apps). Reads the first-party `oxy_device`
|
|
45
|
+
* cookie via a credentialed same-origin fetch and either resolves a full
|
|
46
|
+
* session bundle or reports the device is known-but-signed-out. Never
|
|
47
|
+
* redirects.
|
|
48
|
+
*
|
|
49
|
+
* @throws if the response matches neither arm of {@link webSessionResultSchema}.
|
|
50
|
+
*/
|
|
51
|
+
async requestWebSession() {
|
|
52
|
+
try {
|
|
53
|
+
const res = await this.makeRequest('POST', '/auth/device/web-session', undefined, { cache: false });
|
|
54
|
+
const parsed = (0, contracts_1.safeParseContract)(contracts_1.webSessionResultSchema, res);
|
|
55
|
+
if (!parsed) {
|
|
56
|
+
throw new Error('device/web-session returned an unexpected response shape');
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw this.handleError(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Rotate the persisted refresh-token family (web + native, one call). The
|
|
66
|
+
* caller (refresh handler / cold boot) plants + persists the rotated pair.
|
|
67
|
+
*
|
|
68
|
+
* @throws if the response does not match {@link tokenRefreshResponseSchema}.
|
|
69
|
+
*/
|
|
70
|
+
async refreshWithToken(refreshToken) {
|
|
71
|
+
try {
|
|
72
|
+
const res = await this.makeRequest('POST', '/auth/refresh-token', { refreshToken },
|
|
73
|
+
// `skipAuth`: refresh-token is body-authenticated and is called from
|
|
74
|
+
// inside the refresh handler — sending the near-expired bearer through
|
|
75
|
+
// the preflight would deadlock. See RequestOptions.skipAuth.
|
|
76
|
+
{ cache: false, skipAuth: true });
|
|
77
|
+
const parsed = (0, contracts_1.safeParseContract)(contracts_1.tokenRefreshResponseSchema, res);
|
|
78
|
+
if (!parsed) {
|
|
79
|
+
throw new Error('auth/refresh-token returned an unexpected response shape');
|
|
80
|
+
}
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw this.handleError(error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Issue (or rotate) the native channel's opaque device token. Bearer-gated —
|
|
89
|
+
* the server derives the deviceId from the JWT. Native apps mirror the
|
|
90
|
+
* returned token into the shared keychain via
|
|
91
|
+
* `KeyManager.setSharedDeviceToken`.
|
|
92
|
+
*
|
|
93
|
+
* @throws if the response does not match {@link deviceTokenIssueResponseSchema}.
|
|
94
|
+
*/
|
|
95
|
+
async issueNativeDeviceToken() {
|
|
96
|
+
try {
|
|
97
|
+
const res = await this.makeRequest('POST', '/auth/device/token', undefined, { cache: false });
|
|
98
|
+
const parsed = (0, contracts_1.safeParseContract)(contracts_1.deviceTokenIssueResponseSchema, res);
|
|
99
|
+
if (!parsed) {
|
|
100
|
+
throw new Error('auth/device/token returned an unexpected response shape');
|
|
101
|
+
}
|
|
102
|
+
return parsed.deviceToken;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw this.handleError(error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build the top-level `GET /auth/device/bootstrap` URL for the cross-apex
|
|
110
|
+
* hop. The server validates `return_to` against the trusted-origin lane and
|
|
111
|
+
* echoes `state` back in the return fragment for CSRF verification.
|
|
112
|
+
*/
|
|
113
|
+
buildBootstrapUrl(returnTo, state) {
|
|
114
|
+
const base = this.getBaseURL().replace(/\/+$/, '');
|
|
115
|
+
const params = new URLSearchParams({ return_to: returnTo, state });
|
|
116
|
+
return `${base}/auth/device/bootstrap?${params.toString()}`;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
package/dist/cjs/mixins/index.js
CHANGED
|
@@ -10,10 +10,6 @@ exports.MIXIN_PIPELINE = void 0;
|
|
|
10
10
|
exports.composeOxyServices = composeOxyServices;
|
|
11
11
|
const OxyServices_base_1 = require("../OxyServices.base");
|
|
12
12
|
const OxyServices_auth_1 = require("./OxyServices.auth");
|
|
13
|
-
const OxyServices_fedcm_1 = require("./OxyServices.fedcm");
|
|
14
|
-
const OxyServices_silent_1 = require("./OxyServices.silent");
|
|
15
|
-
const OxyServices_redirect_1 = require("./OxyServices.redirect");
|
|
16
|
-
const OxyServices_sso_1 = require("./OxyServices.sso");
|
|
17
13
|
const OxyServices_user_1 = require("./OxyServices.user");
|
|
18
14
|
const OxyServices_identity_1 = require("./OxyServices.identity");
|
|
19
15
|
const OxyServices_privacy_1 = require("./OxyServices.privacy");
|
|
@@ -35,30 +31,21 @@ const OxyServices_appData_1 = require("./OxyServices.appData");
|
|
|
35
31
|
const OxyServices_civic_1 = require("./OxyServices.civic");
|
|
36
32
|
const OxyServices_nodes_1 = require("./OxyServices.nodes");
|
|
37
33
|
const OxyServices_links_1 = require("./OxyServices.links");
|
|
34
|
+
const OxyServices_deviceBoot_1 = require("./OxyServices.deviceBoot");
|
|
38
35
|
/**
|
|
39
36
|
* Mixin pipeline - applied in order from first to last.
|
|
40
37
|
*
|
|
41
38
|
* Order matters for dependencies:
|
|
42
39
|
* 1. Base auth mixin first (required by all others)
|
|
43
|
-
* 2.
|
|
44
|
-
* 3.
|
|
45
|
-
* 4.
|
|
46
|
-
* 5. Utility mixin last (augments all)
|
|
40
|
+
* 2. User mixin (requires auth)
|
|
41
|
+
* 3. Feature mixins (can depend on user)
|
|
42
|
+
* 4. Utility mixin last (augments all)
|
|
47
43
|
*
|
|
48
44
|
* To add a new mixin: insert it at the appropriate position in this array.
|
|
49
45
|
*/
|
|
50
46
|
const MIXIN_PIPELINE = [
|
|
51
47
|
// Base authentication
|
|
52
48
|
OxyServices_auth_1.OxyServicesAuthMixin,
|
|
53
|
-
// Cross-domain authentication (web-only)
|
|
54
|
-
// - FedCM: Modern browser-native identity federation (Google-style)
|
|
55
|
-
// - Silent: iframe-based restore for first-party IdP hosts
|
|
56
|
-
// - Redirect: Traditional redirect-based authentication
|
|
57
|
-
OxyServices_fedcm_1.OxyServicesFedCMMixin,
|
|
58
|
-
OxyServices_silent_1.OxyServicesSilentAuthMixin,
|
|
59
|
-
OxyServices_redirect_1.OxyServicesRedirectAuthMixin,
|
|
60
|
-
// Central cross-domain SSO (opaque-code exchange).
|
|
61
|
-
OxyServices_sso_1.OxyServicesSsoMixin,
|
|
62
49
|
// User management (requires auth)
|
|
63
50
|
OxyServices_user_1.OxyServicesUserMixin,
|
|
64
51
|
// Self-sovereign identity (DID, signed records, auth-method ↔ VM mapping)
|
|
@@ -92,6 +79,10 @@ const MIXIN_PIPELINE = [
|
|
|
92
79
|
// Link previews / unfurls: SDK-owned link-metadata resolution via oxy-api,
|
|
93
80
|
// so apps stop scraping link metadata locally.
|
|
94
81
|
OxyServices_links_1.OxyServicesLinksMixin,
|
|
82
|
+
// Device-first session bootstrap: the client half of the new
|
|
83
|
+
// /auth/device/* + /auth/refresh-token surface (exchange, web-session,
|
|
84
|
+
// refresh, native device-token, bootstrap-url builder).
|
|
85
|
+
OxyServices_deviceBoot_1.OxyServicesDeviceBootMixin,
|
|
95
86
|
// Utility (last, can use all above)
|
|
96
87
|
OxyServices_utility_1.OxyServicesUtilityMixin,
|
|
97
88
|
];
|
package/dist/cjs/server/index.js
CHANGED
|
@@ -50,8 +50,8 @@ Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function
|
|
|
50
50
|
// SOURCE OF TRUTH shared with the IdP worker and the client FAPI auto-detect.
|
|
51
51
|
// Pure host handling (no browser deps), so it is safe on the server subpath and
|
|
52
52
|
// lets `@oxyhq/api` derive `auth.<apex>` without duplicating PSL logic.
|
|
53
|
-
var
|
|
54
|
-
Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return
|
|
53
|
+
var registrableApex_1 = require("../utils/registrableApex");
|
|
54
|
+
Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return registrableApex_1.registrableApex; } });
|
|
55
55
|
// The single RP callback path the IdP redirects back to. A pure wire-contract
|
|
56
56
|
// constant (no browser deps at module top level), re-used server-side so the
|
|
57
57
|
// `/sso/establish-token` `return_to` cannot drift from what `/sso/establish`
|
|
@@ -40,7 +40,15 @@ class SessionClient {
|
|
|
40
40
|
loggerUtils_1.logger.warn('[SessionClient] discarded invalid session state');
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
// The revision is monotone ONLY within a single deviceId. When the incoming
|
|
44
|
+
// state belongs to a DIFFERENT device than the currently-applied one, reset
|
|
45
|
+
// the baseline and accept it regardless of revision: a freshly-converged
|
|
46
|
+
// device (low revision) must not lose last-writer-wins to a stale, higher-
|
|
47
|
+
// revision state from a retired device. Only when the deviceIds MATCH is the
|
|
48
|
+
// `revision <= current` guard a valid staleness check.
|
|
49
|
+
if (this.state &&
|
|
50
|
+
next.deviceId === this.state.deviceId &&
|
|
51
|
+
next.revision <= this.state.revision) {
|
|
44
52
|
return false;
|
|
45
53
|
}
|
|
46
54
|
this.state = next;
|
|
@@ -50,6 +58,16 @@ class SessionClient {
|
|
|
50
58
|
loggerUtils_1.logger.warn('[SessionClient] ensureActiveToken failed', { component: 'SessionClient' }, error);
|
|
51
59
|
});
|
|
52
60
|
}
|
|
61
|
+
// A device signout-all leaves zero accounts — tell the provider to clear
|
|
62
|
+
// the persisted store so a reload does not try to restore a dead session.
|
|
63
|
+
if (next.accounts.length === 0 && this.options.onUnauthenticated) {
|
|
64
|
+
try {
|
|
65
|
+
this.options.onUnauthenticated();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
loggerUtils_1.logger.error('[SessionClient] onUnauthenticated threw', error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
53
71
|
return true;
|
|
54
72
|
}
|
|
55
73
|
/**
|
|
@@ -96,6 +114,27 @@ class SessionClient {
|
|
|
96
114
|
const res = await this.host.makeRequest('POST', '/session/device/add', undefined, { cache: false });
|
|
97
115
|
this.applySync(res);
|
|
98
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Register the just-signed-in account into the device set AND make it the
|
|
119
|
+
* ACTIVE account — the explicit user-intent activation a sign-in UI performs.
|
|
120
|
+
*
|
|
121
|
+
* `addCurrentAccount` alone honors the server's `activate:'if-empty'` policy
|
|
122
|
+
* (a new account does NOT steal focus from an already-active one), which is
|
|
123
|
+
* correct for a background/silent add but wrong right after a deliberate
|
|
124
|
+
* sign-in. This adds, then switches to the target so the UI lands on the
|
|
125
|
+
* account the user just authenticated.
|
|
126
|
+
*
|
|
127
|
+
* @param accountId - The signed-in account id (e.g. `session.user.id`). When
|
|
128
|
+
* omitted, falls back to the host's current-account ref. If neither
|
|
129
|
+
* resolves, the add still applies and no switch is performed.
|
|
130
|
+
*/
|
|
131
|
+
async registerAndActivate(accountId) {
|
|
132
|
+
await this.addCurrentAccount();
|
|
133
|
+
const target = accountId ?? this.host.getCurrentAccountId();
|
|
134
|
+
if (target && this.state?.activeAccountId !== target) {
|
|
135
|
+
await this.switchAccount(target);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
99
138
|
async start() {
|
|
100
139
|
if (this.started)
|
|
101
140
|
return;
|