@oxyhq/core 12.8.0 → 12.9.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/sessionColdBoot.js +16 -3
- package/dist/cjs/crypto/identityMarker.js +255 -0
- package/dist/cjs/crypto/keyManager.js +844 -106
- package/dist/cjs/index.js +8 -4
- package/dist/cjs/mixins/OxyServices.auth.js +21 -6
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +9 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/sessionColdBoot.js +16 -3
- package/dist/esm/crypto/identityMarker.js +248 -0
- package/dist/esm/crypto/keyManager.js +843 -106
- package/dist/esm/index.js +2 -1
- package/dist/esm/mixins/OxyServices.auth.js +21 -6
- package/dist/esm/mixins/OxyServices.deviceBoot.js +9 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/boot/sessionColdBoot.d.ts +25 -0
- package/dist/types/crypto/identityMarker.d.ts +94 -0
- package/dist/types/crypto/keyManager.d.ts +212 -3
- package/dist/types/index.d.ts +4 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +27 -2
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +8 -0
- package/package.json +1 -1
- package/src/boot/__tests__/sessionColdBoot.test.ts +113 -0
- package/src/boot/sessionColdBoot.ts +42 -3
- package/src/crypto/__tests__/identityMocks.ts +125 -0
- package/src/crypto/__tests__/keyManager.atomicity.test.ts +79 -94
- package/src/crypto/__tests__/keyManager.cacheSafety.test.ts +175 -0
- package/src/crypto/__tests__/keyManager.identityStatus.test.ts +217 -0
- package/src/crypto/__tests__/keyManager.recoveryLadder.test.ts +179 -0
- package/src/crypto/__tests__/keyManager.storageMigration.test.ts +227 -0
- package/src/crypto/__tests__/keyManager.test.ts +77 -87
- package/src/crypto/identityMarker.ts +291 -0
- package/src/crypto/keyManager.ts +1026 -105
- package/src/index.ts +7 -1
- package/src/mixins/OxyServices.auth.ts +31 -7
- package/src/mixins/OxyServices.deviceBoot.ts +9 -1
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +4 -2
- package/src/mixins/__tests__/commonsSignIn.test.ts +84 -1
package/src/index.ts
CHANGED
|
@@ -245,8 +245,14 @@ export {
|
|
|
245
245
|
KeyManager,
|
|
246
246
|
IdentityAlreadyExistsError,
|
|
247
247
|
IdentityPersistError,
|
|
248
|
+
IdentityUnavailableError,
|
|
248
249
|
} from './crypto/keyManager';
|
|
249
|
-
export type { KeyPair } from './crypto/keyManager';
|
|
250
|
+
export type { KeyPair, IdentityStatus, IdentityRecoveryResult } from './crypto/keyManager';
|
|
251
|
+
export {
|
|
252
|
+
readIdentityMarker,
|
|
253
|
+
updateIdentityMarker,
|
|
254
|
+
} from './crypto/identityMarker';
|
|
255
|
+
export type { IdentityMarker } from './crypto/identityMarker';
|
|
250
256
|
export { SignatureService } from './crypto/signatureService';
|
|
251
257
|
export type { SignedMessage, AuthChallenge } from './crypto/signatureService';
|
|
252
258
|
export { RecoveryPhraseService } from './crypto/recoveryPhrase';
|
|
@@ -497,14 +497,21 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
497
497
|
/**
|
|
498
498
|
* Request an authentication challenge
|
|
499
499
|
* The client must sign this challenge with their private key
|
|
500
|
-
*
|
|
500
|
+
*
|
|
501
501
|
* @param publicKey - The user's public key
|
|
502
|
+
* @param requestOptions - Optional per-call transport overrides (`retry`,
|
|
503
|
+
* `timeout`). Interactive callers omit it (defaults keep retries); the
|
|
504
|
+
* cold-boot `shared-key-signin` step passes `{ retry: false }` so a slow
|
|
505
|
+
* network cannot multiply boot latency via the inner retry loop.
|
|
502
506
|
*/
|
|
503
|
-
async requestChallenge(
|
|
507
|
+
async requestChallenge(
|
|
508
|
+
publicKey: string,
|
|
509
|
+
requestOptions?: { retry?: boolean; timeout?: number },
|
|
510
|
+
): Promise<ChallengeResponse> {
|
|
504
511
|
try {
|
|
505
512
|
return await this.makeRequest<ChallengeResponse>('POST', '/auth/challenge', {
|
|
506
513
|
publicKey,
|
|
507
|
-
}, { cache: false });
|
|
514
|
+
}, { cache: false, ...requestOptions });
|
|
508
515
|
} catch (error) {
|
|
509
516
|
throw this.handleError(error);
|
|
510
517
|
}
|
|
@@ -519,6 +526,10 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
519
526
|
* @param timestamp - Timestamp when the signature was created
|
|
520
527
|
* @param deviceName - Optional device name
|
|
521
528
|
* @param deviceFingerprint - Optional device fingerprint
|
|
529
|
+
* @param requestOptions - Optional per-call transport overrides (`retry`,
|
|
530
|
+
* `timeout`). Interactive callers omit it (defaults keep retries); the
|
|
531
|
+
* cold-boot `shared-key-signin` step passes `{ retry: false }` so a slow
|
|
532
|
+
* network cannot multiply boot latency via the inner retry loop.
|
|
522
533
|
*/
|
|
523
534
|
async verifyChallenge(
|
|
524
535
|
publicKey: string,
|
|
@@ -526,7 +537,8 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
526
537
|
signature: string,
|
|
527
538
|
timestamp: number,
|
|
528
539
|
deviceName?: string,
|
|
529
|
-
deviceFingerprint?: string
|
|
540
|
+
deviceFingerprint?: string,
|
|
541
|
+
requestOptions?: { retry?: boolean; timeout?: number },
|
|
530
542
|
): Promise<SessionLoginResponse> {
|
|
531
543
|
try {
|
|
532
544
|
const res = await this.makeRequest<SessionLoginResponse>('POST', '/auth/verify', {
|
|
@@ -536,7 +548,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
536
548
|
timestamp,
|
|
537
549
|
deviceName,
|
|
538
550
|
deviceFingerprint,
|
|
539
|
-
}, { cache: false });
|
|
551
|
+
}, { cache: false, ...requestOptions });
|
|
540
552
|
|
|
541
553
|
// Plant the freshly-minted tokens, mirroring `claimSessionByToken`.
|
|
542
554
|
// `/auth/verify` returns the first access token (and refresh token) in
|
|
@@ -718,9 +730,20 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
718
730
|
*
|
|
719
731
|
* The cold-boot wiring that CALLS this lives in `OxyContext`
|
|
720
732
|
* (`@oxyhq/services`); this method just performs the exchange.
|
|
733
|
+
*
|
|
734
|
+
* @param opts.requestOptions - Optional per-call transport overrides
|
|
735
|
+
* (`retry`, `timeout`) forwarded to BOTH the `requestChallenge` and
|
|
736
|
+
* `verifyChallenge` round-trips. Interactive flows omit it (defaults keep
|
|
737
|
+
* retries); the cold-boot `shared-key-signin` step passes `{ retry: false }`
|
|
738
|
+
* so this network step cannot multiply boot latency via the inner retry
|
|
739
|
+
* loop. The token-refresh scheduler / 401 lane still retry later.
|
|
721
740
|
*/
|
|
722
741
|
async signInWithSharedIdentity(
|
|
723
|
-
opts: {
|
|
742
|
+
opts: {
|
|
743
|
+
deviceName?: string;
|
|
744
|
+
deviceFingerprint?: string;
|
|
745
|
+
requestOptions?: { retry?: boolean; timeout?: number };
|
|
746
|
+
} = {}
|
|
724
747
|
): Promise<SessionLoginResponse | null> {
|
|
725
748
|
try {
|
|
726
749
|
// `hasSharedIdentity()` already returns false on web (the shared
|
|
@@ -734,7 +757,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
734
757
|
return null;
|
|
735
758
|
}
|
|
736
759
|
|
|
737
|
-
const { challenge } = await this.requestChallenge(sharedPublicKey);
|
|
760
|
+
const { challenge } = await this.requestChallenge(sharedPublicKey, opts.requestOptions);
|
|
738
761
|
const signed = await SignatureService.signChallengeWithSharedKey(challenge);
|
|
739
762
|
|
|
740
763
|
// `signed.challenge` carries the SIGNATURE (mirrors `signChallenge`).
|
|
@@ -745,6 +768,7 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
745
768
|
signed.timestamp,
|
|
746
769
|
opts.deviceName,
|
|
747
770
|
opts.deviceFingerprint,
|
|
771
|
+
opts.requestOptions,
|
|
748
772
|
);
|
|
749
773
|
} catch (error) {
|
|
750
774
|
throw this.handleError(error);
|
|
@@ -39,6 +39,14 @@ export function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Bas
|
|
|
39
39
|
* `no_active_session`) to decide whether to drop the secret and fall back or
|
|
40
40
|
* resolve signed-out.
|
|
41
41
|
*
|
|
42
|
+
* `retry: false`: the mint is a single logical attempt. The proactive
|
|
43
|
+
* token-refresh scheduler and the reactive 401 lane already own backoff and
|
|
44
|
+
* re-arm, so `HttpService`'s inner retry loop here would only multiply the
|
|
45
|
+
* mint's latency on a slow/black-hole network (3 retries × 5s timeout ≈ 20s
|
|
46
|
+
* per lane) with no correctness benefit — it is the dominant term in the cold
|
|
47
|
+
* boot's worst-case time-to-route. A transient failure surfaces once and the
|
|
48
|
+
* scheduler/401 path retries it later.
|
|
49
|
+
*
|
|
42
50
|
* @throws if the response does not match {@link deviceTokenMintResponseSchema}.
|
|
43
51
|
*/
|
|
44
52
|
async mintFromDeviceSecret(
|
|
@@ -50,7 +58,7 @@ export function OxyServicesDeviceBootMixin<T extends typeof OxyServicesBase>(Bas
|
|
|
50
58
|
'POST',
|
|
51
59
|
'/session/device/token',
|
|
52
60
|
{ deviceId, deviceSecret },
|
|
53
|
-
{ cache: false, skipAuth: true },
|
|
61
|
+
{ cache: false, skipAuth: true, retry: false },
|
|
54
62
|
);
|
|
55
63
|
const parsed = safeParseContract(deviceTokenMintResponseSchema, res);
|
|
56
64
|
if (!parsed) {
|
|
@@ -31,15 +31,17 @@ describe('OxyServices.deviceBoot', () => {
|
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
it('POSTs deviceId + deviceSecret with skipAuth (no bearer, no cache) and returns the validated mint', async () => {
|
|
34
|
+
it('POSTs deviceId + deviceSecret with skipAuth + retry:false (no bearer, no cache, single attempt) and returns the validated mint', async () => {
|
|
35
35
|
makeRequest.mockResolvedValueOnce(MINT);
|
|
36
36
|
const result = await oxy.mintFromDeviceSecret('dev-1', 'ds-current-secret');
|
|
37
37
|
expect(result).toEqual(MINT);
|
|
38
|
+
// `retry: false` — the scheduler/401 lane own backoff; HttpService's inner
|
|
39
|
+
// retry here would only multiply cold-boot latency on a slow network.
|
|
38
40
|
expect(makeRequest).toHaveBeenCalledWith(
|
|
39
41
|
'POST',
|
|
40
42
|
'/session/device/token',
|
|
41
43
|
{ deviceId: 'dev-1', deviceSecret: 'ds-current-secret' },
|
|
42
|
-
{ cache: false, skipAuth: true },
|
|
44
|
+
{ cache: false, skipAuth: true, retry: false },
|
|
43
45
|
);
|
|
44
46
|
});
|
|
45
47
|
|
|
@@ -242,6 +242,51 @@ describe('OxyServices — "Sign in with Oxy" handoff', () => {
|
|
|
242
242
|
});
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
+
describe('requestChallenge / verifyChallenge — requestOptions spread into makeRequest', () => {
|
|
246
|
+
it('requestChallenge omits transport overrides by default (retries ON) and spreads them when given', async () => {
|
|
247
|
+
makeRequestSpy.mockResolvedValue(challengeFixture);
|
|
248
|
+
|
|
249
|
+
await oxy.requestChallenge('pub-x');
|
|
250
|
+
expect(makeRequestSpy).toHaveBeenLastCalledWith(
|
|
251
|
+
'POST',
|
|
252
|
+
'/auth/challenge',
|
|
253
|
+
{ publicKey: 'pub-x' },
|
|
254
|
+
{ cache: false },
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await oxy.requestChallenge('pub-x', { retry: false });
|
|
258
|
+
expect(makeRequestSpy).toHaveBeenLastCalledWith(
|
|
259
|
+
'POST',
|
|
260
|
+
'/auth/challenge',
|
|
261
|
+
{ publicKey: 'pub-x' },
|
|
262
|
+
{ cache: false, retry: false },
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('verifyChallenge spreads requestOptions (retry + timeout) into makeRequest', async () => {
|
|
267
|
+
makeRequestSpy.mockResolvedValue(sessionFixture);
|
|
268
|
+
|
|
269
|
+
await oxy.verifyChallenge('pub-x', 'chal', 'sig', 123, 'dev', 'fp', {
|
|
270
|
+
retry: false,
|
|
271
|
+
timeout: 9000,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(makeRequestSpy).toHaveBeenLastCalledWith(
|
|
275
|
+
'POST',
|
|
276
|
+
'/auth/verify',
|
|
277
|
+
{
|
|
278
|
+
publicKey: 'pub-x',
|
|
279
|
+
challenge: 'chal',
|
|
280
|
+
signature: 'sig',
|
|
281
|
+
timestamp: 123,
|
|
282
|
+
deviceName: 'dev',
|
|
283
|
+
deviceFingerprint: 'fp',
|
|
284
|
+
},
|
|
285
|
+
{ cache: false, retry: false, timeout: 9000 },
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
245
290
|
describe('signInWithSharedIdentity (Mechanism A — same-device SSO)', () => {
|
|
246
291
|
it('mints a session from the shared key when one exists (native)', async () => {
|
|
247
292
|
jest.spyOn(KeyManager, 'hasSharedIdentity').mockResolvedValue(true);
|
|
@@ -263,7 +308,9 @@ describe('OxyServices — "Sign in with Oxy" handoff', () => {
|
|
|
263
308
|
deviceFingerprint: 'fp-1',
|
|
264
309
|
});
|
|
265
310
|
|
|
266
|
-
|
|
311
|
+
// No requestOptions passed → both round-trips get `undefined` (defaults:
|
|
312
|
+
// retries ON), preserving interactive behaviour.
|
|
313
|
+
expect(requestChallengeSpy).toHaveBeenCalledWith('shared-pub', undefined);
|
|
267
314
|
expect(verifyChallengeSpy).toHaveBeenCalledWith(
|
|
268
315
|
'shared-pub',
|
|
269
316
|
'chal-shared',
|
|
@@ -271,6 +318,42 @@ describe('OxyServices — "Sign in with Oxy" handoff', () => {
|
|
|
271
318
|
1700000000456,
|
|
272
319
|
'iPad',
|
|
273
320
|
'fp-1',
|
|
321
|
+
undefined,
|
|
322
|
+
);
|
|
323
|
+
expect(result).toEqual(sessionFixture);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('threads requestOptions into BOTH the challenge and verify round-trips (cold-boot retry:false)', async () => {
|
|
327
|
+
jest.spyOn(KeyManager, 'hasSharedIdentity').mockResolvedValue(true);
|
|
328
|
+
jest.spyOn(KeyManager, 'getSharedPublicKey').mockResolvedValue('shared-pub');
|
|
329
|
+
const requestChallengeSpy = jest
|
|
330
|
+
.spyOn(oxy, 'requestChallenge')
|
|
331
|
+
.mockResolvedValue({ challenge: 'chal-shared', expiresAt: '2026-06-26T00:05:00.000Z' });
|
|
332
|
+
jest.spyOn(SignatureService, 'signChallengeWithSharedKey').mockResolvedValue({
|
|
333
|
+
challenge: 'sig-shared',
|
|
334
|
+
publicKey: 'shared-pub',
|
|
335
|
+
timestamp: 1700000000456,
|
|
336
|
+
});
|
|
337
|
+
const verifyChallengeSpy = jest
|
|
338
|
+
.spyOn(oxy, 'verifyChallenge')
|
|
339
|
+
.mockResolvedValue(sessionFixture);
|
|
340
|
+
|
|
341
|
+
const result = await oxy.signInWithSharedIdentity({
|
|
342
|
+
requestOptions: { retry: false },
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// The SAME requestOptions object is forwarded to both calls — this is how
|
|
346
|
+
// the cold-boot `shared-key-signin` step keeps its two round-trips as
|
|
347
|
+
// single attempts without changing interactive defaults.
|
|
348
|
+
expect(requestChallengeSpy).toHaveBeenCalledWith('shared-pub', { retry: false });
|
|
349
|
+
expect(verifyChallengeSpy).toHaveBeenCalledWith(
|
|
350
|
+
'shared-pub',
|
|
351
|
+
'chal-shared',
|
|
352
|
+
'sig-shared',
|
|
353
|
+
1700000000456,
|
|
354
|
+
undefined,
|
|
355
|
+
undefined,
|
|
356
|
+
{ retry: false },
|
|
274
357
|
);
|
|
275
358
|
expect(result).toEqual(sessionFixture);
|
|
276
359
|
});
|