@oxyhq/core 12.7.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/cjs/mixins/OxyServices.utility.js +11 -1
- package/dist/cjs/server/auth.js +3 -0
- package/dist/cjs/server/index.js +2 -1
- package/dist/cjs/utils/oxyServiceEnvironment.js +19 -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 +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/esm/mixins/OxyServices.utility.js +11 -1
- package/dist/esm/server/auth.js +2 -0
- package/dist/esm/server/index.js +1 -1
- package/dist/esm/utils/oxyServiceEnvironment.js +16 -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 +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/dist/types/mixins/OxyServices.utility.d.ts +3 -0
- package/dist/types/server/auth.d.ts +4 -0
- package/dist/types/server/index.d.ts +2 -2
- package/dist/types/utils/oxyServiceEnvironment.d.ts +17 -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/OxyServices.utility.ts +19 -1
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +4 -2
- package/src/mixins/__tests__/commonsSignIn.test.ts +84 -1
- package/src/mixins/__tests__/serviceAuth.test.ts +65 -0
- package/src/server/auth.ts +5 -0
- package/src/server/index.ts +2 -0
- package/src/utils/__tests__/oxyServiceEnvironment.test.ts +7 -0
- package/src/utils/oxyServiceEnvironment.ts +17 -0
package/dist/esm/index.js
CHANGED
|
@@ -62,7 +62,8 @@ export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from '.
|
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
63
|
// Crypto / identity
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
|
-
export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto/keyManager.js';
|
|
65
|
+
export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, IdentityUnavailableError, } from './crypto/keyManager.js';
|
|
66
|
+
export { readIdentityMarker, updateIdentityMarker, } from './crypto/identityMarker.js';
|
|
66
67
|
export { SignatureService } from './crypto/signatureService.js';
|
|
67
68
|
export { RecoveryPhraseService } from './crypto/recoveryPhrase.js';
|
|
68
69
|
// Low-level crypto primitives (b3 Phase 0 — encrypted backup + device transfer)
|
|
@@ -293,12 +293,16 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
293
293
|
* The client must sign this challenge with their private key
|
|
294
294
|
*
|
|
295
295
|
* @param publicKey - The user's public key
|
|
296
|
+
* @param requestOptions - Optional per-call transport overrides (`retry`,
|
|
297
|
+
* `timeout`). Interactive callers omit it (defaults keep retries); the
|
|
298
|
+
* cold-boot `shared-key-signin` step passes `{ retry: false }` so a slow
|
|
299
|
+
* network cannot multiply boot latency via the inner retry loop.
|
|
296
300
|
*/
|
|
297
|
-
async requestChallenge(publicKey) {
|
|
301
|
+
async requestChallenge(publicKey, requestOptions) {
|
|
298
302
|
try {
|
|
299
303
|
return await this.makeRequest('POST', '/auth/challenge', {
|
|
300
304
|
publicKey,
|
|
301
|
-
}, { cache: false });
|
|
305
|
+
}, { cache: false, ...requestOptions });
|
|
302
306
|
}
|
|
303
307
|
catch (error) {
|
|
304
308
|
throw this.handleError(error);
|
|
@@ -313,8 +317,12 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
313
317
|
* @param timestamp - Timestamp when the signature was created
|
|
314
318
|
* @param deviceName - Optional device name
|
|
315
319
|
* @param deviceFingerprint - Optional device fingerprint
|
|
320
|
+
* @param requestOptions - Optional per-call transport overrides (`retry`,
|
|
321
|
+
* `timeout`). Interactive callers omit it (defaults keep retries); the
|
|
322
|
+
* cold-boot `shared-key-signin` step passes `{ retry: false }` so a slow
|
|
323
|
+
* network cannot multiply boot latency via the inner retry loop.
|
|
316
324
|
*/
|
|
317
|
-
async verifyChallenge(publicKey, challenge, signature, timestamp, deviceName, deviceFingerprint) {
|
|
325
|
+
async verifyChallenge(publicKey, challenge, signature, timestamp, deviceName, deviceFingerprint, requestOptions) {
|
|
318
326
|
try {
|
|
319
327
|
const res = await this.makeRequest('POST', '/auth/verify', {
|
|
320
328
|
publicKey,
|
|
@@ -323,7 +331,7 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
323
331
|
timestamp,
|
|
324
332
|
deviceName,
|
|
325
333
|
deviceFingerprint,
|
|
326
|
-
}, { cache: false });
|
|
334
|
+
}, { cache: false, ...requestOptions });
|
|
327
335
|
// Plant the freshly-minted tokens, mirroring `claimSessionByToken`.
|
|
328
336
|
// `/auth/verify` returns the first access token (and refresh token) in
|
|
329
337
|
// its body, so installing it here means callers get an authenticated
|
|
@@ -461,6 +469,13 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
461
469
|
*
|
|
462
470
|
* The cold-boot wiring that CALLS this lives in `OxyContext`
|
|
463
471
|
* (`@oxyhq/services`); this method just performs the exchange.
|
|
472
|
+
*
|
|
473
|
+
* @param opts.requestOptions - Optional per-call transport overrides
|
|
474
|
+
* (`retry`, `timeout`) forwarded to BOTH the `requestChallenge` and
|
|
475
|
+
* `verifyChallenge` round-trips. Interactive flows omit it (defaults keep
|
|
476
|
+
* retries); the cold-boot `shared-key-signin` step passes `{ retry: false }`
|
|
477
|
+
* so this network step cannot multiply boot latency via the inner retry
|
|
478
|
+
* loop. The token-refresh scheduler / 401 lane still retry later.
|
|
464
479
|
*/
|
|
465
480
|
async signInWithSharedIdentity(opts = {}) {
|
|
466
481
|
try {
|
|
@@ -474,10 +489,10 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
474
489
|
if (!sharedPublicKey) {
|
|
475
490
|
return null;
|
|
476
491
|
}
|
|
477
|
-
const { challenge } = await this.requestChallenge(sharedPublicKey);
|
|
492
|
+
const { challenge } = await this.requestChallenge(sharedPublicKey, opts.requestOptions);
|
|
478
493
|
const signed = await SignatureService.signChallengeWithSharedKey(challenge);
|
|
479
494
|
// `signed.challenge` carries the SIGNATURE (mirrors `signChallenge`).
|
|
480
|
-
return await this.verifyChallenge(signed.publicKey, challenge, signed.challenge, signed.timestamp, opts.deviceName, opts.deviceFingerprint);
|
|
495
|
+
return await this.verifyChallenge(signed.publicKey, challenge, signed.challenge, signed.timestamp, opts.deviceName, opts.deviceFingerprint, opts.requestOptions);
|
|
481
496
|
}
|
|
482
497
|
catch (error) {
|
|
483
498
|
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');
|
|
@@ -8,6 +8,7 @@ import { jwtDecode } from 'jwt-decode';
|
|
|
8
8
|
import { loadNodeCrypto } from '@oxyhq/protocol';
|
|
9
9
|
import { buildUrl } from '../utils/apiUtils.js';
|
|
10
10
|
import { logger } from '../logger/index.js';
|
|
11
|
+
import { OXY_SERVICE_ENVIRONMENTS } from '../utils/oxyServiceEnvironment.js';
|
|
11
12
|
/**
|
|
12
13
|
* Expected JWT audience for tokens issued by the Oxy auth service.
|
|
13
14
|
*/
|
|
@@ -39,6 +40,10 @@ class ServiceTokenClaimError extends Error {
|
|
|
39
40
|
this.name = 'ServiceTokenClaimError';
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
function isOxyServiceEnvironment(value) {
|
|
44
|
+
return (typeof value === 'string' &&
|
|
45
|
+
OXY_SERVICE_ENVIRONMENTS.includes(value));
|
|
46
|
+
}
|
|
42
47
|
export function OxyServicesUtilityMixin(Base) {
|
|
43
48
|
return class extends Base {
|
|
44
49
|
// TypeScript's mixin pattern requires `(...args: any[])` here — the
|
|
@@ -335,7 +340,11 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
335
340
|
// Validate required service token fields
|
|
336
341
|
const appId = decoded.appId;
|
|
337
342
|
const credentialId = decoded.credentialId;
|
|
338
|
-
|
|
343
|
+
const environment = decoded.environment;
|
|
344
|
+
if (!appId ||
|
|
345
|
+
typeof credentialId !== 'string' ||
|
|
346
|
+
credentialId.length === 0 ||
|
|
347
|
+
!isOxyServiceEnvironment(environment)) {
|
|
339
348
|
if (optional) {
|
|
340
349
|
req.userId = null;
|
|
341
350
|
req.user = null;
|
|
@@ -388,6 +397,7 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
388
397
|
appName: decoded.appName || 'unknown',
|
|
389
398
|
credentialId,
|
|
390
399
|
scopes: Array.isArray(decoded.scopes) ? decoded.scopes : [],
|
|
400
|
+
environment,
|
|
391
401
|
};
|
|
392
402
|
if (debug) {
|
|
393
403
|
logger.debug(`[oxy.auth] Service token OK app=${decoded.appName} delegateUser=${oxyUserId || '(none)'}`, {
|
package/dist/esm/server/auth.js
CHANGED
package/dist/esm/server/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* app.use(createOxyRateLimit(oxy, { store: redisStore }));
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
-
export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyUserId, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, } from './auth.js';
|
|
17
|
+
export { createOptionalOxyAuth, createOxyAuthMiddleware, getOxyUserId, getRequiredOxyUserId, isOxyAuthenticated, requireOxyAuth, OXY_SERVICE_ENVIRONMENTS, } from './auth.js';
|
|
18
18
|
export { createOxyRateLimit } from './rateLimit.js';
|
|
19
19
|
// SSRF-safe upstream fetch + URL validation (Node-only).
|
|
20
20
|
export { assertSafePublicUrl, isBlockedIp, safeFetch, SsrfRejection, UpstreamError, ALLOWED_PORTS, ALLOWED_PROTOCOLS, BLOCKED_HOSTNAMES, DEFAULT_USER_AGENT, MAX_REDIRECTS, MAX_URL_LENGTH, UPSTREAM_HEADERS_TIMEOUT_MS, } from './safeFetch.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment segregation for Oxy service-token JWTs (test/live isolation).
|
|
3
|
+
* Mirrors `ApplicationCredentialEnvironment` on the API's `ApplicationCredential`
|
|
4
|
+
* model (`packages/api/src/models/ApplicationCredential.ts`) as an INDEPENDENT
|
|
5
|
+
* literal union — `@oxyhq/core` has zero dependency on `@oxyhq/api`, so this is
|
|
6
|
+
* kept in sync by hand, not by import.
|
|
7
|
+
*
|
|
8
|
+
* Defined here (not in `server/auth.ts` or `mixins/OxyServices.utility.ts`
|
|
9
|
+
* directly) because BOTH of those files need it and neither may import from
|
|
10
|
+
* the other: `server/` types import `express` (Node-only, a peer dependency
|
|
11
|
+
* `mixins/` deliberately avoids so it stays safe to bundle into RN/browser
|
|
12
|
+
* consumers — see the "Local request/response/socket typing" comment in
|
|
13
|
+
* `OxyServices.utility.ts`). This file has zero imports, so both sides can
|
|
14
|
+
* depend on it without crossing that boundary.
|
|
15
|
+
*/
|
|
16
|
+
export const OXY_SERVICE_ENVIRONMENTS = ['development', 'staging', 'production'];
|