@oxyhq/core 3.4.0 → 3.4.2

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 (174) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +91 -319
  3. package/dist/cjs/CrossDomainAuth.js +19 -106
  4. package/dist/cjs/HttpService.js +49 -73
  5. package/dist/cjs/OxyServices.base.js +2 -2
  6. package/dist/cjs/i18n/index.js +7 -1
  7. package/dist/cjs/i18n/locales/ar-SA.json +18 -2
  8. package/dist/cjs/i18n/locales/ca-ES.json +18 -2
  9. package/dist/cjs/i18n/locales/de-DE.json +18 -2
  10. package/dist/cjs/i18n/locales/en-US.json +16 -2
  11. package/dist/cjs/i18n/locales/es-ES.json +16 -2
  12. package/dist/cjs/i18n/locales/fr-FR.json +18 -2
  13. package/dist/cjs/i18n/locales/it-IT.json +18 -2
  14. package/dist/cjs/i18n/locales/ja-JP.json +18 -2
  15. package/dist/cjs/i18n/locales/ko-KR.json +18 -2
  16. package/dist/cjs/i18n/locales/locales/ar-SA.json +18 -2
  17. package/dist/cjs/i18n/locales/locales/ca-ES.json +18 -2
  18. package/dist/cjs/i18n/locales/locales/de-DE.json +18 -2
  19. package/dist/cjs/i18n/locales/locales/en-US.json +17 -3
  20. package/dist/cjs/i18n/locales/locales/es-ES.json +16 -2
  21. package/dist/cjs/i18n/locales/locales/fr-FR.json +18 -2
  22. package/dist/cjs/i18n/locales/locales/it-IT.json +18 -2
  23. package/dist/cjs/i18n/locales/locales/ja-JP.json +18 -2
  24. package/dist/cjs/i18n/locales/locales/ko-KR.json +18 -2
  25. package/dist/cjs/i18n/locales/locales/pt-PT.json +18 -2
  26. package/dist/cjs/i18n/locales/locales/zh-CN.json +18 -2
  27. package/dist/cjs/i18n/locales/pt-PT.json +18 -2
  28. package/dist/cjs/i18n/locales/zh-CN.json +18 -2
  29. package/dist/cjs/mixins/OxyServices.auth.js +20 -63
  30. package/dist/cjs/mixins/OxyServices.fedcm.js +10 -12
  31. package/dist/cjs/mixins/OxyServices.popup.js +50 -299
  32. package/dist/cjs/mixins/OxyServices.redirect.js +84 -348
  33. package/dist/cjs/mixins/OxyServices.silent.js +204 -0
  34. package/dist/cjs/mixins/OxyServices.sso.js +4 -5
  35. package/dist/cjs/mixins/OxyServices.utility.js +6 -15
  36. package/dist/cjs/mixins/index.js +5 -6
  37. package/dist/cjs/server/index.js +21 -0
  38. package/dist/cjs/server/rateLimit.js +77 -0
  39. package/dist/cjs/shared/utils/debugUtils.js +1 -1
  40. package/dist/cjs/utils/accountUtils.js +4 -4
  41. package/dist/cjs/utils/authHelpers.js +21 -15
  42. package/dist/cjs/utils/coldBoot.js +3 -3
  43. package/dist/cjs/utils/fapiAutoDetect.js +1 -1
  44. package/dist/esm/.tsbuildinfo +1 -1
  45. package/dist/esm/AuthManager.js +91 -319
  46. package/dist/esm/CrossDomainAuth.js +19 -106
  47. package/dist/esm/HttpService.js +49 -73
  48. package/dist/esm/OxyServices.base.js +2 -2
  49. package/dist/esm/i18n/index.js +7 -1
  50. package/dist/esm/i18n/locales/ar-SA.json +18 -2
  51. package/dist/esm/i18n/locales/ca-ES.json +18 -2
  52. package/dist/esm/i18n/locales/de-DE.json +18 -2
  53. package/dist/esm/i18n/locales/en-US.json +16 -2
  54. package/dist/esm/i18n/locales/es-ES.json +16 -2
  55. package/dist/esm/i18n/locales/fr-FR.json +18 -2
  56. package/dist/esm/i18n/locales/it-IT.json +18 -2
  57. package/dist/esm/i18n/locales/ja-JP.json +18 -2
  58. package/dist/esm/i18n/locales/ko-KR.json +18 -2
  59. package/dist/esm/i18n/locales/locales/ar-SA.json +18 -2
  60. package/dist/esm/i18n/locales/locales/ca-ES.json +18 -2
  61. package/dist/esm/i18n/locales/locales/de-DE.json +18 -2
  62. package/dist/esm/i18n/locales/locales/en-US.json +17 -3
  63. package/dist/esm/i18n/locales/locales/es-ES.json +16 -2
  64. package/dist/esm/i18n/locales/locales/fr-FR.json +18 -2
  65. package/dist/esm/i18n/locales/locales/it-IT.json +18 -2
  66. package/dist/esm/i18n/locales/locales/ja-JP.json +18 -2
  67. package/dist/esm/i18n/locales/locales/ko-KR.json +18 -2
  68. package/dist/esm/i18n/locales/locales/pt-PT.json +18 -2
  69. package/dist/esm/i18n/locales/locales/zh-CN.json +18 -2
  70. package/dist/esm/i18n/locales/pt-PT.json +18 -2
  71. package/dist/esm/i18n/locales/zh-CN.json +18 -2
  72. package/dist/esm/mixins/OxyServices.auth.js +20 -63
  73. package/dist/esm/mixins/OxyServices.fedcm.js +10 -12
  74. package/dist/esm/mixins/OxyServices.popup.js +52 -301
  75. package/dist/esm/mixins/OxyServices.redirect.js +84 -349
  76. package/dist/esm/mixins/OxyServices.silent.js +202 -0
  77. package/dist/esm/mixins/OxyServices.sso.js +4 -5
  78. package/dist/esm/mixins/OxyServices.utility.js +6 -15
  79. package/dist/esm/mixins/index.js +5 -6
  80. package/dist/esm/server/index.js +17 -0
  81. package/dist/esm/server/rateLimit.js +71 -0
  82. package/dist/esm/shared/utils/debugUtils.js +1 -1
  83. package/dist/esm/utils/accountUtils.js +4 -4
  84. package/dist/esm/utils/authHelpers.js +21 -15
  85. package/dist/esm/utils/coldBoot.js +3 -3
  86. package/dist/esm/utils/fapiAutoDetect.js +1 -1
  87. package/dist/types/.tsbuildinfo +1 -1
  88. package/dist/types/AuthManager.d.ts +26 -53
  89. package/dist/types/AuthManagerTypes.d.ts +5 -9
  90. package/dist/types/CrossDomainAuth.d.ts +13 -52
  91. package/dist/types/HttpService.d.ts +9 -8
  92. package/dist/types/OxyServices.base.d.ts +1 -1
  93. package/dist/types/OxyServices.d.ts +4 -10
  94. package/dist/types/index.d.ts +1 -1
  95. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -1
  96. package/dist/types/mixins/OxyServices.appData.d.ts +1 -1
  97. package/dist/types/mixins/OxyServices.applications.d.ts +1 -1
  98. package/dist/types/mixins/OxyServices.assets.d.ts +1 -1
  99. package/dist/types/mixins/OxyServices.auth.d.ts +10 -31
  100. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -1
  101. package/dist/types/mixins/OxyServices.devices.d.ts +1 -1
  102. package/dist/types/mixins/OxyServices.features.d.ts +1 -1
  103. package/dist/types/mixins/OxyServices.fedcm.d.ts +5 -5
  104. package/dist/types/mixins/OxyServices.language.d.ts +1 -1
  105. package/dist/types/mixins/OxyServices.location.d.ts +1 -1
  106. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -1
  107. package/dist/types/mixins/OxyServices.payment.d.ts +1 -1
  108. package/dist/types/mixins/OxyServices.popup.d.ts +18 -120
  109. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -1
  110. package/dist/types/mixins/OxyServices.redirect.d.ts +13 -174
  111. package/dist/types/mixins/OxyServices.reputation.d.ts +1 -1
  112. package/dist/types/mixins/OxyServices.security.d.ts +1 -1
  113. package/dist/types/mixins/OxyServices.silent.d.ts +131 -0
  114. package/dist/types/mixins/OxyServices.sso.d.ts +4 -5
  115. package/dist/types/mixins/OxyServices.topics.d.ts +1 -1
  116. package/dist/types/mixins/OxyServices.user.d.ts +1 -1
  117. package/dist/types/mixins/OxyServices.utility.d.ts +3 -8
  118. package/dist/types/mixins/OxyServices.workspaces.d.ts +1 -1
  119. package/dist/types/mixins/index.d.ts +3 -3
  120. package/dist/types/models/interfaces.d.ts +5 -16
  121. package/dist/types/models/session.d.ts +0 -2
  122. package/dist/types/server/index.d.ts +18 -0
  123. package/dist/types/server/rateLimit.d.ts +40 -0
  124. package/dist/types/shared/utils/debugUtils.d.ts +1 -1
  125. package/dist/types/utils/authHelpers.d.ts +4 -3
  126. package/dist/types/utils/coldBoot.d.ts +2 -2
  127. package/dist/types/utils/fapiAutoDetect.d.ts +1 -1
  128. package/package.json +25 -3
  129. package/src/AuthManager.ts +100 -370
  130. package/src/AuthManagerTypes.ts +5 -9
  131. package/src/CrossDomainAuth.ts +22 -129
  132. package/src/HttpService.ts +55 -73
  133. package/src/OxyServices.base.ts +2 -3
  134. package/src/OxyServices.ts +9 -11
  135. package/src/__tests__/authManager.cookiePath.test.ts +19 -17
  136. package/src/__tests__/authManager.security.test.ts +7 -3
  137. package/src/__tests__/crossDomainAuth.test.ts +26 -118
  138. package/src/i18n/index.ts +7 -1
  139. package/src/i18n/locales/ar-SA.json +18 -2
  140. package/src/i18n/locales/ca-ES.json +18 -2
  141. package/src/i18n/locales/de-DE.json +18 -2
  142. package/src/i18n/locales/en-US.json +17 -3
  143. package/src/i18n/locales/es-ES.json +16 -2
  144. package/src/i18n/locales/fr-FR.json +18 -2
  145. package/src/i18n/locales/it-IT.json +18 -2
  146. package/src/i18n/locales/ja-JP.json +18 -2
  147. package/src/i18n/locales/ko-KR.json +18 -2
  148. package/src/i18n/locales/pt-PT.json +18 -2
  149. package/src/i18n/locales/zh-CN.json +18 -2
  150. package/src/index.ts +1 -1
  151. package/src/mixins/OxyServices.auth.ts +23 -75
  152. package/src/mixins/OxyServices.fedcm.ts +10 -12
  153. package/src/mixins/OxyServices.redirect.ts +82 -371
  154. package/src/mixins/OxyServices.silent.ts +272 -0
  155. package/src/mixins/OxyServices.sso.ts +5 -6
  156. package/src/mixins/OxyServices.utility.ts +9 -22
  157. package/src/mixins/__tests__/appData.test.ts +1 -1
  158. package/src/mixins/__tests__/onTokensChanged.test.ts +1 -1
  159. package/src/mixins/__tests__/reputation.test.ts +1 -1
  160. package/src/mixins/__tests__/serviceAuth.test.ts +7 -5
  161. package/src/mixins/__tests__/silent.test.ts +102 -0
  162. package/src/mixins/__tests__/verifyChallenge.test.ts +9 -14
  163. package/src/mixins/index.ts +6 -8
  164. package/src/models/interfaces.ts +5 -16
  165. package/src/models/session.ts +1 -3
  166. package/src/server/index.ts +19 -0
  167. package/src/server/rateLimit.ts +170 -0
  168. package/src/shared/utils/debugUtils.ts +1 -1
  169. package/src/utils/accountUtils.ts +4 -4
  170. package/src/utils/authHelpers.ts +23 -15
  171. package/src/utils/coldBoot.ts +4 -4
  172. package/src/utils/fapiAutoDetect.ts +1 -1
  173. package/src/mixins/OxyServices.popup.ts +0 -631
  174. package/src/mixins/__tests__/popup.test.ts +0 -374
@@ -3,357 +3,93 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OxyServicesRedirectAuthMixin = OxyServicesRedirectAuthMixin;
4
4
  exports.RedirectAuthMixin = OxyServicesRedirectAuthMixin;
5
5
  const OxyServices_errors_1 = require("../OxyServices.errors");
6
+ const ssoBounce_1 = require("../utils/ssoBounce");
6
7
  /**
7
- * Redirect-based Cross-Domain Authentication Mixin
8
+ * Redirect-based authentication without bearer tokens in URLs.
8
9
  *
9
- * Implements traditional OAuth2 redirect flow as a fallback when popup or
10
- * FedCM are not available or fail (e.g., mobile browsers, popup blockers).
11
- *
12
- * Flow:
13
- * 1. Save current URL
14
- * 2. Redirect to auth.oxy.so/login
15
- * 3. User signs in
16
- * 4. Redirect back with token in URL
17
- * 5. Extract token, restore session, clean URL
18
- *
19
- * Features:
20
- * - Works on all browsers (including old mobile browsers)
21
- * - Automatic URL cleanup after auth
22
- * - State preservation option
23
- * - CSRF protection via state parameter
24
- *
25
- * Trade-offs:
26
- * - Loses JavaScript app state (full page navigation)
27
- * - Visible redirect (user sees navigation)
28
- * - Slower perceived performance
10
+ * The redirect fallback now uses the same central SSO code-return contract as
11
+ * cold boot: the RP stores CSRF/destination state in sessionStorage, navigates
12
+ * to the central IdP, receives only an opaque one-time code in the URL fragment,
13
+ * and the provider's `sso-return` step exchanges that code for the real session.
29
14
  */
30
15
  function OxyServicesRedirectAuthMixin(Base) {
31
- var _a;
32
- return _a = class extends Base {
33
- constructor(...args) {
34
- super(...args);
35
- }
36
- /**
37
- * Sign in using full page redirect
38
- *
39
- * Redirects the user to auth.oxy.so for authentication. After successful
40
- * sign-in, the user will be redirected back to the current page (or custom
41
- * redirect URI) with authentication tokens in the URL.
42
- *
43
- * Call handleAuthCallback() on app startup to complete the flow.
44
- *
45
- * @param options - Redirect configuration options
46
- *
47
- * @example
48
- * ```typescript
49
- * // Initiate sign-in
50
- * const handleSignIn = () => {
51
- * oxyServices.signInWithRedirect();
52
- * };
53
- *
54
- * // Handle callback on app startup
55
- * useEffect(() => {
56
- * const session = oxyServices.handleAuthCallback();
57
- * if (session) {
58
- * setUser(session.user);
59
- * }
60
- * }, []);
61
- * ```
62
- */
63
- signInWithRedirect(options = {}) {
64
- if (typeof window === 'undefined') {
65
- throw new OxyServices_errors_1.OxyAuthenticationError('Redirect authentication requires browser environment');
66
- }
67
- const redirectUri = options.redirectUri || window.location.href;
68
- const mode = options.mode || 'login';
69
- const state = this.generateState();
70
- const nonce = this.generateNonce();
71
- // Store state for CSRF protection
72
- this.storeAuthState(state, nonce);
73
- // Save current URL to restore after auth (optional)
74
- if (options.preserveUrl !== false) {
75
- this.savePreAuthUrl(window.location.href);
76
- }
77
- const authUrl = this.buildAuthUrl({
78
- mode,
79
- redirectUri,
80
- state,
81
- nonce,
82
- clientId: window.location.origin,
83
- });
84
- // Perform redirect
85
- window.location.href = authUrl;
86
- }
87
- /**
88
- * Sign up using full page redirect
89
- *
90
- * Same as signInWithRedirect but opens the signup page by default.
91
- */
92
- signUpWithRedirect(options = {}) {
93
- this.signInWithRedirect({ ...options, mode: 'signup' });
94
- }
95
- /**
96
- * Handle authentication callback
97
- *
98
- * Call this on app startup to check if the current page load is a
99
- * redirect back from the authentication server. If it is, this method
100
- * will extract the tokens, store them, and clean up the URL.
101
- *
102
- * @returns Session data if this is a callback, null otherwise
103
- * @throws {OxyAuthenticationError} If state validation fails (CSRF attack)
104
- *
105
- * @example
106
- * ```typescript
107
- * // In your app's root component or startup logic
108
- * useEffect(() => {
109
- * try {
110
- * const session = oxyServices.handleAuthCallback();
111
- * if (session) {
112
- * console.log('Logged in:', session.user);
113
- * setUser(session.user);
114
- * } else {
115
- * // Not a callback, check for existing session
116
- * const restored = oxyServices.restoreSession();
117
- * if (!restored) {
118
- * // No session, show login button
119
- * }
120
- * }
121
- * } catch (error) {
122
- * console.error('Auth callback failed:', error);
123
- * }
124
- * }, []);
125
- * ```
126
- */
127
- handleAuthCallback() {
128
- if (typeof window === 'undefined') {
129
- return null;
130
- }
131
- const url = new URL(window.location.href);
132
- const accessToken = url.searchParams.get('access_token');
133
- const sessionId = url.searchParams.get('session_id');
134
- const expiresAt = url.searchParams.get('expires_at');
135
- const state = url.searchParams.get('state');
136
- const error = url.searchParams.get('error');
137
- const errorDescription = url.searchParams.get('error_description');
138
- // Check if this is an error callback
139
- if (error) {
140
- this.clearAuthState();
141
- throw new OxyServices_errors_1.OxyAuthenticationError(errorDescription || error);
142
- }
143
- // Check if this is an auth callback
144
- if (!accessToken || !sessionId) {
145
- return null; // Not a callback
146
- }
147
- // Verify state to prevent CSRF attacks
148
- const savedState = this.getStoredState();
149
- if (!savedState || state !== savedState) {
150
- this.clearAuthState();
151
- throw new OxyServices_errors_1.OxyAuthenticationError('Invalid state parameter. Possible CSRF attack.');
152
- }
153
- // Store tokens
154
- this.storeTokens(accessToken, sessionId);
155
- this.httpService.setTokens(accessToken);
156
- // Build session response (minimal — full user data is fetched separately
157
- // by the caller via getCurrentUser() once tokens are stored).
158
- const session = {
159
- sessionId,
160
- deviceId: '', // Not available in redirect flow
161
- expiresAt: expiresAt || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
162
- // Placeholder user — caller MUST fetch real user data via getCurrentUser()
163
- // before exposing this session to the application. The empty id signals
164
- // that the user payload has not yet been populated.
165
- user: { id: '', username: '' },
166
- };
167
- // Clean up URL (remove auth parameters)
16
+ return class extends Base {
17
+ constructor(...args) {
18
+ super(...args);
19
+ }
20
+ /**
21
+ * Start a full-page redirect through the central SSO flow.
22
+ *
23
+ * No access token, refresh token, or session id is ever put in the URL or
24
+ * localStorage. The caller's provider must run `consumeSsoReturn` on startup
25
+ * to complete the code exchange and commit the session.
26
+ */
27
+ signInWithRedirect(options = {}) {
28
+ if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') {
29
+ throw new OxyServices_errors_1.OxyAuthenticationError('Redirect authentication requires browser sessionStorage');
30
+ }
31
+ const origin = window.location.origin;
32
+ const state = this.generateState();
33
+ const destination = options.preserveUrl === false
34
+ ? (options.redirectUri || origin)
35
+ : (options.redirectUri || window.location.href);
36
+ window.sessionStorage.setItem((0, ssoBounce_1.ssoStateKey)(origin), state);
37
+ window.sessionStorage.setItem((0, ssoBounce_1.ssoGuardKey)(origin), String(Date.now()));
38
+ window.sessionStorage.setItem((0, ssoBounce_1.ssoDestKey)(origin), destination);
39
+ window.sessionStorage.setItem((0, ssoBounce_1.ssoAttemptedKey)(origin), '1');
40
+ window.location.assign((0, ssoBounce_1.buildSsoBounceUrl)(origin, state, this.config.authWebUrl));
41
+ }
42
+ signUpWithRedirect(options = {}) {
43
+ this.signInWithRedirect({ ...options, mode: 'signup' });
44
+ }
45
+ /**
46
+ * Legacy token-query callbacks are intentionally rejected. Modern providers
47
+ * complete redirect auth through `consumeSsoReturn`, which accepts only
48
+ * `#oxy_sso=ok&code=...`.
49
+ */
50
+ handleAuthCallback() {
51
+ if (typeof window === 'undefined') {
52
+ return null;
53
+ }
54
+ const url = new URL(window.location.href);
55
+ if (url.searchParams.has('access_token') || url.searchParams.has('session_id')) {
168
56
  this.cleanAuthCallbackUrl(url);
169
- // Clean up storage
170
- this.clearAuthState();
171
- return session;
172
- }
173
- /**
174
- * Restore session from storage
175
- *
176
- * Attempts to restore a previously authenticated session from localStorage.
177
- * Call this on app startup if handleAuthCallback() returns null.
178
- *
179
- * @returns True if session was restored, false otherwise
180
- *
181
- * @example
182
- * ```typescript
183
- * useEffect(() => {
184
- * const session = oxyServices.handleAuthCallback();
185
- * if (!session) {
186
- * const restored = oxyServices.restoreSession();
187
- * if (!restored) {
188
- * // No session, user needs to sign in
189
- * setShowLogin(true);
190
- * }
191
- * }
192
- * }, []);
193
- * ```
194
- */
195
- restoreSession() {
196
- if (typeof window === 'undefined') {
197
- return false;
198
- }
199
- const token = localStorage.getItem(this.constructor.TOKEN_STORAGE_KEY);
200
- const sessionId = localStorage.getItem(this.constructor.SESSION_STORAGE_KEY);
201
- if (token && sessionId) {
202
- this.httpService.setTokens(token);
203
- return true;
204
- }
205
- return false;
206
- }
207
- /**
208
- * Clear stored session
209
- *
210
- * Removes all authentication data from storage. Call this on logout.
211
- */
212
- clearStoredSession() {
213
- if (typeof window === 'undefined') {
214
- return;
215
- }
216
- localStorage.removeItem(this.constructor.TOKEN_STORAGE_KEY);
217
- localStorage.removeItem(this.constructor.SESSION_STORAGE_KEY);
218
- this.httpService.clearTokens();
219
- }
220
- /**
221
- * Get stored session ID
222
- */
223
- getStoredSessionId() {
224
- if (typeof window === 'undefined') {
225
- return null;
226
- }
227
- return localStorage.getItem(this.constructor.SESSION_STORAGE_KEY);
228
- }
229
- /**
230
- * Build authentication URL with query parameters
231
- *
232
- * @private
233
- */
234
- buildAuthUrl(params) {
235
- const url = new URL(`${(this.config.authWebUrl || this.constructor.DEFAULT_AUTH_URL)}/${params.mode}`);
236
- url.searchParams.set('redirect_uri', params.redirectUri);
237
- url.searchParams.set('state', params.state);
238
- url.searchParams.set('nonce', params.nonce);
239
- url.searchParams.set('client_id', params.clientId);
240
- url.searchParams.set('response_type', 'token');
241
- return url.toString();
242
- }
243
- /**
244
- * Store tokens in localStorage
245
- *
246
- * @private
247
- */
248
- storeTokens(accessToken, sessionId) {
249
- if (typeof window === 'undefined') {
250
- return;
251
- }
252
- localStorage.setItem(this.constructor.TOKEN_STORAGE_KEY, accessToken);
253
- localStorage.setItem(this.constructor.SESSION_STORAGE_KEY, sessionId);
254
- }
255
- /**
256
- * Generate cryptographically secure state for CSRF protection
257
- *
258
- * @private
259
- */
260
- generateState() {
261
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
262
- return crypto.randomUUID();
263
- }
264
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
265
- const bytes = new Uint8Array(16);
266
- crypto.getRandomValues(bytes);
267
- return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
268
- }
269
- throw new Error('No secure random source available for state generation');
270
- }
271
- /**
272
- * Generate nonce for replay attack prevention
273
- *
274
- * @private
275
- */
276
- generateNonce() {
277
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
278
- return crypto.randomUUID();
279
- }
280
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
281
- const bytes = new Uint8Array(16);
282
- crypto.getRandomValues(bytes);
283
- return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
284
- }
285
- throw new Error('No secure random source available for nonce generation');
286
- }
287
- /**
288
- * Store auth state in session storage
289
- *
290
- * @private
291
- */
292
- storeAuthState(state, nonce) {
293
- if (typeof window === 'undefined') {
294
- return;
295
- }
296
- sessionStorage.setItem(this.constructor.STATE_STORAGE_KEY, state);
297
- sessionStorage.setItem(this.constructor.NONCE_STORAGE_KEY, nonce);
298
- }
299
- /**
300
- * Get stored state
301
- *
302
- * @private
303
- */
304
- getStoredState() {
305
- if (typeof window === 'undefined') {
306
- return null;
307
- }
308
- return sessionStorage.getItem(this.constructor.STATE_STORAGE_KEY);
309
- }
310
- /**
311
- * Clear auth state from storage
312
- *
313
- * @private
314
- */
315
- clearAuthState() {
316
- if (typeof window === 'undefined') {
317
- return;
318
- }
319
- sessionStorage.removeItem(this.constructor.STATE_STORAGE_KEY);
320
- sessionStorage.removeItem(this.constructor.NONCE_STORAGE_KEY);
321
- sessionStorage.removeItem(this.constructor.PRE_AUTH_URL_KEY);
322
- }
323
- /**
324
- * Save pre-authentication URL to restore later
325
- *
326
- * @private
327
- */
328
- savePreAuthUrl(url) {
329
- if (typeof window === 'undefined') {
330
- return;
331
- }
332
- sessionStorage.setItem(this.constructor.PRE_AUTH_URL_KEY, url);
333
- }
334
- /**
335
- * Clean authentication parameters from URL
336
- *
337
- * @private
338
- */
339
- cleanAuthCallbackUrl(url) {
340
- // Remove auth parameters
341
- url.searchParams.delete('access_token');
342
- url.searchParams.delete('session_id');
343
- url.searchParams.delete('expires_at');
344
- url.searchParams.delete('state');
345
- url.searchParams.delete('nonce');
346
- url.searchParams.delete('error');
347
- url.searchParams.delete('error_description');
348
- // Update URL without reloading page
349
- window.history.replaceState({}, '', url.toString());
350
- }
351
- },
352
- _a.DEFAULT_AUTH_URL = 'https://auth.oxy.so',
353
- _a.TOKEN_STORAGE_KEY = 'oxy_access_token',
354
- _a.SESSION_STORAGE_KEY = 'oxy_session_id',
355
- _a.STATE_STORAGE_KEY = 'oxy_auth_state',
356
- _a.PRE_AUTH_URL_KEY = 'oxy_pre_auth_url',
357
- _a.NONCE_STORAGE_KEY = 'oxy_auth_nonce',
358
- _a;
57
+ throw new OxyServices_errors_1.OxyAuthenticationError('Legacy access-token redirect callbacks are no longer accepted.');
58
+ }
59
+ return null;
60
+ }
61
+ restoreSession() {
62
+ return false;
63
+ }
64
+ clearStoredSession() {
65
+ this.httpService.clearTokens();
66
+ }
67
+ getStoredSessionId() {
68
+ return null;
69
+ }
70
+ generateState() {
71
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
72
+ return crypto.randomUUID();
73
+ }
74
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
75
+ const bytes = new Uint8Array(16);
76
+ crypto.getRandomValues(bytes);
77
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
78
+ }
79
+ throw new Error('No secure random source available for state generation');
80
+ }
81
+ generateNonce() {
82
+ return this.generateState();
83
+ }
84
+ cleanAuthCallbackUrl(url) {
85
+ url.searchParams.delete('access_token');
86
+ url.searchParams.delete('session_id');
87
+ url.searchParams.delete('expires_at');
88
+ url.searchParams.delete('state');
89
+ url.searchParams.delete('nonce');
90
+ url.searchParams.delete('error');
91
+ url.searchParams.delete('error_description');
92
+ window.history.replaceState({}, '', url.toString());
93
+ }
94
+ };
359
95
  }
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OxyServicesSilentAuthMixin = OxyServicesSilentAuthMixin;
4
+ exports.SilentAuthMixin = OxyServicesSilentAuthMixin;
5
+ const debugUtils_1 = require("../shared/utils/debugUtils");
6
+ const debug = (0, debugUtils_1.createDebugLogger)("SilentAuth");
7
+ /**
8
+ * Cross-domain silent browser auth helpers.
9
+ *
10
+ * The clean session model supports FedCM, tokenless redirect SSO, and silent
11
+ * iframe SSO. Bearer-token callback URLs are not part of this surface.
12
+ */
13
+ function OxyServicesSilentAuthMixin(Base) {
14
+ var _a;
15
+ return _a = class extends Base {
16
+ constructor(...args) {
17
+ super(...args);
18
+ }
19
+ /** Resolve auth URL from config or static default (method, not getter — getters break in TS mixins) */
20
+ resolveAuthUrl() {
21
+ return (this.config.authWebUrl || this.constructor.DEFAULT_AUTH_URL);
22
+ }
23
+ /**
24
+ * Silent sign-in using hidden iframe
25
+ *
26
+ * Attempts to automatically re-authenticate the user without any UI.
27
+ * This is what enables seamless SSO across all Oxy domains.
28
+ *
29
+ * How it works:
30
+ * 1. Creates hidden iframe pointing to auth.oxy.so/silent-auth
31
+ * 2. If user has valid session at auth.oxy.so, it exchanges an opaque SSO code
32
+ * 3. If not, iframe responds with null (no error thrown)
33
+ *
34
+ * This should be called on app startup to check for existing sessions.
35
+ *
36
+ * @param options - Silent auth options
37
+ * @returns Session if user is signed in, null otherwise
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * useEffect(() => {
42
+ * const checkAuth = async () => {
43
+ * const session = await oxyServices.silentSignIn();
44
+ * if (session) {
45
+ * setUser(session.user);
46
+ * }
47
+ * };
48
+ * checkAuth();
49
+ * }, []);
50
+ * ```
51
+ */
52
+ async silentSignIn(options = {}) {
53
+ if (typeof window === "undefined") {
54
+ return null;
55
+ }
56
+ const timeout = options.timeout || this.constructor.SILENT_TIMEOUT;
57
+ const nonce = this.generateNonce();
58
+ const clientId = window.location.origin;
59
+ // Resolve the IdP origin for the iframe. An explicit per-apex override (the
60
+ // durable cross-domain reload path — see `SilentAuthOptions.authWebUrlOverride`)
61
+ // wins over the instance's configured central auth URL. The SAME origin is
62
+ // handed to `waitForIframeAuth` so the postMessage origin check matches the
63
+ // exact host the iframe was loaded from.
64
+ const authOrigin = options.authWebUrlOverride && options.authWebUrlOverride.length > 0
65
+ ? options.authWebUrlOverride
66
+ : this.resolveAuthUrl();
67
+ const iframe = document.createElement("iframe");
68
+ iframe.style.display = "none";
69
+ iframe.style.position = "absolute";
70
+ iframe.style.width = "0";
71
+ iframe.style.height = "0";
72
+ iframe.style.border = "none";
73
+ const silentUrl = `${authOrigin}/auth/silent?` +
74
+ `client_id=${encodeURIComponent(clientId)}&` +
75
+ `nonce=${nonce}`;
76
+ iframe.src = silentUrl;
77
+ document.body.appendChild(iframe);
78
+ try {
79
+ const session = await this.waitForIframeAuth(iframe, timeout, authOrigin);
80
+ // Bail early on incomplete responses. The iframe contract requires
81
+ // both an access token and a session id; anything less is unusable.
82
+ // Returning `null` here (without installing the token) prevents a
83
+ // stale credential from being committed to HttpService when the
84
+ // user is actually signed out — that pattern caused a `getCurrentUser`
85
+ // -> 401 -> token-clear loop in consumer apps because callers gated
86
+ // on `session?.user` and never installed the user via
87
+ // `handleAuthSuccess`, while HttpService quietly held the token.
88
+ const accessToken = session
89
+ ? session.accessToken
90
+ : undefined;
91
+ if (!session || !accessToken || !session.sessionId) {
92
+ return null;
93
+ }
94
+ // Snapshot the previous token so we can roll back if the user
95
+ // lookup below fails — this avoids leaving a half-committed session
96
+ // (token installed, user missing) which would let the next
97
+ // authenticated request 401 with no way to recover.
98
+ const previousAccessToken = this.httpService.getAccessToken();
99
+ this.httpService.setTokens(accessToken);
100
+ // The iframe typically returns `{ sessionId, accessToken }` without
101
+ // user data. Fetch the user explicitly so callers receive a
102
+ // fully-formed session and never need a second `/users/me` round
103
+ // trip. If this fails the session is unusable — revert the token
104
+ // and return null so the caller treats this exactly like a
105
+ // missing-session response.
106
+ if (!session.user) {
107
+ try {
108
+ const userData = await this.makeRequest("GET", `/session/user/${session.sessionId}`, undefined, { cache: false, retry: false });
109
+ if (!userData) {
110
+ throw new Error("Empty user response");
111
+ }
112
+ session.user = userData;
113
+ }
114
+ catch (userError) {
115
+ debug.warn("silentSignIn: failed to fetch user data, rolling back token", userError);
116
+ if (previousAccessToken) {
117
+ this.httpService.setTokens(previousAccessToken);
118
+ }
119
+ else {
120
+ this.httpService.clearTokens();
121
+ }
122
+ return null;
123
+ }
124
+ }
125
+ return session;
126
+ }
127
+ catch (error) {
128
+ return null;
129
+ }
130
+ finally {
131
+ document.body.removeChild(iframe);
132
+ }
133
+ }
134
+ /**
135
+ * Wait for authentication response from iframe
136
+ *
137
+ * @private
138
+ */
139
+ async waitForIframeAuth(iframe, timeout, expectedOrigin) {
140
+ return new Promise((resolve) => {
141
+ const timeoutId = setTimeout(() => {
142
+ cleanup();
143
+ resolve(null); // Silent failure - don't throw
144
+ }, timeout);
145
+ const messageHandler = (event) => {
146
+ // Verify origin against the EXACT host the iframe was loaded from
147
+ // (`expectedOrigin`). For the per-apex durable-restore path this is
148
+ // `auth.<rp-apex>`, not the instance's central `resolveAuthUrl()` — so
149
+ // we must honour the caller-supplied origin, never re-derive it here.
150
+ if (event.origin !== expectedOrigin) {
151
+ return;
152
+ }
153
+ const { type, session } = event.data;
154
+ if (type !== "oxy_silent_auth") {
155
+ return;
156
+ }
157
+ cleanup();
158
+ resolve(session || null);
159
+ };
160
+ // Fail-fast on a load failure. When the per-apex `/auth/silent` host is
161
+ // unreachable, blocked by CSP `frame-ancestors`/`X-Frame-Options`, or the
162
+ // network drops, the iframe never posts a message — without this handler
163
+ // the silent restore would block for the FULL `timeout` (dead latency in
164
+ // the cold-boot critical path). `onerror`/`onabort` fire on a failed load,
165
+ // so resolve `null` immediately and let the next cold-boot step run. The
166
+ // success path posts a message and is handled above; these only catch the
167
+ // no-message failure modes.
168
+ const failFast = () => {
169
+ cleanup();
170
+ resolve(null);
171
+ };
172
+ iframe.onerror = failFast;
173
+ iframe.onabort = failFast;
174
+ const cleanup = () => {
175
+ clearTimeout(timeoutId);
176
+ iframe.onerror = null;
177
+ iframe.onabort = null;
178
+ window.removeEventListener("message", messageHandler);
179
+ };
180
+ window.addEventListener("message", messageHandler);
181
+ });
182
+ }
183
+ /**
184
+ * Generate nonce for replay attack prevention
185
+ *
186
+ * @private
187
+ */
188
+ generateNonce() {
189
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
190
+ return crypto.randomUUID();
191
+ }
192
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
193
+ const bytes = new Uint8Array(16);
194
+ crypto.getRandomValues(bytes);
195
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
196
+ }
197
+ throw new Error("No secure random source available for nonce generation");
198
+ }
199
+ },
200
+ _a.DEFAULT_AUTH_URL = "https://auth.oxy.so",
201
+ _a.SILENT_TIMEOUT = 5000 // 5 seconds
202
+ ,
203
+ _a;
204
+ }