@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,119 +1,33 @@
1
1
  /**
2
- * Device-first bootstrap mixin (auth centralization, wave 1).
2
+ * Device-first token mint mixin.
3
3
  *
4
- * The client half of the new device-first session bootstrap: the four network
5
- * calls the cold boot (`coldBootV2`) and the unified refresh handler
6
- * (`refresh.ts`) make, plus the URL builder for the cross-apex bootstrap hop.
7
- * Every response is validated against the `@oxyhq/contracts` `deviceBoot`
8
- * schemas via `safeParseContract`, so producer (oxy-api) and consumer cannot
4
+ * The client half of the zero-cookie device transport: the single network call
5
+ * the cold boot (`coldBootV2`) and the unified re-mint handler (`refresh.ts`)
6
+ * make to turn a first-party `deviceId` + `deviceSecret` into a fresh access
7
+ * token. The response is validated against the `@oxyhq/contracts`
8
+ * `deviceTokenMintResponseSchema`, so producer (oxy-api) and consumer cannot
9
9
  * drift — an unexpected shape throws here rather than silently corrupting the
10
10
  * persisted store.
11
11
  *
12
- * These methods are ADDITIVE and carry NO persistence or token-planting side
13
- * effects of their own (except the bearer-authenticated calls that naturally
14
- * flow through `HttpService`). The cold boot / refresh handler own persistence
15
- * and `setTokens`, so the same network primitive can be reused from either
16
- * without double-planting.
12
+ * This method carries NO persistence or token-planting side effects of its own;
13
+ * the cold boot / re-mint handler own persistence and `setTokens`, so the same
14
+ * primitive can be reused from either without double-planting.
17
15
  */
18
- import { authTokenBundleSchema, tokenRefreshResponseSchema, deviceTokenIssueResponseSchema, deviceTokenMintResponseSchema, webSessionResultSchema, safeParseContract, } from '@oxyhq/contracts';
16
+ import { deviceTokenMintResponseSchema, safeParseContract, } from '@oxyhq/contracts';
19
17
  export function OxyServicesDeviceBootMixin(Base) {
20
18
  return class extends Base {
21
19
  /**
22
- * Exchange a single-use boot `code` (from the `#oxy_boot` return fragment)
23
- * for a token bundle. Origin-bound + GETDEL-burned server-side.
20
+ * Zero-cookie mint. Present the first-party `deviceId` + `deviceSecret` to
21
+ * `POST /session/device/token` NO bearer, NO cookies: possession of the
22
+ * secret IS the device-ownership proof. Returns a fresh short access token
23
+ * for the device's active account plus `nextDeviceSecret` (rotation-in-use)
24
+ * and the projected device-session `state`.
24
25
  *
25
- * @throws if the response does not match {@link authTokenBundleSchema}.
26
- */
27
- async exchangeBootCode(code) {
28
- try {
29
- const res = await this.makeRequest('POST', '/auth/device/exchange', { code }, { cache: false });
30
- const parsed = safeParseContract(authTokenBundleSchema, res);
31
- if (!parsed) {
32
- throw new Error('device/exchange returned an unexpected response shape');
33
- }
34
- return parsed;
35
- }
36
- catch (error) {
37
- throw this.handleError(error);
38
- }
39
- }
40
- /**
41
- * Same-site fast path (`*.oxy.so` apps). Reads the first-party `oxy_device`
42
- * cookie via a credentialed same-origin fetch and either resolves a full
43
- * session bundle or reports the device is known-but-signed-out. Never
44
- * redirects.
45
- *
46
- * @throws if the response matches neither arm of {@link webSessionResultSchema}.
47
- */
48
- async requestWebSession() {
49
- try {
50
- const res = await this.makeRequest('POST', '/auth/device/web-session', undefined, { cache: false });
51
- const parsed = safeParseContract(webSessionResultSchema, res);
52
- if (!parsed) {
53
- throw new Error('device/web-session returned an unexpected response shape');
54
- }
55
- return parsed;
56
- }
57
- catch (error) {
58
- throw this.handleError(error);
59
- }
60
- }
61
- /**
62
- * Rotate the persisted refresh-token family (web + native, one call). The
63
- * caller (refresh handler / cold boot) plants + persists the rotated pair.
64
- *
65
- * @throws if the response does not match {@link tokenRefreshResponseSchema}.
66
- */
67
- async refreshWithToken(refreshToken) {
68
- try {
69
- const res = await this.makeRequest('POST', '/auth/refresh-token', { refreshToken },
70
- // `skipAuth`: refresh-token is body-authenticated and is called from
71
- // inside the refresh handler — sending the near-expired bearer through
72
- // the preflight would deadlock. See RequestOptions.skipAuth.
73
- { cache: false, skipAuth: true });
74
- const parsed = safeParseContract(tokenRefreshResponseSchema, res);
75
- if (!parsed) {
76
- throw new Error('auth/refresh-token returned an unexpected response shape');
77
- }
78
- return parsed;
79
- }
80
- catch (error) {
81
- throw this.handleError(error);
82
- }
83
- }
84
- /**
85
- * Issue (or rotate) the native channel's opaque device token. Bearer-gated —
86
- * the server derives the deviceId from the JWT. Native apps mirror the
87
- * returned token into the shared keychain via
88
- * `KeyManager.setSharedDeviceToken`.
89
- *
90
- * @throws if the response does not match {@link deviceTokenIssueResponseSchema}.
91
- */
92
- async issueNativeDeviceToken() {
93
- try {
94
- const res = await this.makeRequest('POST', '/auth/device/token', undefined, { cache: false });
95
- const parsed = safeParseContract(deviceTokenIssueResponseSchema, res);
96
- if (!parsed) {
97
- throw new Error('auth/device/token returned an unexpected response shape');
98
- }
99
- return parsed.deviceToken;
100
- }
101
- catch (error) {
102
- throw this.handleError(error);
103
- }
104
- }
105
- /**
106
- * Zero-cookie mint (phase 2c). Present the first-party `deviceId` +
107
- * `deviceSecret` to `POST /session/device/token` — NO bearer, NO cookies:
108
- * possession of the secret IS the device-ownership proof. Returns a fresh
109
- * short access token for the device's active account plus `nextDeviceSecret`
110
- * (rotation-in-use) and the projected device-session `state`.
111
- *
112
- * `skipAuth` (like {@link refreshWithToken}): this call carries no bearer, so
113
- * a 401 must surface DIRECTLY — never trigger `HttpService`'s 401→refresh→
114
- * retry dance (which would pointlessly rotate the refresh family). The cold
115
- * boot reads the 401 body (`invalid_device_secret` vs `no_active_session`) to
116
- * decide whether to drop the secret and fall back or resolve signed-out.
26
+ * `skipAuth`: this call carries no bearer, so a 401 must surface DIRECTLY —
27
+ * never trigger `HttpService`'s 401→refresh→retry dance. The cold boot / re-
28
+ * mint handler read the 401 body (`invalid_device_secret` vs
29
+ * `no_active_session`) to decide whether to drop the secret and fall back or
30
+ * resolve signed-out.
117
31
  *
118
32
  * @throws if the response does not match {@link deviceTokenMintResponseSchema}.
119
33
  */
@@ -130,15 +44,5 @@ export function OxyServicesDeviceBootMixin(Base) {
130
44
  throw this.handleError(error);
131
45
  }
132
46
  }
133
- /**
134
- * Build the top-level `GET /auth/device/bootstrap` URL for the cross-apex
135
- * hop. The server validates `return_to` against the trusted-origin lane and
136
- * echoes `state` back in the return fragment for CSRF verification.
137
- */
138
- buildBootstrapUrl(returnTo, state) {
139
- const base = this.getBaseURL().replace(/\/+$/, '');
140
- const params = new URLSearchParams({ return_to: returnTo, state });
141
- return `${base}/auth/device/bootstrap?${params.toString()}`;
142
- }
143
47
  };
144
48
  }
@@ -75,9 +75,8 @@ const MIXIN_PIPELINE = [
75
75
  // Link previews / unfurls: SDK-owned link-metadata resolution via oxy-api,
76
76
  // so apps stop scraping link metadata locally.
77
77
  OxyServicesLinksMixin,
78
- // Device-first session bootstrap: the client half of the new
79
- // /auth/device/* + /auth/refresh-token surface (exchange, web-session,
80
- // refresh, native device-token, bootstrap-url builder).
78
+ // Device-first token mint: the client half of the zero-cookie transport
79
+ // (`mintFromDeviceSecret` → `POST /session/device/token`).
81
80
  OxyServicesDeviceBootMixin,
82
81
  // Utility (last, can use all above)
83
82
  OxyServicesUtilityMixin,
@@ -2,11 +2,12 @@ import { deviceSessionStateSchema, deviceSessionSyncSchema, safeParseContract, }
2
2
  import { logger } from '../utils/loggerUtils.js';
3
3
  import { getSocketIO } from './socketLoader.js';
4
4
  /**
5
- * Same-origin `BroadcastChannel` name for instant, network-free session wake
6
- * across tabs of the SAME origin (e.g. two `accounts.oxy.so` tabs). Complements
7
- * the cookie-authed device socket, which covers same-apex cross-origin
8
- * (`accounts` `console` `inbox`, all `*.oxy.so`). Cross-APEX (mention.earth)
9
- * is covered by neither and relies on a reload — the documented limitation.
5
+ * Same-origin `BroadcastChannel` name for instant, network-free session-state
6
+ * propagation across tabs of the SAME origin (e.g. two `accounts.oxy.so` tabs):
7
+ * when one authenticated tab commits an account switch / sign-out, siblings
8
+ * re-sync their device state without waiting on the socket. Cross-origin
9
+ * same-apex propagation rides the authenticated device socket; cross-APEX
10
+ * (mention.earth) relies on a reload — the documented limitation.
10
11
  */
11
12
  const SESSION_BROADCAST_CHANNEL = 'oxy.session';
12
13
  export class SessionClient {
@@ -18,11 +19,7 @@ export class SessionClient {
18
19
  this.socket = null;
19
20
  this.tokenUnsub = null;
20
21
  this.started = false;
21
- /** In-flight guard so a burst of pushes triggers at most ONE acquisition. */
22
- this.acquiring = false;
23
- /** True while the live socket is an anonymous (signed-out) device connection. */
24
- this.socketAnonymous = false;
25
- /** Same-origin cross-tab wake channel; null on platforms without BroadcastChannel. */
22
+ /** Same-origin cross-tab state-propagation channel; null on platforms without BroadcastChannel. */
26
23
  this.channel = null;
27
24
  }
28
25
  getState() {
@@ -154,30 +151,20 @@ export class SessionClient {
154
151
  return;
155
152
  this.started = true;
156
153
  this.tokenUnsub = this.host.onTokensChanged((token) => {
157
- if (!token)
154
+ // A rotated/fresh bearer landed — reconnect a dropped socket so its
155
+ // handshake re-runs with the current token. Sign-out (null token) is
156
+ // handled by the consumer calling `stop()`.
157
+ if (!token || !this.socket)
158
158
  return;
159
- // A bearer just landed: an in-flight acquisition (if any) succeeded — clear
160
- // the guard so a later sign-out can re-acquire.
161
- this.acquiring = false;
162
- if (!this.socket)
163
- return;
164
- if (this.socketAnonymous) {
165
- // The live socket was an anonymous (device-room-only) connection. Force a
166
- // reconnect so the handshake re-runs authenticated and also joins the
167
- // `user:<id>` notification room.
168
- this.socketAnonymous = false;
169
- this.socket.disconnect();
170
- this.socket.connect();
171
- }
172
- else if (!this.socket.connected) {
159
+ if (!this.socket.connected) {
173
160
  this.socket.connect();
174
161
  }
175
162
  });
176
163
  this.openBroadcastChannel();
177
- // `bootstrap` (`GET /session/device/state`) is bearer-authenticated: skip it
178
- // when signed out (the signed-out socket below still joins the device room to
179
- // receive pushes). A bootstrap failure is non-fatal — the socket must still
180
- // connect so realtime sync survives a transient state-fetch error.
164
+ // `bootstrap` (`GET /session/device/state`) is bearer-authenticated. A
165
+ // signed-out client opens no socket and runs no bootstrap; a bootstrap
166
+ // failure is non-fatal — the socket still connects so realtime sync
167
+ // survives a transient state-fetch error.
181
168
  if (this.host.getAccessToken()) {
182
169
  try {
183
170
  await this.bootstrap();
@@ -190,8 +177,6 @@ export class SessionClient {
190
177
  }
191
178
  stop() {
192
179
  this.started = false;
193
- this.acquiring = false;
194
- this.socketAnonymous = false;
195
180
  if (this.tokenUnsub) {
196
181
  this.tokenUnsub();
197
182
  this.tokenUnsub = null;
@@ -222,52 +207,23 @@ export class SessionClient {
222
207
  }
223
208
  if (!this.started)
224
209
  return; // stopped while the dynamic import was in flight
225
- const hasToken = Boolean(this.host.getAccessToken());
226
- // Signed-out connect gate: when there is no bearer, only open the socket if
227
- // the consumer supplies a device anchor (web cookie → `true`; native token →
228
- // string). This lets an idle tab receive its device's `session_state` pushes
229
- // and self-acquire when a sibling signs in.
230
- let signedOutAuth = false;
231
- if (!hasToken && this.options.signedOutSocketAuth) {
232
- try {
233
- signedOutAuth = await this.options.signedOutSocketAuth();
234
- }
235
- catch (error) {
236
- logger.warn('[SessionClient] signedOutSocketAuth failed', { component: 'SessionClient' }, error);
237
- signedOutAuth = false;
238
- }
239
- if (!this.started)
240
- return; // stopped while the async gate was in flight
241
- }
242
- const signedOutConnect = signedOutAuth !== false && signedOutAuth != null;
243
- const signedOutDeviceToken = typeof signedOutAuth === 'string' ? signedOutAuth : undefined;
244
- this.socketAnonymous = !hasToken && signedOutConnect;
210
+ // Sockets are BEARER-ONLY: the server rejects any handshake without a valid
211
+ // bearer, so a signed-out client never opens a socket.
212
+ if (!this.host.getAccessToken())
213
+ return;
245
214
  const socket = io(this.host.getBaseURL(), {
246
215
  transports: ['websocket'],
247
- // Send the first-party `oxy_device` cookie on the same-site handshake so a
248
- // signed-out browser tab can be resolved to its device room server-side.
249
- withCredentials: true,
250
- autoConnect: hasToken || signedOutConnect,
216
+ autoConnect: true,
251
217
  auth: (cb) => {
252
- const token = this.host.getAccessToken() ?? '';
253
- // Present the native device token only while signed out (no bearer). Once
254
- // a token is held the bearer path wins and the deviceToken is redundant.
255
- cb(token || !signedOutDeviceToken ? { token } : { token, deviceToken: signedOutDeviceToken });
218
+ cb({ token: this.host.getAccessToken() ?? '' });
256
219
  },
257
220
  });
258
221
  socket.on('session_state', (payload) => {
259
222
  const applied = this.applyState(payload);
260
223
  if (!applied)
261
224
  return;
262
- // Signed-out tab: a session appeared on this device (a sibling signed in).
263
- // Acquire it so this tab flips to signed-in; the planted token then
264
- // reconnects the socket on the authenticated path.
265
- if (!this.host.getAccessToken()) {
266
- if ((this.state?.accounts.length ?? 0) > 0) {
267
- this.requestAcquisition();
268
- }
269
- return;
270
- }
225
+ // A push changed the active account on another device/tab re-fetch state
226
+ // to plant the access token for the newly-active account.
271
227
  const active = this.state?.activeAccountId ?? null;
272
228
  if (active && active !== this.host.getCurrentAccountId()) {
273
229
  void this.bootstrap().catch((error) => {
@@ -277,35 +233,11 @@ export class SessionClient {
277
233
  });
278
234
  this.socket = socket;
279
235
  }
280
- /**
281
- * Run the consumer's session acquisition at most once at a time. A returned
282
- * promise gates the next attempt (reset on settle), so a failed acquisition
283
- * can retry on the NEXT push while a burst of identical pushes cannot pile up.
284
- */
285
- requestAcquisition() {
286
- if (this.acquiring || !this.options.onSessionAppeared)
287
- return;
288
- this.acquiring = true;
289
- let result;
290
- try {
291
- result = this.options.onSessionAppeared();
292
- }
293
- catch (error) {
294
- this.acquiring = false;
295
- logger.error('[SessionClient] onSessionAppeared threw', error);
296
- return;
297
- }
298
- void Promise.resolve(result).catch((error) => {
299
- logger.warn('[SessionClient] onSessionAppeared rejected', { component: 'SessionClient' }, error);
300
- }).finally(() => {
301
- this.acquiring = false;
302
- });
303
- }
304
236
  /**
305
237
  * Open the same-origin `BroadcastChannel` (web only). A sibling tab that
306
- * commits a session posts a wake ping; on receipt a signed-in tab re-syncs its
307
- * device state and a signed-out tab self-acquires — instant + network-free for
308
- * the common "two tabs of the same origin" case, with no state (and no tokens)
238
+ * commits an account switch / sign-out posts a wake ping; on receipt an
239
+ * authenticated tab re-syncs its device state — instant + network-free for the
240
+ * common "two tabs of the same origin" case, with no state (and no tokens)
309
241
  * ever crossing the channel. No-op on native (no BroadcastChannel).
310
242
  */
311
243
  openBroadcastChannel() {
@@ -328,16 +260,13 @@ export class SessionClient {
328
260
  if (event.data.type !== 'commit')
329
261
  return;
330
262
  // BroadcastChannel never echoes to the posting context, so this is a
331
- // sibling's commit. Re-sync (signed-in) or acquire (signed-out). Neither
332
- // path re-posts, so there is no cross-tab ping loop.
263
+ // sibling's commit. An authenticated tab re-syncs its device state; the
264
+ // re-sync does not re-post, so there is no cross-tab ping loop.
333
265
  if (this.host.getAccessToken()) {
334
266
  void this.bootstrap().catch((error) => {
335
267
  logger.warn('[SessionClient] broadcast re-sync failed', { component: 'SessionClient' }, error);
336
268
  });
337
269
  }
338
- else {
339
- this.requestAcquisition();
340
- }
341
270
  };
342
271
  this.channel = channel;
343
272
  }
@@ -330,7 +330,6 @@ export class AccountDialogController {
330
330
  expiresAt: result.expiresAt,
331
331
  user: result.user,
332
332
  accessToken: result.accessToken,
333
- ...(readRefreshToken(result) ? { refreshToken: readRefreshToken(result) } : {}),
334
333
  }, result.user);
335
334
  }
336
335
  // Re-project + refetch immediately; the subscription also fires.
@@ -506,7 +505,6 @@ export class AccountDialogController {
506
505
  name: claimed.user.name,
507
506
  avatar: claimed.user.avatar ?? undefined,
508
507
  };
509
- const refreshToken = readRefreshToken(claimed);
510
508
  try {
511
509
  await this.completeSignIn({
512
510
  sessionId: claimed.sessionId || sessionId,
@@ -514,7 +512,6 @@ export class AccountDialogController {
514
512
  expiresAt: claimed.expiresAt ?? '',
515
513
  user: minimalUser,
516
514
  accessToken: claimed.accessToken,
517
- ...(refreshToken ? { refreshToken } : {}),
518
515
  }, minimalUser);
519
516
  }
520
517
  catch (error) {
@@ -604,16 +601,6 @@ export function createAccountDialogController(options) {
604
601
  // ---------------------------------------------------------------------------
605
602
  // Local helpers
606
603
  // ---------------------------------------------------------------------------
607
- /**
608
- * The rotating refresh-token family head is threaded on the runtime object by
609
- * the trusted device-flow / switch lanes even though it is NOT on the typed
610
- * return of `claimSessionByToken` / `switchToAccount`. Read it defensively so
611
- * the commit funnel can persist a durable session.
612
- */
613
- function readRefreshToken(value) {
614
- const token = value.refreshToken;
615
- return typeof token === 'string' ? token : undefined;
616
- }
617
604
  /** Current document URL on web; empty string where `location` is absent (native/SSR). */
618
605
  function currentLocationHref() {
619
606
  const location = globalThis.location;
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Persisted auth state — ONE shape, web + native.
3
3
  *
4
- * The device-first session model (auth-centralization wave 1) persists the
5
- * rotating refresh-token family head per ORIGIN so a reload restores the
6
- * session locally without a redirect. This module is the storage seam: a tiny
7
- * `load / save / clear` interface plus platform factories, so the cold boot
8
- * (`coldBootV2`) and the unified refresh handler (`refresh.ts`) never touch a
9
- * platform storage API directly.
4
+ * The zero-cookie device transport persists the device credential per ORIGIN so
5
+ * a reload restores the session locally without a redirect: `deviceId` +
6
+ * `deviceSecret` mint a fresh access token via `POST /session/device/token`.
7
+ * This module is the storage seam: a tiny `load / save / clear` interface plus
8
+ * platform factories, so the cold boot (`coldBootV2`) and the unified re-mint
9
+ * handler (`refresh.ts`) never touch a platform storage API directly.
10
10
  *
11
11
  * Platform-agnostic — the native factory takes an INJECTED key/value store
12
12
  * (`@oxyhq/services` passes a SecureStore-backed adapter) so `@oxyhq/core`
@@ -22,12 +22,6 @@
22
22
  * `oxy_shared_*` keychain keys in `KeyManager`, so it never collides.
23
23
  */
24
24
  export const AUTH_STATE_STORAGE_KEY = 'oxy.auth.v1';
25
- /**
26
- * Storage key for the long-lived device-attribution token. Separate from
27
- * {@link AUTH_STATE_STORAGE_KEY} because it must OUTLIVE a session `clear()`
28
- * (sign-out) — the device is unchanged across sign-ins.
29
- */
30
- export const DEVICE_TOKEN_STORAGE_KEY = 'oxy.device.v1';
31
25
  /**
32
26
  * Parse + shape-validate a stored blob. Returns `null` for anything that is
33
27
  * not a well-formed {@link PersistedAuthState} (absent, malformed JSON, wrong
@@ -49,21 +43,15 @@ function deserialize(raw) {
49
43
  }
50
44
  const candidate = parsed;
51
45
  if (typeof candidate.sessionId !== 'string' ||
52
- typeof candidate.refreshToken !== 'string' ||
53
46
  typeof candidate.userId !== 'string' ||
54
47
  candidate.sessionId.length === 0 ||
55
- candidate.refreshToken.length === 0 ||
56
48
  candidate.userId.length === 0) {
57
49
  return null;
58
50
  }
59
51
  const state = {
60
52
  sessionId: candidate.sessionId,
61
- refreshToken: candidate.refreshToken,
62
53
  userId: candidate.userId,
63
54
  };
64
- if (typeof candidate.deviceToken === 'string' && candidate.deviceToken.length > 0) {
65
- state.deviceToken = candidate.deviceToken;
66
- }
67
55
  if (typeof candidate.deviceId === 'string' && candidate.deviceId.length > 0) {
68
56
  state.deviceId = candidate.deviceId;
69
57
  }
@@ -86,7 +74,6 @@ function deserialize(raw) {
86
74
  */
87
75
  export function createMemoryAuthStateStore() {
88
76
  let current = null;
89
- let deviceToken = null;
90
77
  return {
91
78
  load: async () => current,
92
79
  save: async (state) => {
@@ -95,13 +82,6 @@ export function createMemoryAuthStateStore() {
95
82
  clear: async () => {
96
83
  current = null;
97
84
  },
98
- loadDeviceToken: async () => deviceToken,
99
- saveDeviceToken: async (token) => {
100
- deviceToken = token;
101
- },
102
- clearDeviceToken: async () => {
103
- deviceToken = null;
104
- },
105
85
  };
106
86
  }
107
87
  /**
@@ -132,8 +112,8 @@ function safeGetLocalStorage() {
132
112
  * for this page's lifetime — never throws on construction.
133
113
  * - Individual `getItem`/`setItem`/`removeItem` are each wrapped: a read that
134
114
  * throws yields `null`; a write that throws (quota, private mode) is
135
- * swallowed. The persisted-refresh lane treats a failed persist as "no
136
- * durable state" (falls back to the bootstrap hop) rather than crashing.
115
+ * swallowed. The re-mint lane treats a failed persist as "no durable state"
116
+ * rather than crashing.
137
117
  */
138
118
  export function createWebAuthStateStore() {
139
119
  const storage = safeGetLocalStorage();
@@ -145,7 +125,6 @@ export function createWebAuthStateStore() {
145
125
  // means "never written this session" → fall back to storage; any set value
146
126
  // (including `null` after clear) is authoritative and preferred over storage.
147
127
  let sessionMirror;
148
- let deviceTokenMirror;
149
128
  return {
150
129
  load: async () => {
151
130
  if (sessionMirror !== undefined) {
@@ -177,35 +156,6 @@ export function createWebAuthStateStore() {
177
156
  // Non-fatal — see save().
178
157
  }
179
158
  },
180
- loadDeviceToken: async () => {
181
- if (deviceTokenMirror !== undefined) {
182
- return deviceTokenMirror;
183
- }
184
- try {
185
- return storage.getItem(DEVICE_TOKEN_STORAGE_KEY);
186
- }
187
- catch {
188
- return null;
189
- }
190
- },
191
- saveDeviceToken: async (token) => {
192
- deviceTokenMirror = token;
193
- try {
194
- storage.setItem(DEVICE_TOKEN_STORAGE_KEY, token);
195
- }
196
- catch {
197
- // Non-fatal — see save().
198
- }
199
- },
200
- clearDeviceToken: async () => {
201
- deviceTokenMirror = null;
202
- try {
203
- storage.removeItem(DEVICE_TOKEN_STORAGE_KEY);
204
- }
205
- catch {
206
- // Non-fatal — see save().
207
- }
208
- },
209
159
  };
210
160
  }
211
161
  /**
@@ -220,7 +170,6 @@ export function createNativeAuthStateStore(storage) {
220
170
  // Same in-memory mirror as the web store — a locked/failed SecureStore write
221
171
  // must not silently lose the session for the app's lifetime.
222
172
  let sessionMirror;
223
- let deviceTokenMirror;
224
173
  return {
225
174
  load: async () => {
226
175
  if (sessionMirror !== undefined) {
@@ -251,34 +200,5 @@ export function createNativeAuthStateStore(storage) {
251
200
  // Non-fatal.
252
201
  }
253
202
  },
254
- loadDeviceToken: async () => {
255
- if (deviceTokenMirror !== undefined) {
256
- return deviceTokenMirror;
257
- }
258
- try {
259
- return await storage.getItem(DEVICE_TOKEN_STORAGE_KEY);
260
- }
261
- catch {
262
- return null;
263
- }
264
- },
265
- saveDeviceToken: async (token) => {
266
- deviceTokenMirror = token;
267
- try {
268
- await storage.setItem(DEVICE_TOKEN_STORAGE_KEY, token);
269
- }
270
- catch {
271
- // Non-fatal.
272
- }
273
- },
274
- clearDeviceToken: async () => {
275
- deviceTokenMirror = null;
276
- try {
277
- await storage.removeItem(DEVICE_TOKEN_STORAGE_KEY);
278
- }
279
- catch {
280
- // Non-fatal.
281
- }
282
- },
283
203
  };
284
204
  }
@@ -22,15 +22,8 @@ import { createSessionClientHost } from './sessionClientHost.js';
22
22
  * specifier (bundler-fragile in Metro/Expo-web and Vite against the published
23
23
  * dist). When omitted, the client falls back to the lazy loader.
24
24
  */
25
- export function createSessionClient(oxyServices, transport, socketFactory,
26
- /**
27
- * Optional signed-out realtime wiring: `signedOutSocketAuth` (open the socket
28
- * while signed out so an idle tab receives its device pushes) and
29
- * `onSessionAppeared` (self-acquire when a sibling signs in). See
30
- * {@link SessionClientOptions}.
31
- */
32
- extra) {
25
+ export function createSessionClient(oxyServices, transport, socketFactory) {
33
26
  const host = createSessionClientHost(oxyServices);
34
- const client = new SessionClient(host, { transport, socketFactory, ...extra });
27
+ const client = new SessionClient(host, { transport, socketFactory });
35
28
  return { client, host };
36
29
  }