@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
@@ -1,823 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OxyServicesFedCMMixin = OxyServicesFedCMMixin;
4
- exports.FedCMMixin = OxyServicesFedCMMixin;
5
- const OxyServices_errors_1 = require("../OxyServices.errors");
6
- const debugUtils_1 = require("../shared/utils/debugUtils");
7
- const userIdentity_1 = require("../utils/userIdentity");
8
- const debug = (0, debugUtils_1.createDebugLogger)('FedCM');
9
- // Modern (W3C spec) → legacy (Chrome 125–131) mode value mapping. Used to
10
- // retry a credential request when an older browser rejects the modern enum.
11
- const MODERN_TO_LEGACY_MODE = {
12
- active: 'button',
13
- passive: 'widget',
14
- };
15
- // Legacy → modern mapping so callers may pass either spelling.
16
- const LEGACY_TO_MODERN_MODE = {
17
- button: 'active',
18
- widget: 'passive',
19
- };
20
- /**
21
- * Normalise any accepted mode value to the modern W3C spelling
22
- * (`'active'`/`'passive'`), which is what is sent to the browser first.
23
- */
24
- function toModernMode(mode) {
25
- return mode === 'button' || mode === 'widget' ? LEGACY_TO_MODERN_MODE[mode] : mode;
26
- }
27
- /**
28
- * Detect the synchronous `TypeError` a pre-spec browser throws when it does not
29
- * recognise a modern `mode` enum value (e.g. Chrome 125–131 rejecting
30
- * `'active'`/`'passive'`). Such a browser only understands the legacy
31
- * `'button'`/`'widget'` values, so the caller can retry with those.
32
- */
33
- function isUnknownModeEnumError(error) {
34
- if (!(error instanceof TypeError))
35
- return false;
36
- const message = error.message.toLowerCase();
37
- return (message.includes('identitycredentialrequestoptionsmode') ||
38
- ((message.includes('active') || message.includes('passive')) &&
39
- (message.includes('enum') || message.includes('not a valid'))));
40
- }
41
- /**
42
- * Detect a `navigator.credentials.get` rejection that is consistent with
43
- * "the supplied loginHint matched no account at the IdP".
44
- *
45
- * When an RP passes a `loginHint` and the IdP returns accounts but NONE of them
46
- * declare that hint in their `login_hints`, Chrome filters every account out,
47
- * greys it in the chooser ("You can't sign in using this account"), logs
48
- * "Accounts were received, but none matched the login hint…", and ultimately
49
- * rejects the credential request — surfacing as a `NotAllowedError` /
50
- * `AbortError` (the same shape as a user-cancelled or timed-out request). A
51
- * stale hint left over from a previously-signed-in/test account therefore hard
52
- * -blocks sign-in.
53
- *
54
- * We can only safely apply the clear-and-retry recovery when a `loginHint` was
55
- * actually supplied; without one this is just a normal cancel/timeout and must
56
- * NOT be retried. Callers gate on `hadLoginHint` before calling this.
57
- */
58
- function isPossibleHintMismatchError(error) {
59
- if (!(error instanceof Error))
60
- return false;
61
- // FedCM surfaces a filtered-out / no-eligible-account outcome as
62
- // NotAllowedError (current Chrome) or AbortError (our own timeout abort while
63
- // the chooser had no selectable account). Both are indistinguishable from a
64
- // genuine user cancel at the API level, so the gate on "a hint was supplied"
65
- // (in the caller) is what makes the retry safe and targeted.
66
- return error.name === 'NotAllowedError' || error.name === 'AbortError';
67
- }
68
- const FEDCM_LOGIN_HINT_KEY = 'oxy_fedcm_login_hint';
69
- // Global lock to prevent concurrent FedCM requests
70
- // FedCM only allows one navigator.credentials.get request at a time
71
- let fedCMRequestInProgress = false;
72
- let fedCMRequestPromise = null;
73
- let currentMediationMode = null;
74
- // AbortController of the in-flight request, exposed at module scope so an
75
- // arriving INTERACTIVE request can abort a slow/hung SILENT one instead of
76
- // blocking on it (see requestIdentityCredential). Set when a request starts,
77
- // cleared in that request's `finally`.
78
- let fedCMActiveController = null;
79
- /**
80
- * Federated Credential Management (FedCM) Authentication Mixin
81
- *
82
- * Implements the modern browser-native identity federation API that enables
83
- * Google-style cross-domain authentication without third-party cookies.
84
- *
85
- * Browser Support:
86
- * - Chrome 108+
87
- * - Safari 16.4+
88
- * - Edge 108+
89
- * - Firefox: Not yet supported (fallback required)
90
- *
91
- * Key Features:
92
- * - No redirects or secondary windows required
93
- * - Browser-native UI prompts
94
- * - Privacy-preserving (IdP can't track users)
95
- * - Automatic SSO across domains
96
- * - Silent re-authentication support
97
- *
98
- * @see https://developer.mozilla.org/en-US/docs/Web/API/FedCM_API
99
- */
100
- function OxyServicesFedCMMixin(Base) {
101
- var _a;
102
- return _a = class extends Base {
103
- constructor(...args) {
104
- super(...args);
105
- }
106
- resolveFedcmConfigUrl() {
107
- // `DEFAULT_CONFIG_URL` is a static on the composed class; read it off the
108
- // most-derived constructor through a typed cast (not `any`).
109
- const configCtor = this.constructor;
110
- return this.config.authWebUrl
111
- ? `${this.config.authWebUrl}/fedcm.json`
112
- : configCtor.DEFAULT_CONFIG_URL;
113
- }
114
- /**
115
- * Check if FedCM is supported in the current browser
116
- */
117
- static isFedCMSupported() {
118
- if (typeof window === 'undefined')
119
- return false;
120
- return 'IdentityCredential' in window && 'navigator' in window && 'credentials' in navigator;
121
- }
122
- /**
123
- * Instance method to check FedCM support
124
- */
125
- isFedCMSupported() {
126
- return this.constructor.isFedCMSupported();
127
- }
128
- /**
129
- * Sign in using FedCM (Federated Credential Management API)
130
- *
131
- * This provides a Google-style authentication experience:
132
- * - Browser shows native "Sign in with Oxy" prompt
133
- * - No redirect or secondary window required
134
- * - User approves → credential exchange happens in browser
135
- * - All apps automatically get SSO after first sign-in
136
- *
137
- * @param options - Authentication options
138
- * @returns Session with access token and user data
139
- * @throws {OxyAuthenticationError} If FedCM not supported or user cancels
140
- *
141
- * @example
142
- * ```typescript
143
- * try {
144
- * const session = await oxyServices.signInWithFedCM();
145
- * const user = session.user;
146
- * } catch (error) {
147
- * // Fallback to redirect auth
148
- * oxyServices.signInWithRedirect();
149
- * }
150
- * ```
151
- */
152
- async signInWithFedCM(options = {}) {
153
- if (!this.isFedCMSupported()) {
154
- throw new OxyServices_errors_1.OxyAuthenticationError('FedCM not supported in this browser. Please update your browser or use an alternative sign-in method.');
155
- }
156
- // Use provided loginHint, or fall back to stored last-used account ID.
157
- const initialLoginHint = options.loginHint || this.getStoredLoginHint();
158
- try {
159
- return await this.attemptInteractiveSignIn(options, initialLoginHint);
160
- }
161
- catch (error) {
162
- // A STALE loginHint (e.g. left over from a previously-signed-in or test
163
- // account) that matches no account at the IdP makes Chrome filter out
164
- // every account and reject the request — indistinguishable from a user
165
- // cancel. When that happens AND we supplied a hint, clear the bad hint
166
- // and retry the credential request ONCE with no hint, which lets the
167
- // chooser surface the genuinely available account(s). We only do this for
168
- // a hint we pulled from storage (not a caller-supplied one), and only
169
- // once, so a real cancel never loops.
170
- const usedStoredHint = !!initialLoginHint && !options.loginHint;
171
- if (usedStoredHint && isPossibleHintMismatchError(error)) {
172
- debug.log('Interactive sign-in: stored loginHint matched no account; clearing it and retrying without a hint');
173
- this.clearLoginHint();
174
- return await this.attemptInteractiveSignIn(options, undefined);
175
- }
176
- throw this.normalizeInteractiveSignInError(error);
177
- }
178
- }
179
- /**
180
- * Run a single interactive FedCM credential request + token exchange for the
181
- * given (possibly undefined) loginHint. A successful exchange plants the
182
- * access token and persists the user id as the future loginHint — the hint is
183
- * therefore only ever stored after a GENUINELY successful sign-in, never
184
- * speculatively.
185
- *
186
- * @private
187
- */
188
- async attemptInteractiveSignIn(options, loginHint) {
189
- // Prefer a server-minted, origin-bound nonce so the downstream
190
- // `/fedcm/exchange` can validate it. A caller-supplied nonce is
191
- // respected as-is for advanced use cases.
192
- const nonce = options.nonce || (await this.getFedcmNonce());
193
- const clientId = this.getClientId();
194
- debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
195
- // Request credential from browser's native identity flow.
196
- // mode: 'active' signals this is a user-gesture-initiated (button) flow.
197
- // 'active' is the current W3C spec value; requestIdentityCredential
198
- // transparently retries with the legacy 'button' value for Chrome 125–131.
199
- const credential = await this.requestIdentityCredential({
200
- configURL: this.resolveFedcmConfigUrl(),
201
- clientId,
202
- nonce,
203
- context: options.context,
204
- loginHint,
205
- mode: 'active',
206
- });
207
- if (!credential || !credential.token) {
208
- throw new OxyServices_errors_1.OxyAuthenticationError('No credential received from browser');
209
- }
210
- debug.log('Interactive sign-in: Got credential, exchanging for session');
211
- // Exchange FedCM ID token for Oxy session
212
- const session = await this.exchangeIdTokenForSession(credential.token);
213
- // Store the access token in HttpService. Refresh stays in the httpOnly
214
- // cookie slot set by the API.
215
- if (session?.accessToken) {
216
- this.httpService.setTokens(session.accessToken);
217
- }
218
- // Store the user ID as loginHint for future FedCM requests — only now, after
219
- // a real successful exchange, so we never persist a hint that cannot resolve.
220
- if (session?.user?.id) {
221
- this.storeLoginHint(session.user.id);
222
- }
223
- debug.log('Interactive sign-in: Success!', { userId: session?.user?.id });
224
- return session;
225
- }
226
- /**
227
- * Map a raw FedCM/exchange failure to a user-facing {@link OxyAuthenticationError}
228
- * (or pass it through). Extracted so the clear-and-retry path can reuse the
229
- * exact same error normalisation as the first attempt.
230
- *
231
- * @private
232
- */
233
- normalizeInteractiveSignInError(error) {
234
- debug.log('Interactive sign-in failed:', error);
235
- const errorMessage = error instanceof Error ? error.message : String(error);
236
- // FedCM aborts/network failures surface as DOMException/Error instances,
237
- // both of which carry a `name`. Anything else has no meaningful name.
238
- const errorName = error instanceof Error ? error.name : '';
239
- if (errorName === 'AbortError') {
240
- return new OxyServices_errors_1.OxyAuthenticationError('Sign-in was cancelled by user');
241
- }
242
- if (errorName === 'NetworkError') {
243
- return new OxyServices_errors_1.OxyAuthenticationError('Network error during sign-in. Please check your connection.');
244
- }
245
- if (errorMessage.includes('multiple accounts')) {
246
- return new OxyServices_errors_1.OxyAuthenticationError('Please sign out and sign in again to use FedCM with a single account');
247
- }
248
- if (errorMessage.includes('retrieving a token') || errorMessage.includes('Error retrieving')) {
249
- debug.error('FedCM token retrieval error - this may be a browser or IdP configuration issue');
250
- return new OxyServices_errors_1.OxyAuthenticationError('Authentication failed. Please try again or use an alternative sign-in method.');
251
- }
252
- return error;
253
- }
254
- /**
255
- * Silent sign-in using FedCM
256
- *
257
- * Attempts to automatically re-authenticate the user without any UI.
258
- * This is what enables "instant sign-in" across all Oxy domains after
259
- * the user has signed in once.
260
- *
261
- * The browser will:
262
- * 1. Check if user has previously signed in to Oxy
263
- * 2. Check if user is still signed in at auth.oxy.so
264
- * 3. If yes, automatically provide credential without prompting
265
- *
266
- * @returns Session if user is already signed in, null otherwise
267
- *
268
- * @example
269
- * ```typescript
270
- * // On app startup
271
- * useEffect(() => {
272
- * const checkAuth = async () => {
273
- * const session = await oxyServices.silentSignInWithFedCM();
274
- * if (session) {
275
- * setUser(session.user);
276
- * } else {
277
- * // Show sign-in button
278
- * }
279
- * };
280
- * checkAuth();
281
- * }, []);
282
- * ```
283
- */
284
- async silentSignInWithFedCM() {
285
- if (!this.isFedCMSupported()) {
286
- debug.log('Silent SSO: FedCM not supported in this browser');
287
- return null;
288
- }
289
- const clientId = this.getClientId();
290
- debug.log('Silent SSO: Starting for', clientId);
291
- // Only try silent mediation (no UI) - works if user previously consented.
292
- // We intentionally do NOT fall back to optional mediation here because
293
- // this runs on app startup — showing browser UI without user action is bad UX.
294
- // Optional/interactive mediation should only happen when the user clicks "Sign In".
295
- let credential = null;
296
- const loginHint = this.getStoredLoginHint();
297
- // Fast-skip: with no stored login hint this browser has never completed a
298
- // FedCM sign-in for any Oxy account, so silent mediation cannot return a
299
- // credential — the IdP has nothing to silently re-issue. Doing the full
300
- // round-trip anyway (mint a nonce via `POST /fedcm/nonce`, then a
301
- // `navigator.credentials.get` that aborts after `FEDCM_SILENT_TIMEOUT`) is
302
- // pure latency in the cold-boot critical path. Return `null` immediately so
303
- // the next cold-boot step (stored-session / iframe / bounce) runs without
304
- // the wasted nonce mint + abort wait. A genuinely associated browser always
305
- // has a hint (it is stored only after a real exchange), so this never skips
306
- // a recoverable session.
307
- if (!loginHint) {
308
- debug.log('Silent SSO: No stored login hint — skipping silent mediation (no association on this browser)');
309
- return null;
310
- }
311
- try {
312
- // Server-minted, origin-bound nonce — required for `/fedcm/exchange`
313
- // to accept the resulting ID token (anti-replay binding).
314
- const nonce = await this.getFedcmNonce();
315
- debug.log('Silent SSO: Attempting silent mediation...', loginHint ? `(hint: ${loginHint})` : '');
316
- credential = await this.requestIdentityCredential({
317
- configURL: this.resolveFedcmConfigUrl(),
318
- clientId,
319
- nonce,
320
- loginHint,
321
- mediation: 'silent',
322
- });
323
- debug.log('Silent SSO: Silent mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
324
- }
325
- catch (silentError) {
326
- const errorName = silentError instanceof Error ? silentError.name : 'Unknown';
327
- const errorMessage = silentError instanceof Error ? silentError.message : String(silentError);
328
- // Handle specific FedCM errors with better logging
329
- if (errorMessage.includes('multiple accounts')) {
330
- debug.log('Silent SSO: User has used multiple accounts - silent mediation not available');
331
- debug.log('Silent SSO: User needs to explicitly sign in to choose account');
332
- }
333
- else if (errorMessage.includes('conditions')) {
334
- debug.log('Silent SSO: Conditions not met (user may not be logged in at IdP or not in approved_clients)');
335
- }
336
- else {
337
- debug.log('Silent SSO: Silent mediation failed:', { name: errorName, message: errorMessage });
338
- }
339
- return null;
340
- }
341
- if (!credential || !credential.token) {
342
- debug.log('Silent SSO: No credential returned (user not logged in at IdP or hasn\'t consented)');
343
- return null;
344
- }
345
- debug.log('Silent SSO: Got credential, exchanging for session...');
346
- let session;
347
- try {
348
- session = await this.exchangeIdTokenForSession(credential.token);
349
- }
350
- catch (exchangeError) {
351
- debug.error('Silent SSO: Token exchange failed:', exchangeError);
352
- return null;
353
- }
354
- // Validate session response has required fields
355
- if (!session) {
356
- debug.error('Silent SSO: Exchange returned null session');
357
- return null;
358
- }
359
- if (!session.sessionId) {
360
- debug.error('Silent SSO: Exchange returned session without sessionId:', session);
361
- return null;
362
- }
363
- if (!session.user) {
364
- debug.error('Silent SSO: Exchange returned session without user:', session);
365
- return null;
366
- }
367
- // Store the access token. Refresh stays in the httpOnly cookie slot set by
368
- // the API.
369
- if (session.accessToken) {
370
- this.httpService.setTokens(session.accessToken);
371
- debug.log('Silent SSO: Access token set');
372
- }
373
- else {
374
- debug.warn('Silent SSO: No accessToken in session response');
375
- }
376
- // Store the user ID as loginHint for future FedCM requests
377
- if (session.user?.id) {
378
- this.storeLoginHint(session.user.id);
379
- }
380
- debug.log('Silent SSO: Success!', {
381
- sessionId: session.sessionId?.substring(0, 8) + '...',
382
- userId: session.user?.id
383
- });
384
- return session;
385
- }
386
- /**
387
- * Request identity credential from browser using FedCM API
388
- *
389
- * Uses a global lock to prevent concurrent requests, as FedCM only
390
- * allows one navigator.credentials.get request at a time.
391
- *
392
- * Interactive requests (optional/required) wait for any silent request to finish first.
393
- *
394
- * @private
395
- */
396
- async requestIdentityCredential(options) {
397
- const requestedMediation = options.mediation || 'optional';
398
- const isInteractive = requestedMediation !== 'silent';
399
- debug.log('requestIdentityCredential called:', {
400
- mediation: requestedMediation,
401
- clientId: options.clientId,
402
- inProgress: fedCMRequestInProgress,
403
- });
404
- // If a request is already in progress...
405
- if (fedCMRequestInProgress && fedCMRequestPromise) {
406
- debug.log('Request already in progress, waiting...');
407
- // If the in-flight request is SILENT and this new one is INTERACTIVE,
408
- // abort the silent and proceed immediately. The silent round-trip can be
409
- // slow (it runs on page load and may stall in the browser), and a user who
410
- // just clicked "Sign In" must never be made to wait on — or be blocked by —
411
- // it. Awaiting the silent here is what previously let a hung silent
412
- // request deadlock the sign-in button, so we deliberately do NOT await it:
413
- // we abort it (its own `finally` resets the lock as it settles) and fall
414
- // through to start the interactive request synchronously below.
415
- if (currentMediationMode === 'silent' && isInteractive) {
416
- debug.log('Aborting in-flight silent request to make way for interactive request');
417
- fedCMActiveController?.abort();
418
- // Fall through. The interactive request synchronously overwrites the
419
- // lock globals (below); the aborted silent's `finally` uses identity
420
- // guards so it cannot later clobber this interactive request's state.
421
- }
422
- else {
423
- // Same type of request - wait for the existing one
424
- try {
425
- return await fedCMRequestPromise;
426
- }
427
- catch {
428
- return null;
429
- }
430
- }
431
- }
432
- fedCMRequestInProgress = true;
433
- currentMediationMode = requestedMediation;
434
- const controller = new AbortController();
435
- fedCMActiveController = controller;
436
- // Use shorter timeout for silent mediation since it should be quick.
437
- // The timeout constants are static on the composed class; read them off the
438
- // most-derived constructor through a typed cast (not `any`).
439
- const timeoutCtor = this.constructor;
440
- const timeoutMs = requestedMediation === 'silent'
441
- ? timeoutCtor.FEDCM_SILENT_TIMEOUT
442
- : timeoutCtor.FEDCM_TIMEOUT;
443
- const timeout = setTimeout(() => {
444
- debug.log('Request timed out after', timeoutMs, 'ms (mediation:', requestedMediation + ')');
445
- controller.abort();
446
- }, timeoutMs);
447
- // Hard settle guarantee for the timeout path.
448
- //
449
- // The `setTimeout` above aborts the request's `AbortController`, which is
450
- // the COOPERATIVE cancel signal. For a regular `fetch` an abort deterministically
451
- // rejects the awaited promise — but `navigator.credentials.get()` is a
452
- // browser-internal FedCM primitive whose abort behaviour is NOT guaranteed
453
- // to settle the awaited promise in every Chrome version / internal state
454
- // (the credential request can sit "pending" while the browser-side flow is
455
- // stuck, ignoring the signal). If that happens, `await credentials.get(...)`
456
- // never resolves OR rejects, this IIFE hangs forever, and — because this is
457
- // ONE step of the ordered cold-boot sequence — the whole cold boot hangs and
458
- // the terminal `/sso` bounce never fires. That was the production hang.
459
- //
460
- // `settlePromise` races the credential lookup against a timer that ALWAYS
461
- // resolves to `null` shortly after the abort deadline. The abort still fires
462
- // first (so the browser is asked to cancel), but even if `credentials.get`
463
- // never settles, the race resolves and the step falls through cleanly to the
464
- // next cold-boot step. The small `FEDCM_ABORT_SETTLE_GRACE_MS` margin gives a
465
- // well-behaved browser the chance to surface its own AbortError (preserving
466
- // the existing error path) before we force a clean `null`.
467
- let settleTimer;
468
- const settlePromise = new Promise((resolve) => {
469
- const ctor = this.constructor;
470
- settleTimer = setTimeout(() => {
471
- debug.log('Request hard-settled to null', timeoutMs + ctor.FEDCM_ABORT_SETTLE_GRACE_MS, 'ms (credentials.get never settled after abort)');
472
- resolve(null);
473
- }, timeoutMs + ctor.FEDCM_ABORT_SETTLE_GRACE_MS);
474
- });
475
- // Normalise the caller's mode to the modern W3C value first. A modern
476
- // browser accepts it; an older one (Chrome 125–131) rejects it with a
477
- // synchronous TypeError, in which case we retry with the legacy value.
478
- const modernMode = options.mode ? toModernMode(options.mode) : undefined;
479
- // Build the identity request for a specific mode value. The `mode` field
480
- // lives on the `identity` object (sibling of `providers`), separate from
481
- // the top-level `mediation` field.
482
- const buildCredentialOptions = (modeValue) => ({
483
- identity: {
484
- providers: [
485
- {
486
- configURL: options.configURL,
487
- clientId: options.clientId,
488
- // Older browsers read `nonce` at the top level; Chrome 145+
489
- // expects it inside `params`. Send both for full coverage.
490
- nonce: options.nonce,
491
- params: {
492
- nonce: options.nonce,
493
- },
494
- ...(options.loginHint && { loginHint: options.loginHint }),
495
- },
496
- ],
497
- ...(modeValue && { mode: modeValue }),
498
- },
499
- mediation: requestedMediation,
500
- signal: controller.signal,
501
- });
502
- // The DOM lib's `CredentialsContainer` does not declare the FedCM `identity`
503
- // request in every TypeScript version we build against. Re-type through the
504
- // minimal structural interface above (not `any`) to keep this typed.
505
- const credentials = navigator.credentials;
506
- fedCMRequestPromise = (async () => {
507
- try {
508
- debug.log('Calling navigator.credentials.get with mediation:', requestedMediation, modernMode ? `mode: ${modernMode}` : '');
509
- let credential;
510
- try {
511
- // Race the browser FedCM lookup against the hard settle guarantee so
512
- // a `credentials.get` that ignores the abort signal can never hang
513
- // the cold boot (see `settlePromise`).
514
- credential = await Promise.race([
515
- credentials.get(buildCredentialOptions(modernMode)),
516
- settlePromise,
517
- ]);
518
- }
519
- catch (modeError) {
520
- // Chrome 125–131 only knows the legacy 'button'/'widget' enum and
521
- // throws a synchronous TypeError for the modern 'active'/'passive'
522
- // values. Retry once with the legacy value so older browsers work.
523
- if (modernMode && isUnknownModeEnumError(modeError)) {
524
- const legacyMode = MODERN_TO_LEGACY_MODE[modernMode];
525
- debug.log(`Browser rejected modern mode '${modernMode}'; retrying with legacy mode '${legacyMode}'`);
526
- credential = await Promise.race([
527
- credentials.get(buildCredentialOptions(legacyMode)),
528
- settlePromise,
529
- ]);
530
- }
531
- else {
532
- throw modeError;
533
- }
534
- }
535
- debug.log('navigator.credentials.get returned:', {
536
- hasCredential: !!credential,
537
- type: credential?.type,
538
- hasToken: !!credential?.token,
539
- });
540
- if (!credential || credential.type !== 'identity' || !credential.token) {
541
- debug.log('No valid identity credential returned');
542
- return null;
543
- }
544
- const isAutoSelected = !!credential.isAutoSelected;
545
- debug.log('Got valid identity credential with token', { isAutoSelected });
546
- return { token: credential.token, isAutoSelected };
547
- }
548
- catch (error) {
549
- const errorName = error instanceof Error ? error.name : 'Unknown';
550
- const errorMessage = error instanceof Error ? error.message : String(error);
551
- debug.log('navigator.credentials.get error:', { name: errorName, message: errorMessage });
552
- throw error;
553
- }
554
- finally {
555
- clearTimeout(timeout);
556
- if (settleTimer !== undefined) {
557
- clearTimeout(settleTimer);
558
- }
559
- // Only reset the shared lock if it still belongs to THIS request. When an
560
- // interactive request aborts a slow silent one, the silent settles (and
561
- // runs this `finally`) AFTER the interactive has already taken over the
562
- // lock and installed its own controller/promise. Guarding on identity
563
- // (`fedCMActiveController === controller`) ensures the settling silent
564
- // cannot null out the interactive request's in-progress state. The
565
- // request that still owns the lock clears it; the superseded one is a
566
- // no-op here.
567
- if (fedCMActiveController === controller) {
568
- fedCMRequestInProgress = false;
569
- fedCMRequestPromise = null;
570
- currentMediationMode = null;
571
- fedCMActiveController = null;
572
- }
573
- }
574
- })();
575
- return fedCMRequestPromise;
576
- }
577
- /**
578
- * Exchange FedCM ID token for Oxy session
579
- *
580
- * The ID token is a JWT issued by auth.oxy.so that proves the user's
581
- * identity. We exchange it for a full Oxy session with access token.
582
- *
583
- * @private
584
- */
585
- async exchangeIdTokenForSession(idToken) {
586
- debug.log('Exchanging ID token for session...');
587
- try {
588
- const response = await this.makeRequest('POST', '/fedcm/exchange', { id_token: idToken }, { cache: false });
589
- debug.log('Token exchange complete:', {
590
- hasSession: !!response?.sessionId,
591
- hasUser: !!response?.user,
592
- });
593
- return {
594
- ...response,
595
- user: (0, userIdentity_1.normalizeUserIdentity)(response.user),
596
- };
597
- }
598
- catch (error) {
599
- debug.error('Token exchange failed:', error instanceof Error ? error.message : String(error));
600
- throw error;
601
- }
602
- }
603
- /**
604
- * Revoke FedCM credential (sign out)
605
- *
606
- * Uses IdentityCredential.disconnect() to tell the browser to forget
607
- * the RP-IdP-account association. This resets the "returning account"
608
- * state, which is required for silent mediation to work again.
609
- */
610
- async revokeFedCMCredential() {
611
- // Read hint before clearing so we can pass it to disconnect()
612
- const accountHint = this.getStoredLoginHint();
613
- this.clearLoginHint();
614
- if (!this.isFedCMSupported()) {
615
- return;
616
- }
617
- try {
618
- // The DOM lib does not declare the global `IdentityCredential` interface
619
- // object (with its static `disconnect`) in every TypeScript version we
620
- // build against. Read it off `window` through the minimal structural type
621
- // (not `any`), guarding that `disconnect` is actually present at runtime.
622
- const fedCMWindow = window;
623
- const identityCredential = fedCMWindow.IdentityCredential;
624
- if (identityCredential && typeof identityCredential.disconnect === 'function') {
625
- const clientId = this.getClientId();
626
- await identityCredential.disconnect({
627
- configURL: this.resolveFedcmConfigUrl(),
628
- clientId,
629
- accountHint: accountHint || '*',
630
- });
631
- debug.log('FedCM credential disconnected');
632
- }
633
- }
634
- catch (error) {
635
- debug.log('FedCM disconnect failed (non-critical):', error instanceof Error ? error.message : String(error));
636
- }
637
- }
638
- /**
639
- * Get configuration for FedCM
640
- *
641
- * @returns FedCM configuration with browser support info
642
- */
643
- getFedCMConfig() {
644
- return {
645
- enabled: this.isFedCMSupported(),
646
- configURL: this.resolveFedcmConfigUrl(),
647
- clientId: this.getClientId(),
648
- };
649
- }
650
- /**
651
- * Generate a cryptographically secure local nonce for FedCM.
652
- *
653
- * NOTE: this is a *local* fallback only. The server-side `/fedcm/exchange`
654
- * endpoint requires the nonce embedded in the ID token to have been minted
655
- * by `POST /fedcm/nonce` (see {@link mintServerNonce}) and bound to this
656
- * origin. A purely local nonce will be rejected with `invalid_nonce`. Use
657
- * {@link getFedcmNonce}, which prefers a server-minted nonce and only falls
658
- * back to this generator when the mint endpoint is unreachable.
659
- *
660
- * @private
661
- */
662
- generateNonce() {
663
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
664
- return crypto.randomUUID();
665
- }
666
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
667
- const bytes = new Uint8Array(16);
668
- crypto.getRandomValues(bytes);
669
- return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
670
- }
671
- throw new Error('No secure random source available for nonce generation');
672
- }
673
- /**
674
- * Mint a single-use, origin-bound nonce from the Oxy API.
675
- *
676
- * The FedCM ID token issued by the IdP embeds this nonce as the `nonce`
677
- * claim. When the consuming app calls `POST /fedcm/exchange`, the API burns
678
- * the nonce (atomic `usedAt` transition) and verifies it was minted for the
679
- * same origin as the token `aud`. This is the anti-replay binding required
680
- * by the API's H9 hardening — without a server-minted nonce the exchange
681
- * always fails.
682
- *
683
- * The browser attaches the `Origin` header automatically on this
684
- * cross-origin request, so the API binds the nonce to the calling app's
685
- * origin (which also becomes the FedCM `clientId`/token `aud`).
686
- *
687
- * @private
688
- */
689
- async mintServerNonce() {
690
- const result = await this.makeRequest('POST', '/fedcm/nonce', {}, { cache: false });
691
- if (!result?.nonce) {
692
- throw new OxyServices_errors_1.OxyAuthenticationError('FedCM nonce endpoint returned no nonce');
693
- }
694
- return result.nonce;
695
- }
696
- /**
697
- * Resolve the nonce to use for a FedCM credential request.
698
- *
699
- * Prefers a server-minted, origin-bound nonce (required for the token
700
- * exchange to succeed). If the mint endpoint is unreachable we fall back to
701
- * a locally generated nonce so the browser flow can still proceed; the
702
- * exchange may then fail server-side, but that is strictly better than
703
- * throwing before the browser ever shows its UI.
704
- *
705
- * @private
706
- */
707
- async getFedcmNonce() {
708
- try {
709
- return await this.mintServerNonce();
710
- }
711
- catch (error) {
712
- debug.warn('Could not mint server nonce, falling back to local nonce:', error instanceof Error ? error.message : String(error));
713
- return this.generateNonce();
714
- }
715
- }
716
- /**
717
- * Get the client ID for this origin
718
- *
719
- * @private
720
- */
721
- getClientId() {
722
- if (typeof window === 'undefined') {
723
- return 'unknown';
724
- }
725
- return window.location.origin;
726
- }
727
- /** @internal */
728
- getStoredLoginHint() {
729
- if (typeof window === 'undefined')
730
- return undefined;
731
- try {
732
- return localStorage.getItem(FEDCM_LOGIN_HINT_KEY) || undefined;
733
- }
734
- catch {
735
- return undefined;
736
- }
737
- }
738
- /** @internal */
739
- storeLoginHint(userId) {
740
- if (typeof window === 'undefined')
741
- return;
742
- try {
743
- localStorage.setItem(FEDCM_LOGIN_HINT_KEY, userId);
744
- }
745
- catch {
746
- // Storage full or blocked
747
- }
748
- }
749
- /** @internal */
750
- clearLoginHint() {
751
- if (typeof window === 'undefined')
752
- return;
753
- try {
754
- localStorage.removeItem(FEDCM_LOGIN_HINT_KEY);
755
- }
756
- catch {
757
- // Storage blocked
758
- }
759
- }
760
- /**
761
- * List the authenticated user's authorized RP apps.
762
- *
763
- * Returns the intersection of the user's FedCM grants and the currently-
764
- * approved RP catalog — what powers the "Connected apps" management UI in
765
- * @oxyhq/services. Requires a real user session; service tokens are
766
- * rejected by the underlying endpoint.
767
- */
768
- async listAuthorizedApps() {
769
- try {
770
- const response = await this.makeRequest('GET', '/fedcm/me/authorized-apps', undefined, {
771
- cache: true,
772
- cacheTTL: 30 * 1000, // 30 second cache — short, this drives a manageable UI
773
- });
774
- return response.apps ?? [];
775
- }
776
- catch (error) {
777
- throw this.handleError(error);
778
- }
779
- }
780
- /**
781
- * Revoke the authenticated user's authorization for a specific RP origin.
782
- *
783
- * The next FedCM sign-in from that origin will require explicit re-consent.
784
- * The corresponding cache entry is invalidated so a subsequent
785
- * `listAuthorizedApps()` call sees fresh data.
786
- */
787
- async revokeAuthorizedApp(origin) {
788
- try {
789
- await this.makeRequest('DELETE', `/fedcm/me/authorized-apps/${encodeURIComponent(origin)}`, undefined, { cache: false });
790
- this.clearCacheEntry('GET:/fedcm/me/authorized-apps');
791
- }
792
- catch (error) {
793
- throw this.handleError(error);
794
- }
795
- }
796
- },
797
- _a.DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json',
798
- _a.FEDCM_TIMEOUT = 15000 // 15 seconds for interactive
799
- ,
800
- // Silent mediation runs on page load as ONE step of the ordered cold-boot
801
- // sequence (mint nonce → navigator.credentials.get → /fedcm/exchange). The
802
- // real round-trip was measured at >3s for live users, so the budget must stay
803
- // comfortably above 3s. It must ALSO be tight: on a logged-out browser this
804
- // step never resolves a credential, and every millisecond it spends timing
805
- // out is pure latency in front of the steps that actually hold the answer
806
- // (stored-session bearer, the per-apex silent iframe, the /sso bounce). 4s is
807
- // the floor that preserves the >3s success margin while bounding the dead
808
- // wait — down from the previous 10s, which alone could account for most of a
809
- // 20-30s cold-boot stall. Do NOT lower below 4s (it would clip live success).
810
- _a.FEDCM_SILENT_TIMEOUT = 4000 // 4 seconds for silent mediation
811
- ,
812
- // Grace margin between the cooperative abort deadline (`FEDCM_SILENT_TIMEOUT`
813
- // / `FEDCM_TIMEOUT`) and the HARD settle of `requestIdentityCredential`. The
814
- // abort fires first; a well-behaved browser surfaces its own `AbortError`
815
- // within this window (keeping the existing error path intact). If — as seen
816
- // in production — `navigator.credentials.get()` ignores the abort and the
817
- // awaited promise never settles, the hard settle resolves the request to
818
- // `null` this many ms later, guaranteeing the cold-boot step always settles.
819
- // 500ms is ample for a browser to deliver an abort rejection while keeping the
820
- // worst-case dead wait tight (silent: 4.5s, interactive: 15.5s).
821
- _a.FEDCM_ABORT_SETTLE_GRACE_MS = 500,
822
- _a;
823
- }