@oxyhq/core 5.4.3 → 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 (186) 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 -43
  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 +44 -2
  18. package/dist/cjs/session/authStateStore.js +284 -0
  19. package/dist/cjs/session/createSessionClient.js +8 -2
  20. package/dist/cjs/session/refresh.js +264 -0
  21. package/dist/cjs/utils/accountUtils.js +1 -55
  22. package/dist/cjs/utils/authWebUrl.js +6 -15
  23. package/dist/cjs/utils/fapiAutoDetect.js +16 -64
  24. package/dist/cjs/utils/platform.js +19 -0
  25. package/dist/cjs/utils/ssoBounce.js +15 -340
  26. package/dist/cjs/utils/validationUtils.js +57 -0
  27. package/dist/esm/.tsbuildinfo +1 -1
  28. package/dist/esm/HttpService.js +6 -3
  29. package/dist/esm/OxyServices.base.js +7 -102
  30. package/dist/esm/boot/coldBootV2.js +344 -0
  31. package/dist/esm/boot/deviceBootReturn.js +146 -0
  32. package/dist/esm/crypto/keyManager.js +95 -0
  33. package/dist/esm/i18n/locales/en-US.json +13 -1
  34. package/dist/esm/i18n/locales/es-ES.json +13 -1
  35. package/dist/esm/i18n/locales/locales/en-US.json +13 -1
  36. package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
  37. package/dist/esm/index.js +28 -18
  38. package/dist/esm/mixins/OxyServices.accounts.js +6 -13
  39. package/dist/esm/mixins/OxyServices.auth.js +66 -201
  40. package/dist/esm/mixins/OxyServices.authorizedApps.js +35 -0
  41. package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
  42. package/dist/esm/mixins/index.js +13 -17
  43. package/dist/esm/session/SessionClient.js +44 -2
  44. package/dist/esm/session/authStateStore.js +278 -0
  45. package/dist/esm/session/createSessionClient.js +8 -2
  46. package/dist/esm/session/refresh.js +257 -0
  47. package/dist/esm/utils/accountUtils.js +0 -53
  48. package/dist/esm/utils/authWebUrl.js +6 -14
  49. package/dist/esm/utils/fapiAutoDetect.js +16 -63
  50. package/dist/esm/utils/platform.js +18 -0
  51. package/dist/esm/utils/ssoBounce.js +14 -324
  52. package/dist/esm/utils/validationUtils.js +56 -0
  53. package/dist/types/.tsbuildinfo +1 -1
  54. package/dist/types/HttpService.d.ts +13 -0
  55. package/dist/types/OxyServices.base.d.ts +0 -52
  56. package/dist/types/OxyServices.d.ts +0 -25
  57. package/dist/types/boot/coldBootV2.d.ts +76 -0
  58. package/dist/types/boot/deviceBootReturn.d.ts +83 -0
  59. package/dist/types/crypto/keyManager.d.ts +21 -0
  60. package/dist/types/index.d.ts +16 -21
  61. package/dist/types/mixins/OxyServices.accounts.d.ts +0 -2
  62. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  63. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  64. package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
  65. package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
  66. package/dist/types/mixins/{OxyServices.redirect.d.ts → OxyServices.authorizedApps.d.ts} +35 -33
  67. package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
  68. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  69. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  70. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
  71. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  72. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  73. package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
  74. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  75. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  76. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  77. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
  78. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  79. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  80. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  81. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  82. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  83. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  84. package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
  85. package/dist/types/mixins/index.d.ts +6 -9
  86. package/dist/types/models/interfaces.d.ts +0 -67
  87. package/dist/types/session/SessionClient.d.ts +38 -1
  88. package/dist/types/session/authStateStore.d.ts +119 -0
  89. package/dist/types/session/createSessionClient.d.ts +8 -1
  90. package/dist/types/session/refresh.d.ts +93 -0
  91. package/dist/types/utils/accountUtils.d.ts +0 -14
  92. package/dist/types/utils/authWebUrl.d.ts +6 -12
  93. package/dist/types/utils/fapiAutoDetect.d.ts +15 -38
  94. package/dist/types/utils/platform.d.ts +14 -0
  95. package/dist/types/utils/ssoBounce.d.ts +14 -262
  96. package/dist/types/utils/validationUtils.d.ts +15 -0
  97. package/package.json +2 -2
  98. package/src/HttpService.ts +19 -3
  99. package/src/OxyServices.base.ts +7 -112
  100. package/src/OxyServices.ts +0 -38
  101. package/src/boot/__tests__/coldBootV2.test.ts +317 -0
  102. package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
  103. package/src/boot/coldBootV2.ts +426 -0
  104. package/src/boot/deviceBootReturn.ts +195 -0
  105. package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
  106. package/src/crypto/keyManager.ts +101 -0
  107. package/src/i18n/locales/en-US.json +13 -1
  108. package/src/i18n/locales/es-ES.json +13 -1
  109. package/src/index.ts +78 -64
  110. package/src/mixins/OxyServices.accounts.ts +6 -13
  111. package/src/mixins/OxyServices.auth.ts +78 -253
  112. package/src/mixins/OxyServices.authorizedApps.ts +75 -0
  113. package/src/mixins/OxyServices.deviceBoot.ts +146 -0
  114. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
  115. package/src/mixins/__tests__/accounts.test.ts +17 -44
  116. package/src/mixins/__tests__/authorizedApps.test.ts +63 -0
  117. package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
  118. package/src/mixins/index.ts +16 -22
  119. package/src/models/interfaces.ts +0 -79
  120. package/src/session/SessionClient.ts +69 -3
  121. package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
  122. package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
  123. package/src/session/__tests__/SessionClient.socketFactory.test.ts +79 -0
  124. package/src/session/__tests__/SessionClient.state.test.ts +18 -5
  125. package/src/session/__tests__/authStateStore.test.ts +209 -0
  126. package/src/session/__tests__/refresh.test.ts +256 -0
  127. package/src/session/authStateStore.ts +335 -0
  128. package/src/session/createSessionClient.ts +9 -1
  129. package/src/session/refresh.ts +334 -0
  130. package/src/utils/__tests__/authWebUrl.test.ts +5 -29
  131. package/src/utils/__tests__/fapiAutoDetect.test.ts +5 -126
  132. package/src/utils/__tests__/validationUtils.test.ts +30 -0
  133. package/src/utils/accountUtils.ts +0 -62
  134. package/src/utils/authWebUrl.ts +6 -15
  135. package/src/utils/fapiAutoDetect.ts +16 -60
  136. package/src/utils/platform.ts +21 -0
  137. package/src/utils/ssoBounce.ts +14 -371
  138. package/src/utils/validationUtils.ts +62 -0
  139. package/dist/cjs/AuthManager.js +0 -1110
  140. package/dist/cjs/AuthManagerTypes.js +0 -13
  141. package/dist/cjs/CrossDomainAuth.js +0 -206
  142. package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
  143. package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
  144. package/dist/cjs/mixins/OxyServices.silent.js +0 -204
  145. package/dist/cjs/mixins/OxyServices.sso.js +0 -208
  146. package/dist/cjs/utils/ssoEstablish.js +0 -110
  147. package/dist/cjs/utils/ssoReturn.js +0 -267
  148. package/dist/esm/AuthManager.js +0 -1105
  149. package/dist/esm/AuthManagerTypes.js +0 -12
  150. package/dist/esm/CrossDomainAuth.js +0 -201
  151. package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
  152. package/dist/esm/mixins/OxyServices.redirect.js +0 -92
  153. package/dist/esm/mixins/OxyServices.silent.js +0 -202
  154. package/dist/esm/mixins/OxyServices.sso.js +0 -204
  155. package/dist/esm/utils/ssoEstablish.js +0 -107
  156. package/dist/esm/utils/ssoReturn.js +0 -263
  157. package/dist/types/AuthManager.d.ts +0 -380
  158. package/dist/types/AuthManagerTypes.d.ts +0 -81
  159. package/dist/types/CrossDomainAuth.d.ts +0 -164
  160. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
  161. package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
  162. package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
  163. package/dist/types/utils/ssoEstablish.d.ts +0 -85
  164. package/dist/types/utils/ssoReturn.d.ts +0 -146
  165. package/src/AuthManager.ts +0 -1269
  166. package/src/AuthManagerTypes.ts +0 -86
  167. package/src/CrossDomainAuth.ts +0 -243
  168. package/src/__tests__/authManager.cookiePath.test.ts +0 -390
  169. package/src/__tests__/authManager.security.test.ts +0 -377
  170. package/src/__tests__/crossDomainAuth.test.ts +0 -116
  171. package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
  172. package/src/mixins/OxyServices.fedcm.ts +0 -1026
  173. package/src/mixins/OxyServices.redirect.ts +0 -122
  174. package/src/mixins/OxyServices.silent.ts +0 -272
  175. package/src/mixins/OxyServices.sso.ts +0 -261
  176. package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
  177. package/src/mixins/__tests__/fedcm.test.ts +0 -667
  178. package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
  179. package/src/mixins/__tests__/silent.test.ts +0 -102
  180. package/src/mixins/__tests__/sso.test.ts +0 -228
  181. package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
  182. package/src/utils/__tests__/ssoBounce.test.ts +0 -219
  183. package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
  184. package/src/utils/__tests__/ssoReturn.test.ts +0 -251
  185. package/src/utils/ssoEstablish.ts +0 -174
  186. package/src/utils/ssoReturn.ts +0 -372
@@ -1,92 +0,0 @@
1
- import { OxyAuthenticationError } from '../OxyServices.errors.js';
2
- import { buildSsoBounceUrl, ssoAttemptedKey, ssoDestKey, ssoGuardKey, ssoStateKey, } from '../utils/ssoBounce.js';
3
- /**
4
- * Redirect-based authentication without bearer tokens in URLs.
5
- *
6
- * The redirect fallback now uses the same central SSO code-return contract as
7
- * cold boot: the RP stores CSRF/destination state in sessionStorage, navigates
8
- * to the central IdP, receives only an opaque one-time code in the URL fragment,
9
- * and the provider's `sso-return` step exchanges that code for the real session.
10
- */
11
- export function OxyServicesRedirectAuthMixin(Base) {
12
- return class extends Base {
13
- constructor(...args) {
14
- super(...args);
15
- }
16
- /**
17
- * Start a full-page redirect through the central SSO flow.
18
- *
19
- * No access token, refresh token, or session id is ever put in the URL or
20
- * localStorage. The caller's provider must run `consumeSsoReturn` on startup
21
- * to complete the code exchange and commit the session.
22
- */
23
- signInWithRedirect(options = {}) {
24
- if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') {
25
- throw new OxyAuthenticationError('Redirect authentication requires browser sessionStorage');
26
- }
27
- const origin = window.location.origin;
28
- const state = this.generateState();
29
- const destination = options.preserveUrl === false
30
- ? (options.redirectUri || origin)
31
- : (options.redirectUri || window.location.href);
32
- window.sessionStorage.setItem(ssoStateKey(origin), state);
33
- window.sessionStorage.setItem(ssoGuardKey(origin), String(Date.now()));
34
- window.sessionStorage.setItem(ssoDestKey(origin), destination);
35
- window.sessionStorage.setItem(ssoAttemptedKey(origin), '1');
36
- window.location.assign(buildSsoBounceUrl(origin, state, this.config.authWebUrl));
37
- }
38
- signUpWithRedirect(options = {}) {
39
- this.signInWithRedirect({ ...options, mode: 'signup' });
40
- }
41
- /**
42
- * Legacy token-query callbacks are intentionally rejected. Modern providers
43
- * complete redirect auth through `consumeSsoReturn`, which accepts only
44
- * `#oxy_sso=ok&code=...`.
45
- */
46
- handleAuthCallback() {
47
- if (typeof window === 'undefined') {
48
- return null;
49
- }
50
- const url = new URL(window.location.href);
51
- if (url.searchParams.has('access_token') || url.searchParams.has('session_id')) {
52
- this.cleanAuthCallbackUrl(url);
53
- throw new OxyAuthenticationError('Legacy access-token redirect callbacks are no longer accepted.');
54
- }
55
- return null;
56
- }
57
- restoreSession() {
58
- return false;
59
- }
60
- clearStoredSession() {
61
- this.httpService.clearTokens();
62
- }
63
- getStoredSessionId() {
64
- return null;
65
- }
66
- generateState() {
67
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
68
- return crypto.randomUUID();
69
- }
70
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
71
- const bytes = new Uint8Array(16);
72
- crypto.getRandomValues(bytes);
73
- return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
74
- }
75
- throw new Error('No secure random source available for state generation');
76
- }
77
- generateNonce() {
78
- return this.generateState();
79
- }
80
- cleanAuthCallbackUrl(url) {
81
- url.searchParams.delete('access_token');
82
- url.searchParams.delete('session_id');
83
- url.searchParams.delete('expires_at');
84
- url.searchParams.delete('state');
85
- url.searchParams.delete('nonce');
86
- url.searchParams.delete('error');
87
- url.searchParams.delete('error_description');
88
- window.history.replaceState({}, '', url.toString());
89
- }
90
- };
91
- }
92
- export { OxyServicesRedirectAuthMixin as RedirectAuthMixin };
@@ -1,202 +0,0 @@
1
- import { createDebugLogger } from "../shared/utils/debugUtils.js";
2
- const debug = createDebugLogger("SilentAuth");
3
- /**
4
- * Cross-domain silent browser auth helpers.
5
- *
6
- * The clean session model supports FedCM, tokenless redirect SSO, and silent
7
- * iframe SSO. Bearer-token callback URLs are not part of this surface.
8
- */
9
- export function OxyServicesSilentAuthMixin(Base) {
10
- var _a;
11
- return _a = class extends Base {
12
- constructor(...args) {
13
- super(...args);
14
- }
15
- /** Resolve auth URL from config or static default (method, not getter — getters break in TS mixins) */
16
- resolveAuthUrl() {
17
- return (this.config.authWebUrl || this.constructor.DEFAULT_AUTH_URL);
18
- }
19
- /**
20
- * Silent sign-in using hidden iframe
21
- *
22
- * Attempts to automatically re-authenticate the user without any UI.
23
- * This is what enables seamless SSO across all Oxy domains.
24
- *
25
- * How it works:
26
- * 1. Creates hidden iframe pointing to auth.oxy.so/silent-auth
27
- * 2. If user has valid session at auth.oxy.so, it exchanges an opaque SSO code
28
- * 3. If not, iframe responds with null (no error thrown)
29
- *
30
- * This should be called on app startup to check for existing sessions.
31
- *
32
- * @param options - Silent auth options
33
- * @returns Session if user is signed in, null otherwise
34
- *
35
- * @example
36
- * ```typescript
37
- * useEffect(() => {
38
- * const checkAuth = async () => {
39
- * const session = await oxyServices.silentSignIn();
40
- * if (session) {
41
- * setUser(session.user);
42
- * }
43
- * };
44
- * checkAuth();
45
- * }, []);
46
- * ```
47
- */
48
- async silentSignIn(options = {}) {
49
- if (typeof window === "undefined") {
50
- return null;
51
- }
52
- const timeout = options.timeout || this.constructor.SILENT_TIMEOUT;
53
- const nonce = this.generateNonce();
54
- const clientId = window.location.origin;
55
- // Resolve the IdP origin for the iframe. An explicit per-apex override (the
56
- // durable cross-domain reload path — see `SilentAuthOptions.authWebUrlOverride`)
57
- // wins over the instance's configured central auth URL. The SAME origin is
58
- // handed to `waitForIframeAuth` so the postMessage origin check matches the
59
- // exact host the iframe was loaded from.
60
- const authOrigin = options.authWebUrlOverride && options.authWebUrlOverride.length > 0
61
- ? options.authWebUrlOverride
62
- : this.resolveAuthUrl();
63
- const iframe = document.createElement("iframe");
64
- iframe.style.display = "none";
65
- iframe.style.position = "absolute";
66
- iframe.style.width = "0";
67
- iframe.style.height = "0";
68
- iframe.style.border = "none";
69
- const silentUrl = `${authOrigin}/auth/silent?` +
70
- `client_id=${encodeURIComponent(clientId)}&` +
71
- `nonce=${nonce}`;
72
- iframe.src = silentUrl;
73
- document.body.appendChild(iframe);
74
- try {
75
- const session = await this.waitForIframeAuth(iframe, timeout, authOrigin);
76
- // Bail early on incomplete responses. The iframe contract requires
77
- // both an access token and a session id; anything less is unusable.
78
- // Returning `null` here (without installing the token) prevents a
79
- // stale credential from being committed to HttpService when the
80
- // user is actually signed out — that pattern caused a `getCurrentUser`
81
- // -> 401 -> token-clear loop in consumer apps because callers gated
82
- // on `session?.user` and never installed the user via
83
- // `handleAuthSuccess`, while HttpService quietly held the token.
84
- const accessToken = session
85
- ? session.accessToken
86
- : undefined;
87
- if (!session || !accessToken || !session.sessionId) {
88
- return null;
89
- }
90
- // Snapshot the previous token so we can roll back if the user
91
- // lookup below fails — this avoids leaving a half-committed session
92
- // (token installed, user missing) which would let the next
93
- // authenticated request 401 with no way to recover.
94
- const previousAccessToken = this.httpService.getAccessToken();
95
- this.httpService.setTokens(accessToken);
96
- // The iframe typically returns `{ sessionId, accessToken }` without
97
- // user data. Fetch the user explicitly so callers receive a
98
- // fully-formed session and never need a second `/users/me` round
99
- // trip. If this fails the session is unusable — revert the token
100
- // and return null so the caller treats this exactly like a
101
- // missing-session response.
102
- if (!session.user) {
103
- try {
104
- const userData = await this.makeRequest("GET", `/session/user/${session.sessionId}`, undefined, { cache: false, retry: false });
105
- if (!userData) {
106
- throw new Error("Empty user response");
107
- }
108
- session.user = userData;
109
- }
110
- catch (userError) {
111
- debug.warn("silentSignIn: failed to fetch user data, rolling back token", userError);
112
- if (previousAccessToken) {
113
- this.httpService.setTokens(previousAccessToken);
114
- }
115
- else {
116
- this.httpService.clearTokens();
117
- }
118
- return null;
119
- }
120
- }
121
- return session;
122
- }
123
- catch (error) {
124
- return null;
125
- }
126
- finally {
127
- document.body.removeChild(iframe);
128
- }
129
- }
130
- /**
131
- * Wait for authentication response from iframe
132
- *
133
- * @private
134
- */
135
- async waitForIframeAuth(iframe, timeout, expectedOrigin) {
136
- return new Promise((resolve) => {
137
- const timeoutId = setTimeout(() => {
138
- cleanup();
139
- resolve(null); // Silent failure - don't throw
140
- }, timeout);
141
- const messageHandler = (event) => {
142
- // Verify origin against the EXACT host the iframe was loaded from
143
- // (`expectedOrigin`). For the per-apex durable-restore path this is
144
- // `auth.<rp-apex>`, not the instance's central `resolveAuthUrl()` — so
145
- // we must honour the caller-supplied origin, never re-derive it here.
146
- if (event.origin !== expectedOrigin) {
147
- return;
148
- }
149
- const { type, session } = event.data;
150
- if (type !== "oxy_silent_auth") {
151
- return;
152
- }
153
- cleanup();
154
- resolve(session || null);
155
- };
156
- // Fail-fast on a load failure. When the per-apex `/auth/silent` host is
157
- // unreachable, blocked by CSP `frame-ancestors`/`X-Frame-Options`, or the
158
- // network drops, the iframe never posts a message — without this handler
159
- // the silent restore would block for the FULL `timeout` (dead latency in
160
- // the cold-boot critical path). `onerror`/`onabort` fire on a failed load,
161
- // so resolve `null` immediately and let the next cold-boot step run. The
162
- // success path posts a message and is handled above; these only catch the
163
- // no-message failure modes.
164
- const failFast = () => {
165
- cleanup();
166
- resolve(null);
167
- };
168
- iframe.onerror = failFast;
169
- iframe.onabort = failFast;
170
- const cleanup = () => {
171
- clearTimeout(timeoutId);
172
- iframe.onerror = null;
173
- iframe.onabort = null;
174
- window.removeEventListener("message", messageHandler);
175
- };
176
- window.addEventListener("message", messageHandler);
177
- });
178
- }
179
- /**
180
- * Generate nonce for replay attack prevention
181
- *
182
- * @private
183
- */
184
- generateNonce() {
185
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
186
- return crypto.randomUUID();
187
- }
188
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
189
- const bytes = new Uint8Array(16);
190
- crypto.getRandomValues(bytes);
191
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
192
- }
193
- throw new Error("No secure random source available for nonce generation");
194
- }
195
- },
196
- _a.DEFAULT_AUTH_URL = "https://auth.oxy.so",
197
- _a.SILENT_TIMEOUT = 5000 // 5 seconds
198
- ,
199
- _a;
200
- }
201
- // Export the mixin function as both named and default
202
- export { OxyServicesSilentAuthMixin as SilentAuthMixin };
@@ -1,204 +0,0 @@
1
- /**
2
- * Central Cross-Domain SSO (opaque-code) Mixin
3
- *
4
- * Implements the Relying-Party half of TRUE central cross-domain SSO
5
- * (Google/Meta/Clerk style). The central IdP at `auth.oxy.so` owns the session;
6
- * an RP bounces a top-level redirect (prompt=none) to `auth.oxy.so/sso`, which
7
- * returns an OPAQUE single-use code in the redirect fragment. The RP then
8
- * exchanges that code here for the real session.
9
- *
10
- * Security properties:
11
- * - NO token/JWT ever travels in a URL — only the opaque code does. The real
12
- * `accessToken` is delivered exclusively in this exchange response body.
13
- * - The exchange is a CORS POST with NO credentials/cookies — the opaque code
14
- * is the only bearer of authority, and the central store burns it atomically
15
- * (single-use). Sending no cookies keeps the request a clean, ambient-
16
- * authority-free bearer exchange that the central `POST /sso/exchange`
17
- * endpoint validates by `Origin` against the code's bound `clientOrigin`.
18
- * - The code is minted server-side bound to the RP origin and expires in
19
- * seconds, so a leaked code is useless cross-origin and short-lived.
20
- *
21
- * On success the mixin plants the returned access token via
22
- * `httpService.setTokens(...)` — mirroring `exchangeIdTokenForSession` /
23
- * `verifyChallenge` — so callers do NOT need to plant tokens manually.
24
- */
25
- import { createDebugLogger } from '../shared/utils/debugUtils.js';
26
- import { ssoStateKey } from '../utils/ssoBounce.js';
27
- const debug = createDebugLogger('SSO');
28
- /**
29
- * Generate a cryptographically secure state value for the SSO bounce.
30
- *
31
- * Exposed as a module-level helper (in addition to the instance method below)
32
- * so consumers that do not yet hold an `OxyServices` instance can still mint a
33
- * bounce state. Uses `crypto.randomUUID` with a `getRandomValues` fallback.
34
- */
35
- export function generateSsoState() {
36
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
37
- return crypto.randomUUID();
38
- }
39
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
40
- const bytes = new Uint8Array(16);
41
- crypto.getRandomValues(bytes);
42
- return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
43
- }
44
- throw new Error('No secure random source available for SSO state generation');
45
- }
46
- /**
47
- * Read the SSO bounce state stored for the current browser origin, if any.
48
- *
49
- * Returns `null` outside a browser (no `window`/`sessionStorage`) or when no
50
- * state is stored — in which case the caller cannot (and must not) enforce a
51
- * state match, e.g. native flows or pre-hydration callbacks that already
52
- * validated the state before this exchange.
53
- */
54
- function getStoredSsoStateForCurrentOrigin() {
55
- if (typeof window === 'undefined' || !window.location || !window.sessionStorage) {
56
- return null;
57
- }
58
- try {
59
- return window.sessionStorage.getItem(ssoStateKey(window.location.origin));
60
- }
61
- catch {
62
- return null;
63
- }
64
- }
65
- export function OxyServicesSsoMixin(Base) {
66
- return class extends Base {
67
- constructor(...args) {
68
- super(...args);
69
- }
70
- /**
71
- * Generate cryptographically secure state for the SSO bounce (CSRF
72
- * protection). Delegates to the module-level {@link generateSsoState}
73
- * helper, which uses `crypto.randomUUID` when available and falls back to
74
- * `crypto.getRandomValues`.
75
- */
76
- generateSsoState() {
77
- return generateSsoState();
78
- }
79
- /**
80
- * Exchange an opaque single-use SSO code for the real Oxy session.
81
- *
82
- * POSTs `{ code }` to `${getSessionBaseUrl()}/sso/exchange` as a CORS
83
- * request with NO credentials/cookies. On success the returned access token
84
- * is planted via `httpService.setTokens(...)` (matching
85
- * `exchangeIdTokenForSession` / `verifyChallenge`), so callers do not need
86
- * to plant tokens manually.
87
- *
88
- * @param code - The opaque single-use code delivered in the SSO return
89
- * fragment (see {@link parseSsoReturnFragment}). The central store burns
90
- * it atomically on exchange.
91
- * @param state - The state value returned alongside the code. In browsers,
92
- * when an SSO bounce state is still stored for the current origin, this
93
- * must match before any token-committing exchange is attempted.
94
- * @returns The resolved {@link SessionLoginResponse}.
95
- */
96
- async exchangeSsoCode(code, state) {
97
- if (typeof code !== 'string' || code.length === 0) {
98
- throw this.handleError(new Error('exchangeSsoCode requires a non-empty code'));
99
- }
100
- const expectedState = getStoredSsoStateForCurrentOrigin();
101
- if (expectedState !== null && (typeof state !== 'string' || state.length === 0 || state !== expectedState)) {
102
- throw this.handleError(new Error('SSO exchange state mismatch'));
103
- }
104
- const url = `${this.getSessionBaseUrl().replace(/\/$/, '')}/sso/exchange`;
105
- debug.log('Exchanging SSO code for session...');
106
- let response;
107
- try {
108
- response = await fetch(url, {
109
- method: 'POST',
110
- // No cookies: the opaque code is the sole bearer of authority and the
111
- // server validates by Origin against the code's bound clientOrigin.
112
- credentials: 'omit',
113
- headers: {
114
- 'Content-Type': 'application/json',
115
- Accept: 'application/json',
116
- },
117
- body: JSON.stringify({ code }),
118
- });
119
- }
120
- catch (error) {
121
- debug.error('SSO exchange request failed:', error instanceof Error ? error.message : String(error));
122
- throw this.handleError(error);
123
- }
124
- if (!response.ok) {
125
- throw this.handleError(new Error(`SSO exchange failed with HTTP ${response.status}`));
126
- }
127
- let payload;
128
- try {
129
- payload = (await response.json());
130
- }
131
- catch (error) {
132
- throw this.handleError(error);
133
- }
134
- if (!payload || typeof payload.accessToken !== 'string' || payload.accessToken.length === 0) {
135
- throw this.handleError(new Error('SSO exchange returned no access token'));
136
- }
137
- if (typeof payload.sessionId !== 'string' || payload.sessionId.length === 0) {
138
- throw this.handleError(new Error('SSO exchange returned no sessionId'));
139
- }
140
- const userId = payload.user?.id ?? payload.user?._id;
141
- if (!userId || typeof payload.user?.username !== 'string') {
142
- throw this.handleError(new Error('SSO exchange returned an invalid user'));
143
- }
144
- const user = {
145
- id: userId,
146
- username: payload.user.username,
147
- // `name.displayName` is optional on the contract. A no-name user is
148
- // valid; carry whatever structured name the server sent (possibly an
149
- // empty object) and let consumers fall back to a handle.
150
- name: payload.user.name ?? {},
151
- avatar: payload.user.avatar,
152
- };
153
- // Plant the access token exactly like exchangeIdTokenForSession does.
154
- // The SSO exchange does not return a refresh token (the central store
155
- // holds the refresh credential), so default it to an empty string.
156
- this.httpService.setTokens(payload.accessToken);
157
- debug.log('SSO exchange complete:', { hasSession: !!payload.sessionId });
158
- const session = {
159
- sessionId: payload.sessionId,
160
- deviceId: '',
161
- expiresAt: payload.expiresAt ?? '',
162
- user,
163
- accessToken: payload.accessToken,
164
- };
165
- return session;
166
- }
167
- /**
168
- * Mint a server-formed `/sso/establish` URL for the caller's OWN session,
169
- * bound to an approved RP `origin`.
170
- *
171
- * Bearer-authenticated (the session id is taken from the caller's own
172
- * bearer, server-side — never from any argument). The server validates that
173
- * `origin` is an approved client origin (and matches the request `Origin`),
174
- * derives the per-apex IdP host (`auth.<apex>`), mints a short-lived HS256
175
- * establish-token, and returns a fully-formed
176
- * `https://<auth-host>/sso/establish?et=…&return_to=<origin>/__oxy/sso-callback&state=<state>`.
177
- *
178
- * Used AFTER a web device-flow claim to plant the durable first-party
179
- * `fedcm_session` cookie so a reload can re-mint a token (see
180
- * {@link establishIdpSessionAfterClaim}). Cache-free (a POST is never
181
- * cached, but `cache: false` is explicit).
182
- *
183
- * @param origin - The RP origin (`window.location.origin`) to establish for.
184
- * @param state - The CSRF state echoed back in the callback fragment; the
185
- * caller persists the SAME value under `ssoStateKey(origin)` so the
186
- * post-bounce `sso-return` step validates it.
187
- */
188
- async requestSsoEstablishUrl(origin, state) {
189
- if (typeof origin !== 'string' || origin.length === 0) {
190
- throw this.handleError(new Error('requestSsoEstablishUrl requires a non-empty origin'));
191
- }
192
- if (typeof state !== 'string' || state.length === 0) {
193
- throw this.handleError(new Error('requestSsoEstablishUrl requires a non-empty state'));
194
- }
195
- const response = await this.makeRequest('POST', '/sso/establish-token', { origin, state }, { cache: false });
196
- if (!response ||
197
- typeof response.establishUrl !== 'string' ||
198
- response.establishUrl.length === 0) {
199
- throw this.handleError(new Error('SSO establish-token returned no establishUrl'));
200
- }
201
- return { establishUrl: response.establishUrl };
202
- }
203
- };
204
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * Post-claim durable-session establish hop (web device-flow / "Sign in with
3
- * Oxy" QR).
4
- *
5
- * A WEB device-flow claim (`claimSessionByToken`) plants only IN-MEMORY tokens:
6
- * unlike a redirect/FedCM/silent sign-in, it never causes the IdP to plant a
7
- * `fedcm_session` cookie. So a reload has nothing to restore from — the
8
- * silent-iframe and `/sso` paths find no IdP session and the session is lost.
9
- *
10
- * This primitive closes that gap. AFTER the claim has committed and the session
11
- * has been durably persisted, it performs ONE top-level establish hop through
12
- * the RP's own per-apex IdP host (`auth.<rp-apex>`), reusing the EXISTING
13
- * `/sso/establish` endpoint: the server mints a short-lived, host+audience-bound
14
- * establish-token and returns a fully-formed establish URL; navigating to it
15
- * plants the durable first-party `fedcm_session` cookie and bounces back to the
16
- * RP callback with an opaque code the standard `sso-return` cold-boot step
17
- * exchanges.
18
- *
19
- * It reuses the SAME per-origin `sessionStorage` bounce contract
20
- * (`ssoStateKey` / `ssoGuardKey` / `ssoDestKey`) that {@link buildSsoBounceUrl}
21
- * primes for the terminal `/sso` bounce, so the post-bounce `sso-return` step
22
- * (`consumeSsoReturn`) validates the CSRF `state`, exchanges the code, and
23
- * restores the user's real destination with no extra wiring.
24
- *
25
- * Contract:
26
- * - WEB only — off-web / native it is a no-op returning `false`.
27
- * - NEVER fires while sitting on the central IdP origin (that would loop the
28
- * IdP against itself).
29
- * - Bounce state is persisted ONLY after the establish-URL request succeeds, so
30
- * a failed request leaves no stale state behind.
31
- * - SINGLE attempt: the caller invokes this exactly once per successful claim.
32
- * On ANY failure it does NOT navigate and returns `false`, leaving the
33
- * committed in-memory session exactly as-is (the user is no worse off than
34
- * before this hop existed).
35
- * - Total: never throws. Failures are reported via {@link deps.onError} only.
36
- */
37
- import { ssoStateKey, ssoGuardKey, ssoDestKey, ssoNavigate, isCentralIdPOrigin, } from './ssoBounce.js';
38
- import { generateSsoState } from '../mixins/OxyServices.sso.js';
39
- /**
40
- * Perform the post-claim establish hop. Returns `true` when a navigation to the
41
- * establish URL was initiated (the document is being torn down and replaced by
42
- * the IdP), `false` on every no-op / failure path.
43
- *
44
- * @param client - The exchange surface (`oxyServices.requestSsoEstablishUrl`).
45
- * @param deps - Injectable web seams; see {@link EstablishAfterClaimDeps}.
46
- */
47
- export async function establishIdpSessionAfterClaim(client, deps = {}) {
48
- const isWeb = deps.isWeb ??
49
- (() => typeof window !== 'undefined' &&
50
- typeof window.sessionStorage !== 'undefined');
51
- if (!isWeb()) {
52
- return false;
53
- }
54
- const storage = deps.storage ?? window.sessionStorage;
55
- const location = deps.location ?? window.location;
56
- const navigate = deps.navigate ?? ssoNavigate;
57
- const generateState = deps.generateState ?? generateSsoState;
58
- const now = deps.now ?? (() => Date.now());
59
- const origin = location.origin;
60
- // Never establish while sitting on the central IdP itself — that would bounce
61
- // the IdP against itself. (The device-flow claim can only happen on an RP.)
62
- if (isCentralIdPOrigin(origin)) {
63
- return false;
64
- }
65
- const state = generateState();
66
- let establishUrl;
67
- try {
68
- const result = await client.requestSsoEstablishUrl(origin, state);
69
- if (!result ||
70
- typeof result.establishUrl !== 'string' ||
71
- result.establishUrl.length === 0) {
72
- return false;
73
- }
74
- establishUrl = result.establishUrl;
75
- // Defense-in-depth before a TOP-LEVEL navigation: the establish URL is
76
- // server-formed, but never follow anything that is not a well-formed https
77
- // `/sso/establish` on an `auth.` host. A malformed or unexpected URL aborts
78
- // silently (no navigation, no state persisted) — same total/no-throw
79
- // contract — rather than navigating the document somewhere unintended.
80
- let parsedEstablishUrl;
81
- try {
82
- parsedEstablishUrl = new URL(establishUrl);
83
- }
84
- catch {
85
- return false;
86
- }
87
- if (parsedEstablishUrl.protocol !== 'https:' ||
88
- parsedEstablishUrl.pathname !== '/sso/establish' ||
89
- !parsedEstablishUrl.hostname.toLowerCase().startsWith('auth.')) {
90
- return false;
91
- }
92
- // Persist the bounce state ONLY now that the request has succeeded and the
93
- // URL is validated — so a failed/rejected request never leaves stale state
94
- // behind. This is the exact contract the terminal `/sso` bounce primes via
95
- // `buildSsoBounceUrl`, so the post-bounce `sso-return` step (`consumeSsoReturn`)
96
- // validates `state`, exchanges the returned code, and restores the dest.
97
- storage.setItem(ssoStateKey(origin), state);
98
- storage.setItem(ssoGuardKey(origin), String(now()));
99
- storage.setItem(ssoDestKey(origin), location.href);
100
- }
101
- catch (error) {
102
- deps.onError?.(error);
103
- return false;
104
- }
105
- navigate(establishUrl);
106
- return true;
107
- }