@oxyhq/core 8.1.0 → 9.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 (68) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/boot/coldBootV2.js +22 -332
  3. package/dist/cjs/crypto/keyManager.js +0 -95
  4. package/dist/cjs/index.js +6 -19
  5. package/dist/cjs/mixins/OxyServices.auth.js +4 -7
  6. package/dist/cjs/mixins/OxyServices.deviceBoot.js +19 -115
  7. package/dist/cjs/mixins/index.js +2 -3
  8. package/dist/cjs/session/SessionClient.js +29 -100
  9. package/dist/cjs/session/accountDialogController.js +0 -13
  10. package/dist/cjs/session/authStateStore.js +9 -89
  11. package/dist/cjs/session/createSessionClient.js +2 -9
  12. package/dist/cjs/session/refresh.js +46 -71
  13. package/dist/cjs/utils/registrableApex.js +2 -6
  14. package/dist/esm/.tsbuildinfo +1 -1
  15. package/dist/esm/boot/coldBootV2.js +23 -330
  16. package/dist/esm/crypto/keyManager.js +0 -95
  17. package/dist/esm/index.js +7 -11
  18. package/dist/esm/mixins/OxyServices.auth.js +4 -7
  19. package/dist/esm/mixins/OxyServices.deviceBoot.js +20 -116
  20. package/dist/esm/mixins/index.js +2 -3
  21. package/dist/esm/session/SessionClient.js +29 -100
  22. package/dist/esm/session/accountDialogController.js +0 -13
  23. package/dist/esm/session/authStateStore.js +8 -88
  24. package/dist/esm/session/createSessionClient.js +2 -9
  25. package/dist/esm/session/refresh.js +46 -71
  26. package/dist/esm/utils/registrableApex.js +2 -6
  27. package/dist/types/.tsbuildinfo +1 -1
  28. package/dist/types/HttpService.d.ts +3 -3
  29. package/dist/types/boot/coldBootV2.d.ts +28 -53
  30. package/dist/types/crypto/keyManager.d.ts +0 -21
  31. package/dist/types/index.d.ts +3 -5
  32. package/dist/types/mixins/OxyServices.auth.d.ts +4 -7
  33. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +20 -60
  34. package/dist/types/session/SessionClient.d.ts +4 -38
  35. package/dist/types/session/accountDialogController.d.ts +1 -3
  36. package/dist/types/session/authStateStore.d.ts +32 -57
  37. package/dist/types/session/createSessionClient.d.ts +2 -9
  38. package/dist/types/session/refresh.d.ts +32 -28
  39. package/dist/types/utils/registrableApex.d.ts +2 -6
  40. package/package.json +2 -2
  41. package/src/HttpService.ts +3 -3
  42. package/src/boot/__tests__/coldBootV2.test.ts +140 -353
  43. package/src/boot/coldBootV2.ts +35 -391
  44. package/src/crypto/keyManager.ts +0 -101
  45. package/src/index.ts +7 -30
  46. package/src/mixins/OxyServices.auth.ts +5 -9
  47. package/src/mixins/OxyServices.deviceBoot.ts +19 -142
  48. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +9 -96
  49. package/src/mixins/__tests__/onTokensChanged.test.ts +0 -1
  50. package/src/mixins/__tests__/passwordSignIn.test.ts +33 -9
  51. package/src/mixins/index.ts +2 -3
  52. package/src/session/SessionClient.ts +29 -120
  53. package/src/session/__tests__/SessionClient.broadcastChannel.test.ts +113 -0
  54. package/src/session/__tests__/SessionClient.socket.test.ts +8 -8
  55. package/src/session/__tests__/authStateStore.test.ts +29 -42
  56. package/src/session/__tests__/refresh.test.ts +66 -52
  57. package/src/session/accountDialogController.ts +4 -18
  58. package/src/session/authStateStore.ts +32 -126
  59. package/src/session/createSessionClient.ts +2 -9
  60. package/src/session/refresh.ts +60 -92
  61. package/src/utils/registrableApex.ts +2 -6
  62. package/dist/cjs/boot/deviceBootReturn.js +0 -167
  63. package/dist/esm/boot/deviceBootReturn.js +0 -161
  64. package/dist/types/boot/deviceBootReturn.d.ts +0 -83
  65. package/src/boot/__tests__/deviceBootReturn.test.ts +0 -190
  66. package/src/boot/deviceBootReturn.ts +0 -210
  67. package/src/crypto/__tests__/sharedDeviceToken.test.ts +0 -24
  68. package/src/session/__tests__/SessionClient.signedOut.test.ts +0 -224
@@ -1,10 +1,11 @@
1
1
  import { isNative } from '../utils/platform.js';
2
+ import { extractErrorStatus } from '../utils/errorUtils.js';
2
3
  import { logger } from '../utils/loggerUtils.js';
3
4
  /**
4
5
  * Lead time (ms) before access-token expiry at which the proactive scheduler
5
- * refreshes. Mirrors `HttpService`'s per-request `TOKEN_REFRESH_LEAD_SECONDS`
6
- * (60s) so the scheduled refresh and the request-time preflight refresh use
7
- * the same window — the scheduler just fires it during idle/background.
6
+ * re-mints. Mirrors `HttpService`'s per-request `TOKEN_REFRESH_LEAD_SECONDS`
7
+ * (60s) so the scheduled re-mint and the request-time preflight use the same
8
+ * window — the scheduler just fires it during idle/background.
8
9
  */
9
10
  export const TOKEN_REFRESH_LEAD_MS = 60000;
10
11
  /**
@@ -16,13 +17,13 @@ const MAX_TIMEOUT_DELAY_MS = 2147483647;
16
17
  /**
17
18
  * Floor (ms) on ANY scheduled delay. An already-expired / in-lead-window token
18
19
  * computes a non-positive `exp − now − lead`; without this floor that becomes
19
- * `setTimeout(…, 0)`, and a FAILING refresh (offline / server error) would
20
+ * `setTimeout(…, 0)`, and a FAILING re-mint (offline / server error) would
20
21
  * re-arm at 0 in the finally block → a tight 100%-CPU busy loop. The floor
21
22
  * guarantees every re-arm yields the event loop.
22
23
  */
23
24
  const MIN_SCHEDULE_DELAY_MS = 1000;
24
25
  /**
25
- * Backoff schedule (ms) applied when a scheduled refresh FAILS: first retry
26
+ * Backoff schedule (ms) applied when a scheduled re-mint FAILS: first retry
26
27
  * after {@link MIN_FAILURE_BACKOFF_MS}, doubling up to {@link MAX_FAILURE_BACKOFF_MS}.
27
28
  * Reset to 0 on any success or token change. This is what converts the former
28
29
  * zero-delay failure loop into a bounded, backing-off retry.
@@ -30,81 +31,55 @@ const MIN_SCHEDULE_DELAY_MS = 1000;
30
31
  const MIN_FAILURE_BACKOFF_MS = 5000;
31
32
  const MAX_FAILURE_BACKOFF_MS = 5 * 60000;
32
33
  /**
33
- * Error codes (in addition to HTTP 401/403) that mean the stored refresh token
34
- * is permanently unusable — the family was revoked or a reuse was detected. On
35
- * any of these the persisted store is CLEARED (the session is truly over);
36
- * transient failures (network, 5xx) leave the store intact so a later attempt
37
- * can still succeed.
38
- */
39
- const REVOKED_REFRESH_CODES = new Set([
40
- 'invalid_grant',
41
- 'refresh_token_revoked',
42
- 'refresh_token_reuse',
43
- 'token_reuse',
44
- 'invalid_token',
45
- ]);
46
- /** Does this error mean the refresh token is permanently dead (vs. transient)? */
47
- function isRevokedRefreshError(error) {
48
- if (!error || typeof error !== 'object') {
49
- return false;
50
- }
51
- const e = error;
52
- if (e.status === 401 || e.status === 403) {
53
- return true;
54
- }
55
- return typeof e.code === 'string' && REVOKED_REFRESH_CODES.has(e.code);
56
- }
57
- /**
58
- * Rotate the persisted session and return the fresh access token, or `null`
34
+ * Re-mint the persisted session and return the fresh access token, or `null`
59
35
  * when no arm could produce one.
60
36
  *
61
- * Arm 1 (`POST /auth/refresh-token`): if the store holds a refresh token, rotate
62
- * it — on success plant + persist the rotated pair; on a REVOKED error clear the
63
- * store; on a transient error leave the store and return `null`.
37
+ * Arm 1 (`POST /session/device/token`): if the store holds a `deviceId` +
38
+ * `deviceSecret`, mint — on success plant + persist the rotated secret. A 401
39
+ * means the secret is diverged (`invalid_device_secret`) or the device has no
40
+ * live session (`no_active_session`): drop the secret so the mint lane stops (or
41
+ * clear the store on web, where there is no fallback). A transient error leaves
42
+ * the store and returns `null`.
64
43
  *
65
- * Arm 2 (native shared-keychain): when there is no refresh token or arm 1 was
66
- * revoked, re-mint via `signInWithSharedIdentity` (which plants tokens). The
67
- * shared keychain — not the per-origin store — is the durable native credential,
68
- * so this arm does not write the store.
44
+ * Arm 2 (native shared-keychain): when the secret is absent or was just rejected,
45
+ * re-mint via `signInWithSharedIdentity` (which plants tokens). The shared
46
+ * keychain — not the per-origin store — is the durable native credential, so this
47
+ * arm does not write the store.
69
48
  */
70
49
  export async function refreshPersistedSession(deps) {
71
50
  const { oxy, store } = deps;
72
51
  const allowSharedKeyFallback = deps.allowSharedKeyFallback ?? isNative();
73
52
  const persisted = await store.load();
74
- if (persisted?.refreshToken) {
53
+ if (persisted?.deviceId && persisted?.deviceSecret) {
75
54
  try {
76
- const rotated = await oxy.refreshWithToken(persisted.refreshToken);
77
- oxy.setTokens(rotated.accessToken);
55
+ const mint = await oxy.mintFromDeviceSecret(persisted.deviceId, persisted.deviceSecret);
56
+ oxy.setTokens(mint.accessToken);
57
+ const active = mint.state.accounts.find((a) => a.accountId === mint.state.activeAccountId);
78
58
  const next = {
79
- sessionId: rotated.sessionId,
80
- refreshToken: rotated.refreshToken,
81
- userId: persisted.userId,
82
- accessToken: rotated.accessToken,
83
- expiresAt: rotated.expiresAt,
59
+ ...persisted,
60
+ deviceId: mint.state.deviceId,
61
+ deviceSecret: mint.nextDeviceSecret,
62
+ accessToken: mint.accessToken,
63
+ expiresAt: mint.expiresAt,
64
+ ...(active ? { sessionId: active.sessionId, userId: active.accountId } : {}),
84
65
  };
85
- if (persisted.deviceToken) {
86
- next.deviceToken = persisted.deviceToken;
87
- }
88
- // The refresh response carries no device credentials — carry the persisted
89
- // deviceId/deviceSecret (phase 2c) forward so a rotation never drops the
90
- // zero-cookie mint lane (mirrors the deviceToken preservation above).
91
- if (persisted.deviceId) {
92
- next.deviceId = persisted.deviceId;
93
- }
94
- if (persisted.deviceSecret) {
95
- next.deviceSecret = persisted.deviceSecret;
96
- }
97
66
  await store.save(next);
98
- return rotated.accessToken;
67
+ return mint.accessToken;
99
68
  }
100
69
  catch (error) {
101
- if (isRevokedRefreshError(error)) {
102
- await store.clear();
103
- // Fall through to the native shared-key arm below on a shared-key
104
- // device the refresh family being revoked does not end the session.
70
+ if (extractErrorStatus(error) === 401) {
71
+ // Secret diverged / no active session. On a shared-key device drop only
72
+ // the secret so arm 2 below can recover; otherwise the session is over.
73
+ if (allowSharedKeyFallback) {
74
+ await store.save({ ...persisted, deviceSecret: undefined });
75
+ }
76
+ else {
77
+ await store.clear();
78
+ }
79
+ // Fall through to the native shared-key arm.
105
80
  }
106
81
  else {
107
- logger.debug('Persisted refresh failed (transient) — keeping store', { component: 'refresh', method: 'refreshPersistedSession' }, error);
82
+ logger.debug('Persisted deviceSecret mint failed (transient) — keeping store', { component: 'refresh', method: 'refreshPersistedSession' }, error);
108
83
  return null;
109
84
  }
110
85
  }
@@ -117,7 +92,7 @@ export async function refreshPersistedSession(deps) {
117
92
  }
118
93
  }
119
94
  catch (error) {
120
- logger.debug('Shared-key refresh fallback failed', { component: 'refresh', method: 'refreshPersistedSession' }, error);
95
+ logger.debug('Shared-key re-mint fallback failed', { component: 'refresh', method: 'refreshPersistedSession' }, error);
121
96
  }
122
97
  }
123
98
  return null;
@@ -145,7 +120,7 @@ export function installAuthRefreshHandler(deps) {
145
120
  };
146
121
  }
147
122
  /**
148
- * Start the proactive refresh scheduler against `oxy`.
123
+ * Start the proactive re-mint scheduler against `oxy`.
149
124
  *
150
125
  * Schedules a single timer to fire {@link TOKEN_REFRESH_LEAD_MS} before the
151
126
  * current access token's `exp`, calling
@@ -153,18 +128,18 @@ export function installAuthRefreshHandler(deps) {
153
128
  * handler; deduped + cooldown-guarded). After every attempt it reschedules
154
129
  * from the possibly-rotated token. It also reschedules whenever the token
155
130
  * changes (a sign-out that clears the token cancels the timer) and, on web
156
- * tab-focus, refreshes immediately if already inside the lead window (a
131
+ * tab-focus, re-mints immediately if already inside the lead window (a
157
132
  * long-hidden tab throttles timers, so the token can be expired on return).
158
133
  *
159
134
  * No-ops cleanly when there is no token or an opaque/no-`exp` token — the
160
- * reactive 401 path stays the only refresh trigger in that case. The timer is
135
+ * reactive 401 path stays the only re-mint trigger in that case. The timer is
161
136
  * `.unref?.()`-ed so it never keeps a Node/Jest event loop alive.
162
137
  */
163
138
  export function startTokenRefreshScheduler(oxy) {
164
139
  let disposed = false;
165
140
  let timer = null;
166
141
  // 0 = no active backoff; grows on consecutive failures, resets on success /
167
- // token change. Keeps a failing refresh from re-arming at zero delay.
142
+ // token change. Keeps a failing re-mint from re-arming at zero delay.
168
143
  let failureBackoffMs = 0;
169
144
  const clearTimer = () => {
170
145
  if (timer !== null) {
@@ -177,10 +152,10 @@ export function startTokenRefreshScheduler(oxy) {
177
152
  clearTimer();
178
153
  const clamped = Math.min(Math.max(delayMs, MIN_SCHEDULE_DELAY_MS), MAX_TIMEOUT_DELAY_MS);
179
154
  timer = setTimeout(runRefresh, clamped);
180
- // Never keep a Node/Jest event loop alive for a background refresh timer.
155
+ // Never keep a Node/Jest event loop alive for a background re-mint timer.
181
156
  timer.unref?.();
182
157
  };
183
- /** Schedule the next refresh from the current token's expiry (the healthy path). */
158
+ /** Schedule the next re-mint from the current token's expiry (the healthy path). */
184
159
  const scheduleFromExpiry = () => {
185
160
  clearTimer();
186
161
  if (disposed || !oxy.getAccessToken()) {
@@ -4,12 +4,8 @@
4
4
  * The client FAPI auto-detection helper was removed in the device-first cutover
5
5
  * (which is why this file is now named for what it actually is, not the old
6
6
  * `fapiAutoDetect`). What survives is the pure registrable-domain kernel, still
7
- * used server-side by the api's device-first same-apex trust checks
8
- * (`deviceAuth.ts`'s `POST /auth/device/web-session`, via `sameSite.ts`'s
9
- * `isSameSiteTrustedRequest`) and the `@oxyhq/core/server` CORS/re-export layer.
10
- *
11
- * `registrableApex` is NOT legacy — the device-first same-apex check is a live
12
- * consumer, so this kernel stays regardless of the SSO/FedCM removal.
7
+ * used by the `@oxyhq/core/server` CORS layer (the `*.oxy.so` same-apex trust
8
+ * check) and its re-export surface.
13
9
  */
14
10
  import { getDomain } from 'tldts';
15
11
  /**