@oxyhq/core 12.8.0 → 12.10.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 +933 -106
- package/dist/cjs/index.js +11 -4
- package/dist/cjs/mixins/OxyServices.auth.js +25 -7
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +9 -1
- package/dist/cjs/mixins/OxyServices.identityBackup.js +11 -0
- package/dist/cjs/mixins/OxyServices.user.js +4 -0
- package/dist/cjs/utils/commonsApproval.js +31 -0
- 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 +932 -106
- package/dist/esm/index.js +4 -2
- package/dist/esm/mixins/OxyServices.auth.js +22 -6
- package/dist/esm/mixins/OxyServices.deviceBoot.js +9 -1
- package/dist/esm/mixins/OxyServices.identityBackup.js +11 -0
- package/dist/esm/mixins/OxyServices.user.js +4 -0
- package/dist/esm/utils/commonsApproval.js +27 -0
- 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 +245 -3
- package/dist/types/index.d.ts +6 -3
- package/dist/types/mixins/OxyServices.auth.d.ts +31 -6
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +8 -0
- package/dist/types/utils/commonsApproval.d.ts +13 -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.recoveryMnemonic.test.ts +138 -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 +1125 -105
- package/src/index.ts +14 -2
- package/src/mixins/OxyServices.auth.ts +40 -12
- package/src/mixins/OxyServices.deviceBoot.ts +9 -1
- package/src/mixins/OxyServices.identityBackup.ts +13 -0
- package/src/mixins/OxyServices.user.ts +4 -0
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +4 -2
- package/src/mixins/__tests__/commonsSignIn.test.ts +84 -1
- package/src/mixins/__tests__/identityBackup.test.ts +57 -0
- package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +6 -0
- package/src/utils/__tests__/commonsApproval.test.ts +60 -0
- package/src/utils/commonsApproval.ts +39 -0
package/dist/esm/index.js
CHANGED
|
@@ -26,7 +26,8 @@ export { OXY_CLOUD_URL, oxyClient } from './OxyServices.js';
|
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
// Authentication
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
|
-
export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth.js';
|
|
29
|
+
export { ServiceCredentialMismatchError, } from './mixins/OxyServices.auth.js';
|
|
30
|
+
export { getCommonsApprovalBlockingReason, parseCommonsApprovalExpiresAt, } from './utils/commonsApproval.js';
|
|
30
31
|
export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData.js';
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// User identity and handles
|
|
@@ -62,7 +63,8 @@ export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from '.
|
|
|
62
63
|
// ---------------------------------------------------------------------------
|
|
63
64
|
// Crypto / identity
|
|
64
65
|
// ---------------------------------------------------------------------------
|
|
65
|
-
export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto/keyManager.js';
|
|
66
|
+
export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, IdentityUnavailableError, } from './crypto/keyManager.js';
|
|
67
|
+
export { readIdentityMarker, updateIdentityMarker, } from './crypto/identityMarker.js';
|
|
66
68
|
export { SignatureService } from './crypto/signatureService.js';
|
|
67
69
|
export { RecoveryPhraseService } from './crypto/recoveryPhrase.js';
|
|
68
70
|
// Low-level crypto primitives (b3 Phase 0 — encrypted backup + device transfer)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loginResultSchema, safeParseContract } from '@oxyhq/contracts';
|
|
2
|
+
export { getCommonsApprovalBlockingReason, parseCommonsApprovalExpiresAt, } from '../utils/commonsApproval.js';
|
|
2
3
|
import { OxyAuthenticationError } from '../OxyServices.errors.js';
|
|
3
4
|
import { KeyManager } from '../crypto/keyManager.js';
|
|
4
5
|
import { SignatureService } from '../crypto/signatureService.js';
|
|
@@ -293,12 +294,16 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
293
294
|
* The client must sign this challenge with their private key
|
|
294
295
|
*
|
|
295
296
|
* @param publicKey - The user's public key
|
|
297
|
+
* @param requestOptions - Optional per-call transport overrides (`retry`,
|
|
298
|
+
* `timeout`). Interactive callers omit it (defaults keep retries); the
|
|
299
|
+
* cold-boot `shared-key-signin` step passes `{ retry: false }` so a slow
|
|
300
|
+
* network cannot multiply boot latency via the inner retry loop.
|
|
296
301
|
*/
|
|
297
|
-
async requestChallenge(publicKey) {
|
|
302
|
+
async requestChallenge(publicKey, requestOptions) {
|
|
298
303
|
try {
|
|
299
304
|
return await this.makeRequest('POST', '/auth/challenge', {
|
|
300
305
|
publicKey,
|
|
301
|
-
}, { cache: false });
|
|
306
|
+
}, { cache: false, ...requestOptions });
|
|
302
307
|
}
|
|
303
308
|
catch (error) {
|
|
304
309
|
throw this.handleError(error);
|
|
@@ -313,8 +318,12 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
313
318
|
* @param timestamp - Timestamp when the signature was created
|
|
314
319
|
* @param deviceName - Optional device name
|
|
315
320
|
* @param deviceFingerprint - Optional device fingerprint
|
|
321
|
+
* @param requestOptions - Optional per-call transport overrides (`retry`,
|
|
322
|
+
* `timeout`). Interactive callers omit it (defaults keep retries); the
|
|
323
|
+
* cold-boot `shared-key-signin` step passes `{ retry: false }` so a slow
|
|
324
|
+
* network cannot multiply boot latency via the inner retry loop.
|
|
316
325
|
*/
|
|
317
|
-
async verifyChallenge(publicKey, challenge, signature, timestamp, deviceName, deviceFingerprint) {
|
|
326
|
+
async verifyChallenge(publicKey, challenge, signature, timestamp, deviceName, deviceFingerprint, requestOptions) {
|
|
318
327
|
try {
|
|
319
328
|
const res = await this.makeRequest('POST', '/auth/verify', {
|
|
320
329
|
publicKey,
|
|
@@ -323,7 +332,7 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
323
332
|
timestamp,
|
|
324
333
|
deviceName,
|
|
325
334
|
deviceFingerprint,
|
|
326
|
-
}, { cache: false });
|
|
335
|
+
}, { cache: false, ...requestOptions });
|
|
327
336
|
// Plant the freshly-minted tokens, mirroring `claimSessionByToken`.
|
|
328
337
|
// `/auth/verify` returns the first access token (and refresh token) in
|
|
329
338
|
// its body, so installing it here means callers get an authenticated
|
|
@@ -461,6 +470,13 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
461
470
|
*
|
|
462
471
|
* The cold-boot wiring that CALLS this lives in `OxyContext`
|
|
463
472
|
* (`@oxyhq/services`); this method just performs the exchange.
|
|
473
|
+
*
|
|
474
|
+
* @param opts.requestOptions - Optional per-call transport overrides
|
|
475
|
+
* (`retry`, `timeout`) forwarded to BOTH the `requestChallenge` and
|
|
476
|
+
* `verifyChallenge` round-trips. Interactive flows omit it (defaults keep
|
|
477
|
+
* retries); the cold-boot `shared-key-signin` step passes `{ retry: false }`
|
|
478
|
+
* so this network step cannot multiply boot latency via the inner retry
|
|
479
|
+
* loop. The token-refresh scheduler / 401 lane still retry later.
|
|
464
480
|
*/
|
|
465
481
|
async signInWithSharedIdentity(opts = {}) {
|
|
466
482
|
try {
|
|
@@ -474,10 +490,10 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
474
490
|
if (!sharedPublicKey) {
|
|
475
491
|
return null;
|
|
476
492
|
}
|
|
477
|
-
const { challenge } = await this.requestChallenge(sharedPublicKey);
|
|
493
|
+
const { challenge } = await this.requestChallenge(sharedPublicKey, opts.requestOptions);
|
|
478
494
|
const signed = await SignatureService.signChallengeWithSharedKey(challenge);
|
|
479
495
|
// `signed.challenge` carries the SIGNATURE (mirrors `signChallenge`).
|
|
480
|
-
return await this.verifyChallenge(signed.publicKey, challenge, signed.challenge, signed.timestamp, opts.deviceName, opts.deviceFingerprint);
|
|
496
|
+
return await this.verifyChallenge(signed.publicKey, challenge, signed.challenge, signed.timestamp, opts.deviceName, opts.deviceFingerprint, opts.requestOptions);
|
|
481
497
|
}
|
|
482
498
|
catch (error) {
|
|
483
499
|
throw this.handleError(error);
|
|
@@ -29,11 +29,19 @@ export function OxyServicesDeviceBootMixin(Base) {
|
|
|
29
29
|
* `no_active_session`) to decide whether to drop the secret and fall back or
|
|
30
30
|
* resolve signed-out.
|
|
31
31
|
*
|
|
32
|
+
* `retry: false`: the mint is a single logical attempt. The proactive
|
|
33
|
+
* token-refresh scheduler and the reactive 401 lane already own backoff and
|
|
34
|
+
* re-arm, so `HttpService`'s inner retry loop here would only multiply the
|
|
35
|
+
* mint's latency on a slow/black-hole network (3 retries × 5s timeout ≈ 20s
|
|
36
|
+
* per lane) with no correctness benefit — it is the dominant term in the cold
|
|
37
|
+
* boot's worst-case time-to-route. A transient failure surfaces once and the
|
|
38
|
+
* scheduler/401 path retries it later.
|
|
39
|
+
*
|
|
32
40
|
* @throws if the response does not match {@link deviceTokenMintResponseSchema}.
|
|
33
41
|
*/
|
|
34
42
|
async mintFromDeviceSecret(deviceId, deviceSecret) {
|
|
35
43
|
try {
|
|
36
|
-
const res = await this.makeRequest('POST', '/session/device/token', { deviceId, deviceSecret }, { cache: false, skipAuth: true });
|
|
44
|
+
const res = await this.makeRequest('POST', '/session/device/token', { deviceId, deviceSecret }, { cache: false, skipAuth: true, retry: false });
|
|
37
45
|
const parsed = safeParseContract(deviceTokenMintResponseSchema, res);
|
|
38
46
|
if (!parsed) {
|
|
39
47
|
throw new Error('session/device/token returned an unexpected response shape');
|
|
@@ -138,6 +138,17 @@ export function OxyServicesIdentityBackupMixin(Base) {
|
|
|
138
138
|
const aad = buildBackupAad(envelope.version, envelope.publicKeyHint);
|
|
139
139
|
const plaintext = decryptAead(backupKey, fromHex(envelope.nonce), fromHex(envelope.ciphertext), aad);
|
|
140
140
|
const payload = JSON.parse(new TextDecoder().decode(plaintext));
|
|
141
|
+
if (!payload.privateKey || !payload.publicKey) {
|
|
142
|
+
throw new Error('Backup payload is missing key material');
|
|
143
|
+
}
|
|
144
|
+
const derivedFromPhrase = await RecoveryPhraseService.derivePublicKeyFromPhrase(phrase);
|
|
145
|
+
const derivedFromPrivate = KeyManager.derivePublicKey(payload.privateKey);
|
|
146
|
+
const phrasePk = derivedFromPhrase.toLowerCase();
|
|
147
|
+
const payloadPk = payload.publicKey.toLowerCase();
|
|
148
|
+
const privatePk = derivedFromPrivate.toLowerCase();
|
|
149
|
+
if (phrasePk !== payloadPk || privatePk !== payloadPk) {
|
|
150
|
+
throw new Error('Backup payload does not match the recovery phrase');
|
|
151
|
+
}
|
|
141
152
|
// Persist the recovered key. Native-only; refuses to clobber a different
|
|
142
153
|
// identity unless overwrite — the IdentityAlreadyExistsError propagates.
|
|
143
154
|
return await KeyManager.importKeyPair(payload.privateKey, {
|
|
@@ -420,6 +420,10 @@ export function OxyServicesUserMixin(Base) {
|
|
|
420
420
|
const result = await this.makeRequest('PATCH', `/privacy/${id}/privacy`, settings, {
|
|
421
421
|
cache: false,
|
|
422
422
|
});
|
|
423
|
+
this.clearCacheByPrefix('GET:/session/user/');
|
|
424
|
+
this.clearCacheByPrefix('GET:/users/me');
|
|
425
|
+
this.clearCacheByPrefix('GET:/profiles/username/');
|
|
426
|
+
this.clearCacheEntry(`GET:/users/${id}`);
|
|
423
427
|
this.clearCacheEntry(`GET:/privacy/${id}/privacy`);
|
|
424
428
|
return result;
|
|
425
429
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a user-facing blocking reason when an approval payload must not be
|
|
3
|
+
* shown as actionable, or `null` when the request is still pending and valid.
|
|
4
|
+
*/
|
|
5
|
+
export function getCommonsApprovalBlockingReason(info) {
|
|
6
|
+
if (!info.application?.id) {
|
|
7
|
+
return 'The requesting application could not be resolved.';
|
|
8
|
+
}
|
|
9
|
+
if (info.status !== 'pending') {
|
|
10
|
+
return 'This sign-in request is invalid, already used, or expired.';
|
|
11
|
+
}
|
|
12
|
+
const expiresAtMs = parseCommonsApprovalExpiresAt(info.expiresAt);
|
|
13
|
+
if (expiresAtMs !== null && expiresAtMs < Date.now()) {
|
|
14
|
+
return 'This sign-in request has expired. Ask for a new QR code.';
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
/** Normalize API `expiresAt` (number or ISO string) to epoch ms. */
|
|
19
|
+
export function parseCommonsApprovalExpiresAt(expiresAt) {
|
|
20
|
+
if (typeof expiresAt === 'number' && Number.isFinite(expiresAt))
|
|
21
|
+
return expiresAt;
|
|
22
|
+
if (typeof expiresAt === 'string') {
|
|
23
|
+
const ms = Date.parse(expiresAt);
|
|
24
|
+
return Number.isFinite(ms) ? ms : null;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|