@oxyhq/core 8.0.0 → 8.1.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 +97 -2
- package/dist/cjs/boot/deviceBootReturn.js +15 -0
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +28 -0
- package/dist/cjs/session/authStateStore.js +6 -0
- package/dist/cjs/session/refresh.js +9 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/coldBootV2.js +97 -2
- package/dist/esm/boot/deviceBootReturn.js +15 -0
- package/dist/esm/mixins/OxyServices.deviceBoot.js +29 -1
- package/dist/esm/session/authStateStore.js +6 -0
- package/dist/esm/session/refresh.js +9 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +17 -1
- package/dist/types/session/authStateStore.d.ts +20 -0
- package/package.json +2 -2
- package/src/boot/__tests__/coldBootV2.test.ts +215 -1
- package/src/boot/__tests__/deviceBootReturn.test.ts +32 -0
- package/src/boot/coldBootV2.ts +117 -2
- package/src/boot/deviceBootReturn.ts +15 -0
- package/src/mixins/OxyServices.deviceBoot.ts +38 -0
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +44 -1
- package/src/session/__tests__/authStateStore.test.ts +27 -0
- package/src/session/__tests__/refresh.test.ts +14 -0
- package/src/session/authStateStore.ts +26 -0
- package/src/session/refresh.ts +9 -0
|
@@ -65,6 +65,33 @@ describe('createWebAuthStateStore', () => {
|
|
|
65
65
|
expect(await store.load()).toEqual(SAMPLE);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
it('round-trips the optional phase-2c deviceId + deviceSecret', async () => {
|
|
69
|
+
installLocalStorage(makeFakeStorage());
|
|
70
|
+
const store = createWebAuthStateStore();
|
|
71
|
+
const withCreds: PersistedAuthState = { ...SAMPLE, deviceId: 'dev-abc', deviceSecret: 'ds-secret-xyz' };
|
|
72
|
+
|
|
73
|
+
await store.save(withCreds);
|
|
74
|
+
const loaded = await store.load();
|
|
75
|
+
expect(loaded?.deviceId).toBe('dev-abc');
|
|
76
|
+
expect(loaded?.deviceSecret).toBe('ds-secret-xyz');
|
|
77
|
+
expect(loaded).toEqual(withCreds);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('deserializes a legacy blob with no device credentials (additive — fields absent)', async () => {
|
|
81
|
+
const storage = makeFakeStorage();
|
|
82
|
+
installLocalStorage(storage);
|
|
83
|
+
const store = createWebAuthStateStore();
|
|
84
|
+
|
|
85
|
+
storage.setItem(
|
|
86
|
+
AUTH_STATE_STORAGE_KEY,
|
|
87
|
+
JSON.stringify({ sessionId: 's-1', refreshToken: 'r-abcdefghijklmnop', userId: 'u-1' }),
|
|
88
|
+
);
|
|
89
|
+
const loaded = await store.load();
|
|
90
|
+
expect(loaded).not.toBeNull();
|
|
91
|
+
expect(loaded && 'deviceId' in loaded).toBe(false);
|
|
92
|
+
expect(loaded && 'deviceSecret' in loaded).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
68
95
|
it('clear() wipes the session but the deviceToken survives', async () => {
|
|
69
96
|
installLocalStorage(makeFakeStorage());
|
|
70
97
|
const store = createWebAuthStateStore();
|
|
@@ -53,6 +53,20 @@ describe('refreshPersistedSession — arm 1 (refresh-token rotation)', () => {
|
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it('carries the persisted deviceId + deviceSecret (phase 2c) forward across a rotation', async () => {
|
|
57
|
+
const store = createMemoryAuthStateStore();
|
|
58
|
+
await store.save({ ...STORED, deviceId: 'dev-mint', deviceSecret: 'ds-secret-orig' });
|
|
59
|
+
const { oxy } = makeOxy();
|
|
60
|
+
|
|
61
|
+
await refreshPersistedSession({ oxy, store, allowSharedKeyFallback: false });
|
|
62
|
+
|
|
63
|
+
const persisted = await store.load();
|
|
64
|
+
expect(persisted?.deviceId).toBe('dev-mint');
|
|
65
|
+
expect(persisted?.deviceSecret).toBe('ds-secret-orig');
|
|
66
|
+
// The refresh family head still rotated.
|
|
67
|
+
expect(persisted?.refreshToken).toBe('refresh-new-abcdefghij');
|
|
68
|
+
});
|
|
69
|
+
|
|
56
70
|
it('clears the store on a family-revoked (401) error', async () => {
|
|
57
71
|
const store = createMemoryAuthStateStore();
|
|
58
72
|
await store.save(STORED);
|
|
@@ -39,6 +39,26 @@ export interface PersistedAuthState {
|
|
|
39
39
|
refreshToken: string;
|
|
40
40
|
userId: string;
|
|
41
41
|
deviceToken?: string;
|
|
42
|
+
/**
|
|
43
|
+
* The stable device identifier this session is bound to (phase 2c —
|
|
44
|
+
* zero-cookie transport). Persisted alongside {@link deviceSecret} because the
|
|
45
|
+
* `POST /session/device/token` mint presents BOTH — the secret is the proof,
|
|
46
|
+
* the deviceId selects the device doc. Sourced from the lanes that carry it
|
|
47
|
+
* (password login / 2FA / QR claim / challenge verify); the cookie-bootstrap
|
|
48
|
+
* lanes (`AuthTokenBundle`) omit it and preserve any prior value. Additive: a
|
|
49
|
+
* blob without it simply never takes the mint lane and falls back to the
|
|
50
|
+
* refresh family.
|
|
51
|
+
*/
|
|
52
|
+
deviceId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The rotating device secret (phase 2c — zero-cookie transport). Possession of
|
|
55
|
+
* it mints a short access token for the device's active account via
|
|
56
|
+
* `POST /session/device/token`, replacing the cookie lane. Rotated in-use: the
|
|
57
|
+
* mint returns `nextDeviceSecret`, which the cold boot persists BEFORE planting
|
|
58
|
+
* the minted access token (multi-tab anti-loss). Additive and optional — same
|
|
59
|
+
* XSS risk profile as the already-persisted `refreshToken`.
|
|
60
|
+
*/
|
|
61
|
+
deviceSecret?: string;
|
|
42
62
|
/** Optional warm-boot access token (short-lived; see interface docs). */
|
|
43
63
|
accessToken?: string;
|
|
44
64
|
/** Optional warm-boot access-token expiry, ISO-8601. */
|
|
@@ -130,6 +150,12 @@ function deserialize(raw: string | null): PersistedAuthState | null {
|
|
|
130
150
|
if (typeof candidate.deviceToken === 'string' && candidate.deviceToken.length > 0) {
|
|
131
151
|
state.deviceToken = candidate.deviceToken;
|
|
132
152
|
}
|
|
153
|
+
if (typeof candidate.deviceId === 'string' && candidate.deviceId.length > 0) {
|
|
154
|
+
state.deviceId = candidate.deviceId;
|
|
155
|
+
}
|
|
156
|
+
if (typeof candidate.deviceSecret === 'string' && candidate.deviceSecret.length > 0) {
|
|
157
|
+
state.deviceSecret = candidate.deviceSecret;
|
|
158
|
+
}
|
|
133
159
|
if (typeof candidate.accessToken === 'string' && candidate.accessToken.length > 0) {
|
|
134
160
|
state.accessToken = candidate.accessToken;
|
|
135
161
|
}
|
package/src/session/refresh.ts
CHANGED
|
@@ -134,6 +134,15 @@ export async function refreshPersistedSession(deps: RefreshDeps): Promise<string
|
|
|
134
134
|
if (persisted.deviceToken) {
|
|
135
135
|
next.deviceToken = persisted.deviceToken;
|
|
136
136
|
}
|
|
137
|
+
// The refresh response carries no device credentials — carry the persisted
|
|
138
|
+
// deviceId/deviceSecret (phase 2c) forward so a rotation never drops the
|
|
139
|
+
// zero-cookie mint lane (mirrors the deviceToken preservation above).
|
|
140
|
+
if (persisted.deviceId) {
|
|
141
|
+
next.deviceId = persisted.deviceId;
|
|
142
|
+
}
|
|
143
|
+
if (persisted.deviceSecret) {
|
|
144
|
+
next.deviceSecret = persisted.deviceSecret;
|
|
145
|
+
}
|
|
137
146
|
await store.save(next);
|
|
138
147
|
return rotated.accessToken;
|
|
139
148
|
} catch (error) {
|