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