@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,224 +0,0 @@
1
- import type { DeviceSessionState } from '@oxyhq/contracts';
2
-
3
- type Handler = (...args: unknown[]) => void;
4
- class FakeSocket {
5
- connected = false;
6
- handlers = new Map<string, Handler[]>();
7
- connectCalls = 0;
8
- disconnectCalls = 0;
9
- on(event: string, cb: Handler) { const l = this.handlers.get(event) ?? []; l.push(cb); this.handlers.set(event, l); }
10
- off(event: string, cb?: Handler) { if (!cb) { this.handlers.delete(event); return; } this.handlers.set(event, (this.handlers.get(event) ?? []).filter((h) => h !== cb)); }
11
- connect() { this.connectCalls += 1; this.connected = true; this.trigger('connect'); }
12
- disconnect() { this.disconnectCalls += 1; this.connected = false; }
13
- trigger(event: string, ...args: unknown[]) { for (const h of this.handlers.get(event) ?? []) h(...args); }
14
- }
15
- let fakeSocket: FakeSocket;
16
- let lastOpts: Record<string, unknown> | undefined;
17
- const ioMock = jest.fn((_uri: string, opts?: Record<string, unknown>) => {
18
- lastOpts = opts;
19
- if (!opts || opts.autoConnect !== false) fakeSocket.connected = true;
20
- return fakeSocket;
21
- });
22
- jest.mock('socket.io-client', () => ({ __esModule: true, io: (...args: unknown[]) => ioMock(...(args as [string, Record<string, unknown>?])) }));
23
-
24
- // A same-name in-process BroadcastChannel bus: postMessage delivers to every
25
- // OTHER open channel of the same name (never the sender) — matching the spec.
26
- type BusEntry = { name: string; onmessage: ((event: { data: unknown }) => void) | null };
27
- const bus = new Set<BusEntry>();
28
- class FakeBroadcastChannel {
29
- private entry: BusEntry;
30
- constructor(public name: string) { this.entry = { name, onmessage: null }; bus.add(this.entry); }
31
- get onmessage(): ((event: { data: unknown }) => void) | null { return this.entry.onmessage; }
32
- set onmessage(cb: ((event: { data: unknown }) => void) | null) { this.entry.onmessage = cb; }
33
- postMessage(data: unknown) {
34
- for (const e of bus) {
35
- if (e === this.entry || e.name !== this.name) continue;
36
- e.onmessage?.({ data });
37
- }
38
- }
39
- close() { bus.delete(this.entry); }
40
- }
41
-
42
- import { SessionClient, type SessionClientHost, type SessionClientOptions } from '../SessionClient';
43
-
44
- const STATE = (rev: number, accounts = [{ accountId: 'a1', sessionId: 's1', authuser: 0 }]): DeviceSessionState =>
45
- ({ deviceId: 'd1', accounts, activeAccountId: accounts[0]?.accountId ?? null, revision: rev, updatedAt: 1720000000000 });
46
- const SYNC = (rev: number) => ({ state: STATE(rev), activeToken: { accessToken: `jwt-${rev}`, expiresAt: 'x' } });
47
-
48
- function makeHost(over: Partial<SessionClientHost> = {}): SessionClientHost {
49
- return {
50
- makeRequest: jest.fn().mockResolvedValue(SYNC(1)),
51
- getBaseURL: () => 'http://test.invalid',
52
- getAccessToken: () => null,
53
- onTokensChanged: () => () => undefined,
54
- setTokens: jest.fn(),
55
- getCurrentAccountId: () => null,
56
- ...over,
57
- };
58
- }
59
-
60
- const flush = async () => { await Promise.resolve(); await Promise.resolve(); };
61
-
62
- beforeEach(() => {
63
- fakeSocket = new FakeSocket();
64
- lastOpts = undefined;
65
- ioMock.mockClear();
66
- bus.clear();
67
- (globalThis as { BroadcastChannel?: unknown }).BroadcastChannel = FakeBroadcastChannel as unknown;
68
- });
69
- afterEach(() => {
70
- (globalThis as { BroadcastChannel?: unknown }).BroadcastChannel = undefined;
71
- });
72
-
73
- describe('SessionClient signed-out socket', () => {
74
- it('opens the socket while signed-out when signedOutSocketAuth resolves true (web cookie), with credentials', async () => {
75
- const c = new SessionClient(makeHost(), { signedOutSocketAuth: () => true });
76
- await c.start();
77
- expect(ioMock).toHaveBeenCalledTimes(1);
78
- expect(lastOpts?.autoConnect).toBe(true);
79
- expect(lastOpts?.withCredentials).toBe(true);
80
- // No bearer, no native token → handshake presents an empty token, no deviceToken.
81
- const authCb = jest.fn();
82
- (lastOpts?.auth as (cb: (d: unknown) => void) => void)(authCb);
83
- expect(authCb).toHaveBeenCalledWith({ token: '' });
84
- c.stop();
85
- });
86
-
87
- it('presents the native device token in the handshake when signedOutSocketAuth returns a string', async () => {
88
- const c = new SessionClient(makeHost(), { signedOutSocketAuth: () => 'dt-native' });
89
- await c.start();
90
- expect(lastOpts?.autoConnect).toBe(true);
91
- const authCb = jest.fn();
92
- (lastOpts?.auth as (cb: (d: unknown) => void) => void)(authCb);
93
- expect(authCb).toHaveBeenCalledWith({ token: '', deviceToken: 'dt-native' });
94
- c.stop();
95
- });
96
-
97
- it('does NOT open the socket while signed-out when signedOutSocketAuth is absent (default)', async () => {
98
- const c = new SessionClient(makeHost());
99
- await c.start();
100
- expect(lastOpts?.autoConnect).toBe(false);
101
- c.stop();
102
- });
103
-
104
- it('does NOT open the socket while signed-out when signedOutSocketAuth resolves false', async () => {
105
- const c = new SessionClient(makeHost(), { signedOutSocketAuth: async () => false });
106
- await c.start();
107
- expect(lastOpts?.autoConnect).toBe(false);
108
- c.stop();
109
- });
110
-
111
- it('skips the bearer-authenticated bootstrap when signed-out', async () => {
112
- const makeRequest = jest.fn().mockResolvedValue(SYNC(1));
113
- const c = new SessionClient(makeHost({ makeRequest }), { signedOutSocketAuth: () => true });
114
- await c.start();
115
- expect(makeRequest).not.toHaveBeenCalled();
116
- c.stop();
117
- });
118
-
119
- it('a session_state push while signed-out triggers acquisition exactly once for a burst', async () => {
120
- const onSessionAppeared = jest.fn(() => new Promise<void>(() => undefined)); // never resolves (in flight)
121
- const c = new SessionClient(makeHost(), { signedOutSocketAuth: () => true, onSessionAppeared });
122
- await c.start();
123
- fakeSocket.trigger('session_state', STATE(9));
124
- fakeSocket.trigger('session_state', STATE(10));
125
- await flush();
126
- expect(onSessionAppeared).toHaveBeenCalledTimes(1);
127
- c.stop();
128
- });
129
-
130
- it('does NOT acquire when the pushed signed-out state has zero accounts', async () => {
131
- const onSessionAppeared = jest.fn();
132
- const c = new SessionClient(makeHost(), { signedOutSocketAuth: () => true, onSessionAppeared });
133
- await c.start();
134
- fakeSocket.trigger('session_state', STATE(9, []));
135
- await flush();
136
- expect(onSessionAppeared).not.toHaveBeenCalled();
137
- c.stop();
138
- });
139
-
140
- it('re-acquires on a LATER push after the prior acquisition settled (no permanent lock)', async () => {
141
- const onSessionAppeared = jest.fn(() => Promise.resolve());
142
- const c = new SessionClient(makeHost(), { signedOutSocketAuth: () => true, onSessionAppeared });
143
- await c.start();
144
- fakeSocket.trigger('session_state', STATE(9));
145
- await flush();
146
- expect(onSessionAppeared).toHaveBeenCalledTimes(1);
147
- fakeSocket.trigger('session_state', STATE(10));
148
- await flush();
149
- expect(onSessionAppeared).toHaveBeenCalledTimes(2);
150
- c.stop();
151
- });
152
-
153
- it('reconnects the anonymous socket authenticated once a token arrives', async () => {
154
- let tokenListener: ((t: string | null) => void) | null = null;
155
- let token: string | null = null;
156
- const host = makeHost({
157
- getAccessToken: () => token,
158
- onTokensChanged: (l) => { tokenListener = l; return () => undefined; },
159
- });
160
- const c = new SessionClient(host, { signedOutSocketAuth: () => true });
161
- await c.start();
162
- expect(fakeSocket.connected).toBe(true); // anonymous connect
163
- const before = fakeSocket.disconnectCalls;
164
- token = 'fresh';
165
- tokenListener?.('fresh');
166
- // Anonymous → authenticated: force a reconnect so the handshake re-runs.
167
- expect(fakeSocket.disconnectCalls).toBe(before + 1);
168
- expect(fakeSocket.connected).toBe(true);
169
- c.stop();
170
- });
171
- });
172
-
173
- describe('SessionClient BroadcastChannel wake', () => {
174
- const opts = (over: Partial<SessionClientOptions>): SessionClientOptions => ({ signedOutSocketAuth: () => true, ...over });
175
-
176
- it('a local mutation wakes a signed-out sibling to acquire', async () => {
177
- const onSessionAppeared = jest.fn(() => Promise.resolve());
178
- // Signed-out sibling B listening on the channel.
179
- const b = new SessionClient(makeHost(), opts({ onSessionAppeared }));
180
- await b.start();
181
- // Signed-in tab A commits (switchAccount posts a commit ping).
182
- const a = new SessionClient(makeHost({ getAccessToken: () => 'tok-a' }), opts({}));
183
- await a.start();
184
- await a.switchAccount('a1');
185
- await flush();
186
- expect(onSessionAppeared).toHaveBeenCalledTimes(1);
187
- a.stop();
188
- b.stop();
189
- });
190
-
191
- it('a local mutation wakes a signed-in sibling to re-sync (bootstrap)', async () => {
192
- const bMakeRequest = jest.fn().mockResolvedValue(SYNC(1));
193
- const b = new SessionClient(makeHost({ makeRequest: bMakeRequest, getAccessToken: () => 'tok-b' }), opts({}));
194
- await b.start();
195
- bMakeRequest.mockClear();
196
- const a = new SessionClient(makeHost({ getAccessToken: () => 'tok-a' }), opts({}));
197
- await a.start();
198
- await a.addCurrentAccount();
199
- await flush();
200
- expect(bMakeRequest).toHaveBeenCalledWith('GET', '/session/device/state', undefined, { cache: false });
201
- a.stop();
202
- b.stop();
203
- });
204
-
205
- it('does not deliver a commit ping back to the posting tab (no self-loop)', async () => {
206
- // A signed-out tab that commits locally must not re-trigger its OWN acquisition.
207
- const onSessionAppeared = jest.fn();
208
- const a = new SessionClient(makeHost(), opts({ onSessionAppeared }));
209
- await a.start();
210
- await a.addCurrentAccount();
211
- await flush();
212
- expect(onSessionAppeared).not.toHaveBeenCalled();
213
- a.stop();
214
- });
215
-
216
- it('is a no-op on platforms without BroadcastChannel (native)', async () => {
217
- (globalThis as { BroadcastChannel?: unknown }).BroadcastChannel = undefined;
218
- const c = new SessionClient(makeHost({ getAccessToken: () => 'tok' }), { });
219
- await c.start();
220
- // Mutations must not throw when BroadcastChannel is absent.
221
- await expect(c.switchAccount('a1')).resolves.toBeUndefined();
222
- c.stop();
223
- });
224
- });