@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,263 +0,0 @@
1
- /**
2
- * Parse the SSO return fragment delivered by the central IdP.
3
- *
4
- * After a top-level redirect bounce to `auth.oxy.so/sso` (prompt=none), the
5
- * central IdP returns the Relying Party to its `redirect_uri` with the result
6
- * encoded in the URL fragment (the `#…` part). The fragment is used — not a
7
- * query string — so the opaque single-use code never reaches a server access
8
- * log, a `Referer` header, or browser history in a recoverable form.
9
- *
10
- * Three outcomes are possible:
11
- * - `#oxy_sso=ok&code=<opaque>&state=<state>` — the IdP had a session; the RP
12
- * exchanges `code` (via `oxy.exchangeSsoCode`) for the real session. NO
13
- * token/JWT ever appears in the URL — only the opaque code.
14
- * - `#oxy_sso=none&state=<state>` — the IdP had no session (prompt=none, user
15
- * not signed in centrally). The RP shows its own signed-out UI.
16
- * - `#oxy_sso=error&state=<state>` — the bounce failed. The RP recovers.
17
- *
18
- * This parser is pure and defensive: it never throws, and `kind` is strictly
19
- * one of `'ok' | 'none' | 'error'`. It returns `null` when the fragment is not
20
- * an oxy_sso fragment at all (i.e. `oxy_sso` is absent or an unrecognised
21
- * value), so the caller can ignore unrelated fragments without special-casing.
22
- */
23
- import { SSO_CALLBACK_PATH, ssoStateKey, ssoGuardKey, ssoDestKey, ssoNoSessionKey, ssoAttemptedKey, ssoCallbackBootstrapKey, } from './ssoBounce.js';
24
- const VALID_KINDS = new Set(['ok', 'none', 'error']);
25
- /**
26
- * Parse an SSO return fragment.
27
- *
28
- * @param hash - The URL fragment, with or without the leading `#`
29
- * (e.g. `location.hash`). May be `undefined`/empty.
30
- * @returns The parsed result when `hash` is a recognised oxy_sso fragment,
31
- * otherwise `null`. Never throws.
32
- */
33
- export function parseSsoReturnFragment(hash) {
34
- if (typeof hash !== 'string' || hash.length === 0) {
35
- return null;
36
- }
37
- // Strip a single leading '#'. A bare '#' (empty fragment) yields no params.
38
- const raw = hash.startsWith('#') ? hash.slice(1) : hash;
39
- if (raw.length === 0) {
40
- return null;
41
- }
42
- let params;
43
- try {
44
- params = new URLSearchParams(raw);
45
- }
46
- catch {
47
- // URLSearchParams does not throw for malformed input in practice, but guard
48
- // against any environment/polyfill that might so this stays total.
49
- return null;
50
- }
51
- const kind = params.get('oxy_sso');
52
- if (kind === null || !VALID_KINDS.has(kind)) {
53
- // Not an oxy_sso fragment (absent or unrecognised value) — ignore it.
54
- return null;
55
- }
56
- const result = { kind: kind };
57
- const state = params.get('state');
58
- if (state !== null && state.length > 0) {
59
- result.state = state;
60
- }
61
- // The opaque code is only meaningful on success; ignore any stray `code` on
62
- // none/error so callers never attempt an exchange for a non-ok outcome.
63
- if (result.kind === 'ok') {
64
- const code = params.get('code');
65
- if (code !== null && code.length > 0) {
66
- result.code = code;
67
- }
68
- }
69
- return result;
70
- }
71
- /**
72
- * Consume an SSO return: the commit-free, security-critical kernel of the
73
- * cross-domain SSO `sso-return` cold-boot step.
74
- *
75
- * This performs the CSRF/fragment/exchange/dest-restore/loop-breaker sequence
76
- * and RETURNS the exchanged session (or `null`). It deliberately does NOT
77
- * commit any UI/auth state — each provider commits its own way AROUND this
78
- * (e.g. `@oxyhq/services` `OxyContext` calls its `handleWebSSOSession`,
79
- * `@oxyhq/auth` `WebOxyProvider` updates its React state). Hoisting the kernel
80
- * here keeps the two providers byte-for-byte identical on the parts that matter
81
- * for security (state validation, fragment stripping order, loop prevention).
82
- *
83
- * Security/loop invariants (preserved exactly from both former copies):
84
- * - The fragment is stripped via `history.replaceState` FIRST — before the
85
- * exchange — so the opaque code never lingers in the URL, browser history,
86
- * or a `Referer` header even if a later step throws.
87
- * - `state` must match (CSRF). A mismatch or a missing code sets the
88
- * NO_SESSION flag so `sso-bounce` is disabled (no rebounce loop).
89
- * - `none`/`error` outcomes set BOTH the NO_SESSION flag and the
90
- * outcome-independent attempted-flag (the load2 half of the loop proof).
91
- * - A throwing exchange is caught, reported via `onExchangeError`, and
92
- * treated exactly like "no session" (never loops, never rethrows).
93
- * - On EVERY consumed outcome (ok, none, error, state-mismatch, no-code,
94
- * failed-exchange, no-sessionId) — not just ok — if the page landed on
95
- * {@link SSO_CALLBACK_PATH}, the user is taken to a same-origin TARGET so
96
- * they are never stranded on the internal callback path (which is an
97
- * unregistered route in every consumer router → a hard 404). The target is
98
- * the stored DEST when it parses as same-origin (an attacker-planted
99
- * cross-origin / protocol-relative dest is rejected), ELSE the app root
100
- * (`origin + '/'`). The DEST key is removed unconditionally.
101
- * - For the `ok` outcome the target is applied via a SOFT
102
- * `history.replaceState` + synthetic `popstate` so the freshly exchanged
103
- * in-memory session the provider is about to commit is preserved (no
104
- * reload). `popstate` is dispatched only on the `ok` same-origin restore.
105
- * - For every NON-`ok` outcome there is no in-memory session to preserve, and
106
- * the consumer router has ALREADY synchronously rendered its 404 for the
107
- * unregistered callback route — a soft replaceState+popstate does not
108
- * reliably make it re-resolve. So these outcomes perform a HARD
109
- * full-document navigation to the target (`hardRedirect`), which is both
110
- * safe (nothing to lose) and guaranteed to clear the 404 in every router.
111
- *
112
- * Total: this function NEVER throws. Off-web it is a no-op returning `null`.
113
- *
114
- * @param oxy - The exchange surface (`oxyServices.exchangeSsoCode`).
115
- * @param deps - Injectable web seams; see {@link ConsumeSsoReturnDeps}.
116
- * @returns The exchanged session on success, otherwise `null`.
117
- */
118
- export async function consumeSsoReturn(oxy, deps = {}) {
119
- const isWeb = deps.isWeb ??
120
- (() => typeof window !== 'undefined' &&
121
- typeof window.sessionStorage !== 'undefined');
122
- if (!isWeb()) {
123
- return null;
124
- }
125
- const storage = deps.storage ?? window.sessionStorage;
126
- const location = deps.location ?? window.location;
127
- const history = deps.history ?? window.history;
128
- const onExchangeError = deps.onExchangeError;
129
- // Default: emit a synthetic `popstate` so URL-driven routers re-sync after a
130
- // `history.replaceState` (which does NOT emit `popstate` on its own). Feature-
131
- // detected end to end so it never throws in any environment.
132
- const dispatchPopState = deps.dispatchPopState ??
133
- (() => {
134
- if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function') {
135
- return;
136
- }
137
- if (typeof PopStateEvent !== 'undefined') {
138
- window.dispatchEvent(new PopStateEvent('popstate'));
139
- }
140
- else if (typeof Event !== 'undefined') {
141
- window.dispatchEvent(new Event('popstate'));
142
- }
143
- });
144
- // Default: a hard, full-document navigation used to leave the callback path
145
- // on non-`ok` outcomes. Feature-detected end to end so it never throws in any
146
- // environment (SSR / native / a stubbed location without `replace`).
147
- const hardRedirect = deps.hardRedirect ??
148
- ((url) => {
149
- if (typeof window !== 'undefined' &&
150
- window.location &&
151
- typeof window.location.replace === 'function') {
152
- window.location.replace(url);
153
- }
154
- });
155
- const ret = parseSsoReturnFragment(location.hash);
156
- if (!ret) {
157
- // Not an oxy_sso fragment — nothing to do (do NOT touch any flags).
158
- return null;
159
- }
160
- const origin = location.origin;
161
- const callbackBootstrapKey = ssoCallbackBootstrapKey(origin);
162
- const wasCallbackBootstrapped = storage.getItem(callbackBootstrapKey) === '1';
163
- const expectedState = storage.getItem(ssoStateKey(origin));
164
- const stateOk = !!ret.state && !!expectedState && ret.state === expectedState;
165
- // Strip the fragment FIRST so the opaque code never lingers in the address
166
- // bar, history, or a `Referer` — even if a later step throws.
167
- history.replaceState(null, '', location.pathname + location.search);
168
- storage.removeItem(ssoStateKey(origin));
169
- // The in-flight bounce is now resolved — drop its guard so a later cold boot
170
- // (e.g. after sign-out) can bounce again.
171
- storage.removeItem(ssoGuardKey(origin));
172
- const markNoSession = () => {
173
- storage.setItem(ssoNoSessionKey(origin), '1');
174
- // A return was consumed, so the probe definitively happened. Set the
175
- // outcome-independent attempted-flag too so the bounce can never re-fire
176
- // even if some consumer path skipped setting it pre-bounce.
177
- storage.setItem(ssoAttemptedKey(origin), '1');
178
- };
179
- // Compute the same-origin TARGET to leave the callback path for. Returns the
180
- // stored DEST when present AND it parses as same-origin (never honour a
181
- // cross-origin / protocol-relative dest that could have been planted to
182
- // redirect the user), ELSE the app root (`origin + '/'`) so the user is never
183
- // stranded on the internal callback path even when no dest was stored. The
184
- // DEST key is removed unconditionally. Returns the relative path+search+hash
185
- // (so it can be fed to either `history.replaceState` or a `hardRedirect`),
186
- // or `null` when the page is not on the callback path (nothing to leave).
187
- const consumeCallbackTarget = () => {
188
- storage.removeItem(callbackBootstrapKey);
189
- if (location.pathname !== SSO_CALLBACK_PATH && !wasCallbackBootstrapped) {
190
- // Not on the callback path — still drop the dest key (consumed) but there
191
- // is nothing to navigate away from.
192
- storage.removeItem(ssoDestKey(origin));
193
- return null;
194
- }
195
- const dest = storage.getItem(ssoDestKey(origin));
196
- storage.removeItem(ssoDestKey(origin));
197
- if (dest) {
198
- try {
199
- const destUrl = new URL(dest, origin);
200
- if (destUrl.origin === origin) {
201
- return destUrl.pathname + destUrl.search + destUrl.hash;
202
- }
203
- }
204
- catch {
205
- // Malformed stored destination — fall through to the app-root fallback.
206
- }
207
- }
208
- // No dest, a cross-origin/protocol-relative dest, or an unparseable dest:
209
- // fall back to the app root so the router always leaves the 404.
210
- return '/';
211
- };
212
- // Non-`ok` outcomes: there is no in-memory session to preserve, and the
213
- // consumer router has already rendered its 404 for the unregistered callback
214
- // route — a soft replaceState+popstate does not reliably make it re-resolve.
215
- // Perform a HARD full-document navigation to the target (safe: nothing to
216
- // lose; guaranteed: every router leaves the 404). Off the callback path this
217
- // is a no-op (target is null).
218
- const leaveCallbackHard = () => {
219
- const target = consumeCallbackTarget();
220
- if (target !== null) {
221
- hardRedirect(origin + target);
222
- }
223
- };
224
- if (ret.kind === 'none' || ret.kind === 'error') {
225
- // The central IdP had no session (or the bounce failed). Record it so we do
226
- // not bounce again this tab — the definitive loop breaker.
227
- markNoSession();
228
- leaveCallbackHard();
229
- return null;
230
- }
231
- if (!stateOk || !ret.code) {
232
- // Forged / replayed / stale fragment, or a malformed ok with no code. Treat
233
- // exactly like "no session": never exchange, never loop.
234
- markNoSession();
235
- leaveCallbackHard();
236
- return null;
237
- }
238
- let session;
239
- try {
240
- session = await oxy.exchangeSsoCode(ret.code, ret.state);
241
- }
242
- catch (error) {
243
- onExchangeError?.(error);
244
- markNoSession();
245
- leaveCallbackHard();
246
- return null;
247
- }
248
- if (!session?.sessionId) {
249
- markNoSession();
250
- leaveCallbackHard();
251
- return null;
252
- }
253
- // `ok`: the provider is about to commit the freshly exchanged in-memory
254
- // session — do NOT hard-redirect (a full navigation would discard it). Use a
255
- // SOFT `history.replaceState` to the target + a synthetic `popstate` so
256
- // URL-driven routers re-sync to the restored route without a reload.
257
- const target = consumeCallbackTarget();
258
- if (target !== null) {
259
- history.replaceState(null, '', target);
260
- dispatchPopState();
261
- }
262
- return session;
263
- }
@@ -1,380 +0,0 @@
1
- /**
2
- * AuthManager - Centralized Authentication Manager
3
- *
4
- * Provides a unified authentication interface for all platforms.
5
- * Handles token storage, session management, and auth state changes.
6
- *
7
- * @module core/AuthManager
8
- */
9
- import type { OxyServices } from './OxyServices';
10
- import type { SessionLoginResponse, MinimalUserData } from './models/session';
11
- import type { AuthManagerAccount, RestoreFromCookiesResult, RestoreFromCookiesOptions, SwitchAuthuserResult } from './AuthManagerTypes';
12
- /**
13
- * Storage adapter interface for platform-agnostic storage.
14
- */
15
- export interface StorageAdapter {
16
- getItem: (key: string) => Promise<string | null> | string | null;
17
- setItem: (key: string, value: string) => Promise<void> | void;
18
- removeItem: (key: string) => Promise<void> | void;
19
- }
20
- /**
21
- * Auth state change callback type.
22
- */
23
- export type AuthStateChangeCallback = (user: MinimalUserData | null) => void;
24
- /**
25
- * Auth method types.
26
- */
27
- export type AuthMethod = 'fedcm' | 'redirect' | 'credentials' | 'identity';
28
- /**
29
- * Auth manager configuration.
30
- */
31
- export interface AuthManagerConfig {
32
- /** Storage adapter (localStorage, AsyncStorage, etc.) */
33
- storage?: StorageAdapter;
34
- /** Whether to auto-refresh tokens */
35
- autoRefresh?: boolean;
36
- /** Token refresh interval in milliseconds (default: 5 minutes before expiry) */
37
- refreshBuffer?: number;
38
- /** Enable cross-tab coordination via BroadcastChannel (default: true in browsers) */
39
- crossTabSync?: boolean;
40
- }
41
- /**
42
- * AuthManager - Centralized authentication management.
43
- *
44
- * Provides a single point of control for:
45
- * - Token storage and retrieval
46
- * - Session management
47
- * - Auth state change notifications
48
- * - Multiple auth method support
49
- *
50
- * @example
51
- * ```typescript
52
- * const authManager = new AuthManager(oxyServices);
53
- *
54
- * // Listen for auth changes
55
- * authManager.onAuthStateChange((user) => {
56
- * console.log('Auth state changed:', user);
57
- * });
58
- *
59
- * // Handle successful auth
60
- * await authManager.handleAuthSuccess(session);
61
- *
62
- * // Sign out
63
- * await authManager.signOut();
64
- * ```
65
- */
66
- export declare class AuthManager {
67
- private oxyServices;
68
- private storage;
69
- private listeners;
70
- private currentUser;
71
- private currentAuthMethod;
72
- private refreshTimer;
73
- private refreshPromise;
74
- private config;
75
- /** Tracks the access token this instance last knew about, for cross-tab adoption. */
76
- private _lastKnownAccessToken;
77
- /** BroadcastChannel for coordinating token refreshes across browser tabs. */
78
- private _broadcastChannel;
79
- /**
80
- * Identifier for this AuthManager instance (≈ "this tab"). Random hex
81
- * generated at construction; advertised in every outgoing broadcast and
82
- * used as the lookup key in `_knownPeerNonces`.
83
- */
84
- private readonly _tabId;
85
- /**
86
- * Per-tab nonce, advertised in every outgoing broadcast. Receivers record
87
- * the first (tabId, nonce) pair they see from a given peer; subsequent
88
- * messages from the same tabId MUST carry the same nonce or they're
89
- * ignored.
90
- *
91
- * Threat model: a same-origin XSS payload can post to the channel but can
92
- * NOT read this instance's private `_broadcastNonce` field (it lives in
93
- * closure, not on `window`). Forged broadcasts from XSS therefore can't
94
- * impersonate this tab. A new attacker-controlled tabId trips the
95
- * "first message from a new peer" branch, which is by definition trusted
96
- * — so the gate raises the bar but is not a complete defence (a perfect
97
- * mitigation would require message signing with a server-issued key).
98
- */
99
- private readonly _broadcastNonce;
100
- /**
101
- * Bounded LRU of `(tabId → nonce)` pairs seen on inbound broadcasts. First
102
- * sighting of a new tabId records its nonce; later messages from that
103
- * tabId are rejected if the nonce doesn't match.
104
- */
105
- private readonly _knownPeerNonces;
106
- private static readonly _MAX_KNOWN_PEERS;
107
- /**
108
- * In-flight `switchAuthuser` promise. Deduplicates concurrent calls so two
109
- * near-simultaneous switches don't both fire refresh requests and rotate
110
- * the slot twice. Mirrors the `refreshPromise` pattern used by
111
- * `refreshToken`.
112
- */
113
- private _switchPromise;
114
- /**
115
- * Last `restoreFromCookies()` completion timestamp, keyed by the
116
- * AuthManager's active authuser at the time of completion. Used to gate
117
- * cross-tab cascade: a flurry of BroadcastChannel events from sibling
118
- * tabs can otherwise trigger N back-to-back snapshots and rotate every
119
- * slot's access token N times.
120
- */
121
- private readonly _lastRestoreAt;
122
- private static readonly _RESTORE_DEBOUNCE_MS;
123
- /**
124
- * In-memory registry of every device-local account the AuthManager knows
125
- * about, keyed by `authuser` slot index. Populated by:
126
- * - `restoreFromCookies()` (cold boot)
127
- * - `switchAuthuser()` (per-account rotation)
128
- * - `handleAuthSuccess()` (fresh login when the server response carries
129
- * an `authuser` field)
130
- * Access tokens live ONLY here in the cookie path — they are never
131
- * persisted to localStorage.
132
- */
133
- private accounts;
134
- /**
135
- * Currently-active `authuser` slot in the cookie path. `null` means either
136
- * the cookie path hasn't been initialised yet, or no slots are signed in.
137
- */
138
- private activeAuthuser;
139
- constructor(oxyServices: OxyServices, config?: AuthManagerConfig);
140
- /**
141
- * Initialize BroadcastChannel for cross-tab token refresh coordination.
142
- * Only called in browser environments where BroadcastChannel is available.
143
- */
144
- private _initBroadcastChannel;
145
- /**
146
- * Handle messages from other tabs about token refresh activity.
147
- */
148
- private _handleCrossTabMessage;
149
- /**
150
- * Broadcast a message to other tabs. Always stamps this tab's `tabId` and
151
- * `nonce` onto the message so receivers can run the cross-tab nonce gate.
152
- */
153
- private _broadcast;
154
- /**
155
- * Generate `bytes` bytes of cryptographic randomness encoded as lowercase
156
- * hex. Prefers Web Crypto's `getRandomValues` when available (browser /
157
- * modern Node); falls back to `Math.random` ONLY in environments without
158
- * Web Crypto (the resulting nonce is still unguessable to a same-origin
159
- * XSS payload — the goal is unforgeability across tabs, not cryptographic
160
- * secrecy across the network).
161
- */
162
- private static _randomHex;
163
- /**
164
- * Validate an inbound broadcast against the cross-tab nonce gate.
165
- *
166
- * Returns `true` when the message should be honoured, `false` when it
167
- * MUST be ignored:
168
- * - Message is missing `tabId` or `nonce` → ignore (forged or
169
- * mismatched-version sibling tab).
170
- * - First sighting of `tabId` → record the nonce and honour the message
171
- * (trust-on-first-use, the best we can do without a shared secret).
172
- * - Subsequent message from the same `tabId` with the SAME nonce →
173
- * honour.
174
- * - Subsequent message from the same `tabId` with a DIFFERENT nonce →
175
- * ignore (the canonical "forged broadcast" case — a same-origin XSS
176
- * payload can't read the real tab's `_broadcastNonce`).
177
- *
178
- * Echoes of this tab's own broadcasts (same `tabId`) are also dropped so
179
- * we don't react to our own messages.
180
- */
181
- private _acceptBroadcast;
182
- /**
183
- * Get default storage based on environment.
184
- */
185
- private getDefaultStorage;
186
- /**
187
- * Subscribe to auth state changes.
188
- *
189
- * @param callback - Function called when auth state changes
190
- * @returns Unsubscribe function
191
- */
192
- onAuthStateChange(callback: AuthStateChangeCallback): () => void;
193
- /**
194
- * Notify all listeners of auth state change.
195
- */
196
- private notifyListeners;
197
- /**
198
- * Handle successful authentication.
199
- *
200
- * @param session - Session response from auth
201
- * @param method - Auth method used
202
- */
203
- handleAuthSuccess(session: SessionLoginResponse, method?: AuthMethod): Promise<void>;
204
- /**
205
- * Refresh the access token. Deduplicates concurrent calls so only one
206
- * refresh request is in-flight at a time. The only refresh authority is the
207
- * active httpOnly refresh-cookie slot; this method never reads access tokens
208
- * from storage.
209
- */
210
- refreshToken(): Promise<boolean>;
211
- private _doRefreshToken;
212
- /**
213
- * Sign out and clear all auth data.
214
- */
215
- signOut(): Promise<void>;
216
- /**
217
- * Clear local cookie-path state. The only persisted AuthManager value is the
218
- * active numeric slot; tokens and user objects are intentionally memory-only.
219
- */
220
- private clearSession;
221
- /**
222
- * Get current user.
223
- */
224
- getCurrentUser(): MinimalUserData | null;
225
- /**
226
- * Check if user is authenticated.
227
- */
228
- isAuthenticated(): boolean;
229
- /**
230
- * Get a valid access token, refreshing automatically if expired or expiring
231
- * soon. The token is read from memory only.
232
- */
233
- getAccessToken(): Promise<string | null>;
234
- /**
235
- * Get the auth method used for current session.
236
- */
237
- getAuthMethod(): Promise<AuthMethod | null>;
238
- /**
239
- * Initialize auth state on app startup.
240
- *
241
- * Only the cookie path is authoritative. `restoreFromCookies()` refreshes
242
- * the httpOnly `oxy_rt_${authuser}` slots through `/auth/refresh-all`,
243
- * plants the active access token in memory, and returns the active user.
244
- * No access token, refresh token, or session JSON is read from localStorage.
245
- *
246
- * Returns the active user on success, or `null` when no cookie-backed
247
- * account was restored.
248
- */
249
- initialize(options?: RestoreFromCookiesOptions): Promise<MinimalUserData | null>;
250
- /**
251
- * Read the persisted active `authuser` slot index. Returns `null` when
252
- * none is persisted, the value is corrupt, or the storage adapter has no
253
- * record. Storage failures are non-fatal: the cookie path falls back to
254
- * "lowest authuser" deterministic selection.
255
- */
256
- private readActiveAuthuser;
257
- /**
258
- * Persist the active `authuser` slot index. No-ops on storage failure
259
- * (e.g. Safari private mode, native SecureStore unavailable) — this is
260
- * best-effort UX persistence, not authoritative state.
261
- */
262
- private writeActiveAuthuser;
263
- /**
264
- * Clear the persisted active `authuser` so the next cold boot starts from
265
- * a clean slate (used on full sign-out).
266
- */
267
- private clearActiveAuthuser;
268
- /**
269
- * Build a `MinimalUserData` from a `RefreshAllAccount`. Returns `null` when
270
- * the wire entry has no user shape; the AuthManager's caller is expected to
271
- * hydrate via `/users/me` in that case.
272
- */
273
- private static toMinimalUser;
274
- /**
275
- * Hydrate the user shape for a slot whose AuthManagerAccount currently has
276
- * `user: null` (for example, a switch onto a previously unknown slot). Calls
277
- * `/users/me` with the slot's freshly-planted access
278
- * token already on the HTTP client; merges the result back into the
279
- * registry entry. Network failures are non-fatal — the slot remains with
280
- * `user: null` and the UI is expected to render the public-key fallback
281
- * handle until a later restore picks the real user shape up.
282
- */
283
- private _hydrateUnknownUser;
284
- /**
285
- * Snapshot of the registered cookie-path accounts, sorted by `authuser`
286
- * ascending (canonical order). Mutating the returned array does not
287
- * affect AuthManager state.
288
- */
289
- getAccounts(): AuthManagerAccount[];
290
- /**
291
- * The slot index that is currently active in the cookie path, or `null`
292
- * if the cookie path hasn't been initialised or no slots are signed in.
293
- */
294
- getActiveAuthuser(): number | null;
295
- /**
296
- * Convenience: the AuthManagerAccount currently flagged active.
297
- */
298
- getActiveAccount(): AuthManagerAccount | null;
299
- /**
300
- * Restore every device-local account from the httpOnly refresh cookies.
301
- *
302
- * Calls `oxyServices.refreshAllSessions()` (`POST /auth/refresh-all` with
303
- * `credentials: 'include'`). The server rotates every presented
304
- * `oxy_rt_${authuser}` cookie in parallel and returns one entry per
305
- * valid slot.
306
- *
307
- * Plants the active account's access token on the shared HTTP client;
308
- * sibling slots' tokens stay in the in-memory registry so a later
309
- * `switchAuthuser()` can hot-swap them without a network round-trip.
310
- *
311
- * The persisted `oxy_active_authuser` slot wins when it matches a
312
- * returned account; otherwise the lowest returned `authuser` is chosen
313
- * deterministically.
314
- *
315
- * Returns `{ accounts: [], activeAuthuser: null }` on any failure or
316
- * empty snapshot — callers treat that as "no signed-in accounts" and
317
- * proceed unauthenticated. State is NOT cleared on failure; existing
318
- * accounts (if any) remain intact.
319
- */
320
- restoreFromCookies(options?: RestoreFromCookiesOptions): Promise<RestoreFromCookiesResult>;
321
- /**
322
- * Switch the active account to a different device-local slot.
323
- *
324
- * Calls `oxyServices.refreshTokenViaCookie({ authuser })` to mint a fresh
325
- * access token from the slot's httpOnly cookie, updates the in-memory
326
- * registry entry, plants the token on the HTTP client, persists the new
327
- * active slot, and broadcasts cross-tab.
328
- *
329
- * Throws when the slot's refresh cookie is missing / expired / reused
330
- * (the SDK returns `null` from `refreshTokenViaCookie` in that case, and
331
- * we surface it as an `Error` so callers can clean up the slot from
332
- * their UI).
333
- */
334
- switchAuthuser(authuser: number): Promise<SwitchAuthuserResult>;
335
- private _doSwitchAuthuser;
336
- /**
337
- * Sign out a single device-local slot.
338
- *
339
- * Calls `oxyServices.logoutSessionByAuthuser(authuser)`: server-side
340
- * revokes the slot's refresh-token family and clears the
341
- * `oxy_rt_${authuser}` cookie via `Set-Cookie`. The slot is removed from
342
- * the in-memory registry. If the slot was active, the next lowest
343
- * remaining authuser becomes active (or `null` when none remain).
344
- */
345
- signOutAuthuser(authuser: number): Promise<void>;
346
- /**
347
- * Sign out EVERY device-local account on this device.
348
- *
349
- * Calls `oxyServices.logoutAllSessionsViaCookie()`: server-side revokes
350
- * every presented family and `Set-Cookie`s an immediate expiry for every
351
- * recognised `oxy_rt_${n}` slot. The in-memory registry is wiped, the active
352
- * slot is cleared, and the persisted `oxy_active_authuser` is removed so the
353
- * next cold boot starts fresh.
354
- */
355
- signOutAllViaCookies(): Promise<void>;
356
- /**
357
- * Schedule an auto-refresh for the cookie path on the active slot. The
358
- * AuthManager has exactly one active slot at a time, so one timer suffices.
359
- */
360
- private setupCookieRefresh;
361
- /**
362
- * Decode the session id from an unverified JWT access token. Decode-only
363
- * (no signature verification) — the server already verified the
364
- * signature when minting the token. Returns `null` on malformed input.
365
- */
366
- private static decodeSessionIdFromAccessToken;
367
- private static decodeAuthuserFromAccessToken;
368
- /**
369
- * Destroy the auth manager and clean up resources.
370
- */
371
- destroy(): void;
372
- }
373
- /**
374
- * Create an AuthManager instance.
375
- *
376
- * @param oxyServices - OxyServices instance
377
- * @param config - Optional configuration
378
- * @returns AuthManager instance
379
- */
380
- export declare function createAuthManager(oxyServices: OxyServices, config?: AuthManagerConfig): AuthManager;