@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/boot/coldBootV2.js +66 -281
- package/dist/cjs/crypto/keyManager.js +0 -95
- package/dist/cjs/index.js +6 -19
- package/dist/cjs/mixins/OxyServices.auth.js +4 -7
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +24 -92
- package/dist/cjs/mixins/index.js +2 -3
- package/dist/cjs/session/SessionClient.js +29 -100
- package/dist/cjs/session/accountDialogController.js +0 -13
- package/dist/cjs/session/authStateStore.js +14 -88
- package/dist/cjs/session/createSessionClient.js +2 -9
- package/dist/cjs/session/refresh.js +46 -62
- package/dist/cjs/utils/registrableApex.js +2 -6
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/coldBootV2.js +67 -279
- package/dist/esm/crypto/keyManager.js +0 -95
- package/dist/esm/index.js +7 -11
- package/dist/esm/mixins/OxyServices.auth.js +4 -7
- package/dist/esm/mixins/OxyServices.deviceBoot.js +25 -93
- package/dist/esm/mixins/index.js +2 -3
- package/dist/esm/session/SessionClient.js +29 -100
- package/dist/esm/session/accountDialogController.js +0 -13
- package/dist/esm/session/authStateStore.js +13 -87
- package/dist/esm/session/createSessionClient.js +2 -9
- package/dist/esm/session/refresh.js +46 -62
- package/dist/esm/utils/registrableApex.js +2 -6
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +3 -3
- package/dist/types/boot/coldBootV2.d.ts +28 -53
- package/dist/types/crypto/keyManager.d.ts +0 -21
- package/dist/types/index.d.ts +3 -5
- package/dist/types/mixins/OxyServices.auth.d.ts +4 -7
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +22 -46
- package/dist/types/session/SessionClient.d.ts +4 -38
- package/dist/types/session/accountDialogController.d.ts +1 -3
- package/dist/types/session/authStateStore.d.ts +38 -43
- package/dist/types/session/createSessionClient.d.ts +2 -9
- package/dist/types/session/refresh.d.ts +32 -28
- package/dist/types/utils/registrableApex.d.ts +2 -6
- package/package.json +2 -2
- package/src/HttpService.ts +3 -3
- package/src/boot/__tests__/coldBootV2.test.ts +237 -236
- package/src/boot/coldBootV2.ts +92 -333
- package/src/crypto/keyManager.ts +0 -101
- package/src/index.ts +7 -30
- package/src/mixins/OxyServices.auth.ts +5 -9
- package/src/mixins/OxyServices.deviceBoot.ts +30 -115
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +36 -80
- package/src/mixins/__tests__/onTokensChanged.test.ts +0 -1
- package/src/mixins/__tests__/passwordSignIn.test.ts +33 -9
- package/src/mixins/index.ts +2 -3
- package/src/session/SessionClient.ts +29 -120
- package/src/session/__tests__/SessionClient.broadcastChannel.test.ts +113 -0
- package/src/session/__tests__/SessionClient.socket.test.ts +8 -8
- package/src/session/__tests__/authStateStore.test.ts +47 -33
- package/src/session/__tests__/refresh.test.ts +72 -44
- package/src/session/accountDialogController.ts +4 -18
- package/src/session/authStateStore.ts +43 -111
- package/src/session/createSessionClient.ts +2 -9
- package/src/session/refresh.ts +60 -83
- package/src/utils/registrableApex.ts +2 -6
- package/dist/cjs/boot/deviceBootReturn.js +0 -152
- package/dist/esm/boot/deviceBootReturn.js +0 -146
- package/dist/types/boot/deviceBootReturn.d.ts +0 -83
- package/src/boot/__tests__/deviceBootReturn.test.ts +0 -158
- package/src/boot/deviceBootReturn.ts +0 -195
- package/src/crypto/__tests__/sharedDeviceToken.test.ts +0 -24
- 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
|
|
5
|
+
* Device-first token mint mixin.
|
|
6
6
|
*
|
|
7
|
-
* The client half of the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
35
|
+
* @throws if the response does not match {@link deviceTokenMintResponseSchema}.
|
|
50
36
|
*/
|
|
51
|
-
async
|
|
37
|
+
async mintFromDeviceSecret(deviceId, deviceSecret) {
|
|
52
38
|
try {
|
|
53
|
-
const res = await this.makeRequest('POST', '/
|
|
54
|
-
const parsed = (0, contracts_1.safeParseContract)(contracts_1.
|
|
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/
|
|
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
|
}
|
package/dist/cjs/mixins/index.js
CHANGED
|
@@ -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
|
|
83
|
-
// /
|
|
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
|
|
9
|
-
* across tabs of the SAME origin (e.g. two `accounts.oxy.so` tabs)
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
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
|
-
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
266
|
-
//
|
|
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
|
|
310
|
-
*
|
|
311
|
-
*
|
|
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.
|
|
335
|
-
//
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `load / save / clear` interface plus
|
|
9
|
-
*
|
|
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.
|
|
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.
|
|
71
|
-
state.
|
|
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
|
|
136
|
-
*
|
|
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
|
|
30
|
+
const client = new SessionClient_1.SessionClient(host, { transport, socketFactory });
|
|
38
31
|
return { client, host };
|
|
39
32
|
}
|