@oxyhq/core 5.5.0 → 6.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.
Files changed (181) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +6 -3
  3. package/dist/cjs/OxyServices.base.js +7 -102
  4. package/dist/cjs/boot/coldBootV2.js +350 -0
  5. package/dist/cjs/boot/deviceBootReturn.js +152 -0
  6. package/dist/cjs/crypto/keyManager.js +95 -0
  7. package/dist/cjs/i18n/locales/en-US.json +13 -1
  8. package/dist/cjs/i18n/locales/es-ES.json +13 -1
  9. package/dist/cjs/i18n/locales/locales/en-US.json +13 -1
  10. package/dist/cjs/i18n/locales/locales/es-ES.json +13 -1
  11. package/dist/cjs/index.js +47 -44
  12. package/dist/cjs/mixins/OxyServices.accounts.js +6 -13
  13. package/dist/cjs/mixins/OxyServices.auth.js +66 -201
  14. package/dist/cjs/mixins/OxyServices.authorizedApps.js +38 -0
  15. package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
  16. package/dist/cjs/mixins/index.js +13 -17
  17. package/dist/cjs/session/SessionClient.js +40 -1
  18. package/dist/cjs/session/authStateStore.js +284 -0
  19. package/dist/cjs/session/refresh.js +264 -0
  20. package/dist/cjs/utils/accountUtils.js +1 -55
  21. package/dist/cjs/utils/authWebUrl.js +6 -15
  22. package/dist/cjs/utils/fapiAutoDetect.js +16 -64
  23. package/dist/cjs/utils/platform.js +19 -0
  24. package/dist/cjs/utils/ssoBounce.js +15 -362
  25. package/dist/cjs/utils/validationUtils.js +57 -0
  26. package/dist/esm/.tsbuildinfo +1 -1
  27. package/dist/esm/HttpService.js +6 -3
  28. package/dist/esm/OxyServices.base.js +7 -102
  29. package/dist/esm/boot/coldBootV2.js +344 -0
  30. package/dist/esm/boot/deviceBootReturn.js +146 -0
  31. package/dist/esm/crypto/keyManager.js +95 -0
  32. package/dist/esm/i18n/locales/en-US.json +13 -1
  33. package/dist/esm/i18n/locales/es-ES.json +13 -1
  34. package/dist/esm/i18n/locales/locales/en-US.json +13 -1
  35. package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
  36. package/dist/esm/index.js +28 -18
  37. package/dist/esm/mixins/OxyServices.accounts.js +6 -13
  38. package/dist/esm/mixins/OxyServices.auth.js +66 -201
  39. package/dist/esm/mixins/OxyServices.authorizedApps.js +35 -0
  40. package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
  41. package/dist/esm/mixins/index.js +13 -17
  42. package/dist/esm/session/SessionClient.js +40 -1
  43. package/dist/esm/session/authStateStore.js +278 -0
  44. package/dist/esm/session/refresh.js +257 -0
  45. package/dist/esm/utils/accountUtils.js +0 -53
  46. package/dist/esm/utils/authWebUrl.js +6 -14
  47. package/dist/esm/utils/fapiAutoDetect.js +16 -63
  48. package/dist/esm/utils/platform.js +18 -0
  49. package/dist/esm/utils/ssoBounce.js +14 -345
  50. package/dist/esm/utils/validationUtils.js +56 -0
  51. package/dist/types/.tsbuildinfo +1 -1
  52. package/dist/types/HttpService.d.ts +13 -0
  53. package/dist/types/OxyServices.base.d.ts +0 -52
  54. package/dist/types/OxyServices.d.ts +0 -25
  55. package/dist/types/boot/coldBootV2.d.ts +76 -0
  56. package/dist/types/boot/deviceBootReturn.d.ts +83 -0
  57. package/dist/types/crypto/keyManager.d.ts +21 -0
  58. package/dist/types/index.d.ts +15 -21
  59. package/dist/types/mixins/OxyServices.accounts.d.ts +0 -2
  60. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  61. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  62. package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
  63. package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
  64. package/dist/types/mixins/{OxyServices.redirect.d.ts → OxyServices.authorizedApps.d.ts} +35 -33
  65. package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
  66. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  67. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  68. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
  69. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  70. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  71. package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
  72. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  73. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  74. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  75. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
  76. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  77. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  78. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  79. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  80. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  81. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  82. package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
  83. package/dist/types/mixins/index.d.ts +6 -9
  84. package/dist/types/models/interfaces.d.ts +0 -67
  85. package/dist/types/session/SessionClient.d.ts +25 -0
  86. package/dist/types/session/authStateStore.d.ts +119 -0
  87. package/dist/types/session/refresh.d.ts +93 -0
  88. package/dist/types/utils/accountUtils.d.ts +0 -14
  89. package/dist/types/utils/authWebUrl.d.ts +6 -12
  90. package/dist/types/utils/fapiAutoDetect.d.ts +15 -38
  91. package/dist/types/utils/platform.d.ts +14 -0
  92. package/dist/types/utils/ssoBounce.d.ts +14 -280
  93. package/dist/types/utils/validationUtils.d.ts +15 -0
  94. package/package.json +2 -2
  95. package/src/HttpService.ts +19 -3
  96. package/src/OxyServices.base.ts +7 -112
  97. package/src/OxyServices.ts +0 -38
  98. package/src/boot/__tests__/coldBootV2.test.ts +317 -0
  99. package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
  100. package/src/boot/coldBootV2.ts +426 -0
  101. package/src/boot/deviceBootReturn.ts +195 -0
  102. package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
  103. package/src/crypto/keyManager.ts +101 -0
  104. package/src/i18n/locales/en-US.json +13 -1
  105. package/src/i18n/locales/es-ES.json +13 -1
  106. package/src/index.ts +74 -65
  107. package/src/mixins/OxyServices.accounts.ts +6 -13
  108. package/src/mixins/OxyServices.auth.ts +78 -253
  109. package/src/mixins/OxyServices.authorizedApps.ts +75 -0
  110. package/src/mixins/OxyServices.deviceBoot.ts +146 -0
  111. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
  112. package/src/mixins/__tests__/accounts.test.ts +17 -44
  113. package/src/mixins/__tests__/authorizedApps.test.ts +63 -0
  114. package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
  115. package/src/mixins/index.ts +16 -22
  116. package/src/models/interfaces.ts +0 -79
  117. package/src/session/SessionClient.ts +52 -1
  118. package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
  119. package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
  120. package/src/session/__tests__/SessionClient.state.test.ts +18 -5
  121. package/src/session/__tests__/authStateStore.test.ts +209 -0
  122. package/src/session/__tests__/refresh.test.ts +256 -0
  123. package/src/session/authStateStore.ts +335 -0
  124. package/src/session/refresh.ts +334 -0
  125. package/src/utils/__tests__/authWebUrl.test.ts +5 -29
  126. package/src/utils/__tests__/fapiAutoDetect.test.ts +5 -126
  127. package/src/utils/__tests__/validationUtils.test.ts +30 -0
  128. package/src/utils/accountUtils.ts +0 -62
  129. package/src/utils/authWebUrl.ts +6 -15
  130. package/src/utils/fapiAutoDetect.ts +16 -60
  131. package/src/utils/platform.ts +21 -0
  132. package/src/utils/ssoBounce.ts +14 -393
  133. package/src/utils/validationUtils.ts +62 -0
  134. package/dist/cjs/AuthManager.js +0 -1110
  135. package/dist/cjs/AuthManagerTypes.js +0 -13
  136. package/dist/cjs/CrossDomainAuth.js +0 -206
  137. package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
  138. package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
  139. package/dist/cjs/mixins/OxyServices.silent.js +0 -204
  140. package/dist/cjs/mixins/OxyServices.sso.js +0 -208
  141. package/dist/cjs/utils/ssoEstablish.js +0 -110
  142. package/dist/cjs/utils/ssoReturn.js +0 -275
  143. package/dist/esm/AuthManager.js +0 -1105
  144. package/dist/esm/AuthManagerTypes.js +0 -12
  145. package/dist/esm/CrossDomainAuth.js +0 -201
  146. package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
  147. package/dist/esm/mixins/OxyServices.redirect.js +0 -92
  148. package/dist/esm/mixins/OxyServices.silent.js +0 -202
  149. package/dist/esm/mixins/OxyServices.sso.js +0 -204
  150. package/dist/esm/utils/ssoEstablish.js +0 -107
  151. package/dist/esm/utils/ssoReturn.js +0 -271
  152. package/dist/types/AuthManager.d.ts +0 -380
  153. package/dist/types/AuthManagerTypes.d.ts +0 -81
  154. package/dist/types/CrossDomainAuth.d.ts +0 -164
  155. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
  156. package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
  157. package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
  158. package/dist/types/utils/ssoEstablish.d.ts +0 -85
  159. package/dist/types/utils/ssoReturn.d.ts +0 -156
  160. package/src/AuthManager.ts +0 -1269
  161. package/src/AuthManagerTypes.ts +0 -86
  162. package/src/CrossDomainAuth.ts +0 -243
  163. package/src/__tests__/authManager.cookiePath.test.ts +0 -390
  164. package/src/__tests__/authManager.security.test.ts +0 -377
  165. package/src/__tests__/crossDomainAuth.test.ts +0 -116
  166. package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
  167. package/src/mixins/OxyServices.fedcm.ts +0 -1026
  168. package/src/mixins/OxyServices.redirect.ts +0 -122
  169. package/src/mixins/OxyServices.silent.ts +0 -272
  170. package/src/mixins/OxyServices.sso.ts +0 -261
  171. package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
  172. package/src/mixins/__tests__/fedcm.test.ts +0 -667
  173. package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
  174. package/src/mixins/__tests__/silent.test.ts +0 -102
  175. package/src/mixins/__tests__/sso.test.ts +0 -228
  176. package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
  177. package/src/utils/__tests__/ssoBounce.test.ts +0 -219
  178. package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
  179. package/src/utils/__tests__/ssoReturn.test.ts +0 -276
  180. package/src/utils/ssoEstablish.ts +0 -174
  181. 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,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OxyServicesAuthorizedAppsMixin = OxyServicesAuthorizedAppsMixin;
4
+ function OxyServicesAuthorizedAppsMixin(Base) {
5
+ return class extends Base {
6
+ /**
7
+ * List the authenticated user's authorized RP apps — the intersection of the
8
+ * user's FedCM grants and the currently-approved RP catalog. Powers the
9
+ * "Connected apps" management UI. Requires a real user session.
10
+ */
11
+ async listAuthorizedApps() {
12
+ try {
13
+ const response = await this.makeRequest('GET', '/fedcm/me/authorized-apps', undefined, {
14
+ cache: true,
15
+ cacheTTL: 30 * 1000, // 30 second cache — short, this drives a manageable UI
16
+ });
17
+ return response?.apps ?? [];
18
+ }
19
+ catch (error) {
20
+ throw this.handleError(error);
21
+ }
22
+ }
23
+ /**
24
+ * Revoke the authenticated user's authorization for a specific RP origin.
25
+ * The corresponding cache entry is invalidated so a subsequent
26
+ * `listAuthorizedApps()` sees fresh data.
27
+ */
28
+ async revokeAuthorizedApp(origin) {
29
+ try {
30
+ await this.makeRequest('DELETE', `/fedcm/me/authorized-apps/${encodeURIComponent(origin)}`, undefined, { cache: false });
31
+ this.clearCacheEntry('GET:/fedcm/me/authorized-apps');
32
+ }
33
+ catch (error) {
34
+ throw this.handleError(error);
35
+ }
36
+ }
37
+ };
38
+ }
@@ -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
+ }
@@ -10,10 +10,7 @@ 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");
13
+ const OxyServices_authorizedApps_1 = require("./OxyServices.authorizedApps");
17
14
  const OxyServices_user_1 = require("./OxyServices.user");
18
15
  const OxyServices_identity_1 = require("./OxyServices.identity");
19
16
  const OxyServices_privacy_1 = require("./OxyServices.privacy");
@@ -35,30 +32,25 @@ const OxyServices_appData_1 = require("./OxyServices.appData");
35
32
  const OxyServices_civic_1 = require("./OxyServices.civic");
36
33
  const OxyServices_nodes_1 = require("./OxyServices.nodes");
37
34
  const OxyServices_links_1 = require("./OxyServices.links");
35
+ const OxyServices_deviceBoot_1 = require("./OxyServices.deviceBoot");
38
36
  /**
39
37
  * Mixin pipeline - applied in order from first to last.
40
38
  *
41
39
  * Order matters for dependencies:
42
40
  * 1. Base auth mixin first (required by all others)
43
- * 2. Cross-domain auth mixins (FedCM, silent iframe, Redirect)
44
- * 3. User mixin (requires auth)
45
- * 4. Feature mixins (can depend on user)
46
- * 5. Utility mixin last (augments all)
41
+ * 2. User mixin (requires auth)
42
+ * 3. Feature mixins (can depend on user)
43
+ * 4. Utility mixin last (augments all)
47
44
  *
48
45
  * To add a new mixin: insert it at the appropriate position in this array.
49
46
  */
50
47
  const MIXIN_PIPELINE = [
51
48
  // Base authentication
52
49
  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,
50
+ // Legacy FedCM "Connected apps" management (list/revoke authorized RP grants).
51
+ // The FedCM sign-in surface was removed in the device-first cutover; only
52
+ // this management pair survives until the AppGrant migration.
53
+ OxyServices_authorizedApps_1.OxyServicesAuthorizedAppsMixin,
62
54
  // User management (requires auth)
63
55
  OxyServices_user_1.OxyServicesUserMixin,
64
56
  // Self-sovereign identity (DID, signed records, auth-method ↔ VM mapping)
@@ -92,6 +84,10 @@ const MIXIN_PIPELINE = [
92
84
  // Link previews / unfurls: SDK-owned link-metadata resolution via oxy-api,
93
85
  // so apps stop scraping link metadata locally.
94
86
  OxyServices_links_1.OxyServicesLinksMixin,
87
+ // Device-first session bootstrap: the client half of the new
88
+ // /auth/device/* + /auth/refresh-token surface (exchange, web-session,
89
+ // refresh, native device-token, bootstrap-url builder).
90
+ OxyServices_deviceBoot_1.OxyServicesDeviceBootMixin,
95
91
  // Utility (last, can use all above)
96
92
  OxyServices_utility_1.OxyServicesUtilityMixin,
97
93
  ];
@@ -40,7 +40,15 @@ class SessionClient {
40
40
  loggerUtils_1.logger.warn('[SessionClient] discarded invalid session state');
41
41
  return false;
42
42
  }
43
- if (this.state && next.revision <= this.state.revision) {
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;