@oxyhq/core 3.10.1 → 3.11.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/AuthManager.js +9 -2
- package/dist/cjs/HttpService.js +27 -9
- package/dist/cjs/OxyServices.base.js +3 -2
- package/dist/cjs/crypto/canonicalJson.js +107 -0
- package/dist/cjs/crypto/keyManager.js +67 -8
- package/dist/cjs/crypto/signatureService.js +103 -0
- package/dist/cjs/index.js +15 -4
- package/dist/cjs/mixins/OxyServices.assets.js +16 -1
- package/dist/cjs/mixins/OxyServices.auth.js +190 -1
- package/dist/cjs/mixins/OxyServices.identity.js +291 -0
- package/dist/cjs/mixins/OxyServices.sso.js +28 -1
- package/dist/cjs/mixins/OxyServices.user.js +1 -0
- package/dist/cjs/mixins/index.js +3 -0
- package/dist/cjs/server/cors.js +20 -21
- package/dist/cjs/server/rateLimit.js +32 -8
- package/dist/cjs/utils/ssoReturn.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/AuthManager.js +9 -2
- package/dist/esm/HttpService.js +27 -9
- package/dist/esm/OxyServices.base.js +3 -2
- package/dist/esm/crypto/canonicalJson.js +104 -0
- package/dist/esm/crypto/keyManager.js +67 -8
- package/dist/esm/crypto/signatureService.js +102 -0
- package/dist/esm/index.js +9 -1
- package/dist/esm/mixins/OxyServices.assets.js +16 -1
- package/dist/esm/mixins/OxyServices.auth.js +190 -1
- package/dist/esm/mixins/OxyServices.identity.js +287 -0
- package/dist/esm/mixins/OxyServices.sso.js +28 -1
- package/dist/esm/mixins/OxyServices.user.js +1 -0
- package/dist/esm/mixins/index.js +3 -0
- package/dist/esm/server/cors.js +20 -21
- package/dist/esm/server/rateLimit.js +32 -8
- package/dist/esm/utils/ssoReturn.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +3 -0
- package/dist/types/OxyServices.d.ts +2 -2
- package/dist/types/crypto/canonicalJson.d.ts +44 -0
- package/dist/types/crypto/keyManager.d.ts +7 -0
- package/dist/types/crypto/signatureService.d.ts +61 -0
- package/dist/types/index.d.ts +6 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +136 -0
- package/dist/types/mixins/OxyServices.identity.d.ts +249 -0
- package/dist/types/mixins/OxyServices.sso.d.ts +4 -1
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/models/interfaces.d.ts +3 -0
- package/dist/types/server/cors.d.ts +5 -5
- package/dist/types/utils/ssoReturn.d.ts +1 -1
- package/package.json +2 -2
- package/src/AuthManager.ts +8 -2
- package/src/HttpService.ts +36 -8
- package/src/OxyServices.base.ts +3 -2
- package/src/OxyServices.ts +1 -1
- package/src/__tests__/authManager.security.test.ts +31 -0
- package/src/__tests__/httpServiceCsrf.test.ts +75 -0
- package/src/crypto/__tests__/canonicalJson.test.ts +116 -0
- package/src/crypto/__tests__/keyManager.atomicity.test.ts +41 -2
- package/src/crypto/__tests__/signChallengeShared.test.ts +64 -0
- package/src/crypto/__tests__/signedRecord.test.ts +125 -0
- package/src/crypto/canonicalJson.ts +120 -0
- package/src/crypto/keyManager.ts +62 -12
- package/src/crypto/signatureService.ts +126 -0
- package/src/index.ts +27 -2
- package/src/mixins/OxyServices.assets.ts +16 -1
- package/src/mixins/OxyServices.auth.ts +309 -1
- package/src/mixins/OxyServices.identity.ts +445 -0
- package/src/mixins/OxyServices.sso.ts +30 -1
- package/src/mixins/OxyServices.user.ts +1 -0
- package/src/mixins/__tests__/OxyServices.identity.test.ts +364 -0
- package/src/mixins/__tests__/assetCredentials.test.ts +47 -0
- package/src/mixins/__tests__/commonsSignIn.test.ts +277 -0
- package/src/mixins/__tests__/serviceAuth.test.ts +19 -0
- package/src/mixins/__tests__/sso.test.ts +31 -0
- package/src/mixins/index.ts +4 -0
- package/src/models/interfaces.ts +3 -0
- package/src/server/__tests__/cors.test.ts +5 -1
- package/src/server/__tests__/rateLimit.test.ts +116 -0
- package/src/server/cors.ts +25 -20
- package/src/server/rateLimit.ts +39 -8
- package/src/utils/__tests__/consumeSsoReturn.test.ts +1 -1
- package/src/utils/__tests__/ssoReturn.test.ts +1 -1
- package/src/utils/ssoReturn.ts +2 -2
package/dist/cjs/AuthManager.js
CHANGED
|
@@ -340,8 +340,15 @@ class AuthManager {
|
|
|
340
340
|
* Get default storage based on environment.
|
|
341
341
|
*/
|
|
342
342
|
getDefaultStorage() {
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
try {
|
|
344
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
345
|
+
return new LocalStorageAdapter();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// Accessing window.localStorage can throw in opaque-origin/sandboxed
|
|
350
|
+
// browser contexts or when storage is disabled. Fall back to memory so
|
|
351
|
+
// AuthManager construction remains safe during provider render.
|
|
345
352
|
}
|
|
346
353
|
return new MemoryStorage();
|
|
347
354
|
}
|
package/dist/cjs/HttpService.js
CHANGED
|
@@ -351,13 +351,13 @@ class HttpService {
|
|
|
351
351
|
// use fetch on every platform.
|
|
352
352
|
const useXhrForUpload = isFormData && (0, platform_1.isReactNative)() && typeof XMLHttpRequest !== 'undefined';
|
|
353
353
|
const response = useXhrForUpload
|
|
354
|
-
? await this.uploadViaXHR(fullUrl, method, headers, bodyValue, controller.signal, timeout)
|
|
354
|
+
? await this.uploadViaXHR(fullUrl, method, headers, bodyValue, controller.signal, timeout, this.shouldSendCredentials(fullUrl))
|
|
355
355
|
: await fetch(fullUrl, {
|
|
356
356
|
method,
|
|
357
357
|
headers,
|
|
358
358
|
body: bodyValue,
|
|
359
359
|
signal: controller.signal,
|
|
360
|
-
credentials:
|
|
360
|
+
credentials: this.getCredentialsMode(fullUrl),
|
|
361
361
|
});
|
|
362
362
|
if (timeoutId)
|
|
363
363
|
clearTimeout(timeoutId);
|
|
@@ -383,7 +383,7 @@ class HttpService {
|
|
|
383
383
|
const errBody = await clonedResponse.json();
|
|
384
384
|
if (errBody?.code === 'CSRF_TOKEN_INVALID' || errBody?.code === 'CSRF_TOKEN_MISSING') {
|
|
385
385
|
this.tokenStore.clearCsrfToken();
|
|
386
|
-
return this.request({ ...config, _isCsrfRetry: true, retry: false });
|
|
386
|
+
return this.request({ ...config, _isCsrfRetry: true, retry: false, deduplicate: false });
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
catch {
|
|
@@ -417,7 +417,10 @@ class HttpService {
|
|
|
417
417
|
// Handle different response types (optimized - read response once)
|
|
418
418
|
const contentType = response.headers.get('content-type');
|
|
419
419
|
let responseData;
|
|
420
|
-
if (
|
|
420
|
+
if (config.responseType === 'blob') {
|
|
421
|
+
responseData = await response.blob();
|
|
422
|
+
}
|
|
423
|
+
else if (contentType && contentType.includes('application/json')) {
|
|
421
424
|
// Use response.json() directly for better performance
|
|
422
425
|
try {
|
|
423
426
|
responseData = await response.json();
|
|
@@ -528,13 +531,14 @@ class HttpService {
|
|
|
528
531
|
* (status checks, 401/403 retries, JSON/blob/text parsing) is identical
|
|
529
532
|
* to the fetch path.
|
|
530
533
|
*/
|
|
531
|
-
uploadViaXHR(url, method, headers, body, abortSignal, timeout) {
|
|
534
|
+
uploadViaXHR(url, method, headers, body, abortSignal, timeout, withCredentials) {
|
|
532
535
|
return new Promise((resolve, reject) => {
|
|
533
536
|
const xhr = new XMLHttpRequest();
|
|
534
537
|
xhr.open(method, url, true);
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
+
// Only send ambient cookies to the configured API origin. Absolute
|
|
539
|
+
// caller-supplied URLs can target arbitrary origins, so they must not
|
|
540
|
+
// receive credential-bearing requests by default.
|
|
541
|
+
xhr.withCredentials = withCredentials;
|
|
538
542
|
// Forward headers but skip Content-Type — XHR sets the multipart
|
|
539
543
|
// boundary automatically and overriding it breaks the upload.
|
|
540
544
|
for (const [key, value] of Object.entries(headers)) {
|
|
@@ -687,6 +691,17 @@ class HttpService {
|
|
|
687
691
|
const queryString = searchParams.toString();
|
|
688
692
|
return queryString ? `${base}${base.includes('?') ? '&' : '?'}${queryString}` : base;
|
|
689
693
|
}
|
|
694
|
+
getCredentialsMode(url) {
|
|
695
|
+
return this.shouldSendCredentials(url) ? 'include' : 'omit';
|
|
696
|
+
}
|
|
697
|
+
shouldSendCredentials(url) {
|
|
698
|
+
try {
|
|
699
|
+
return new URL(url).origin === new URL(this.baseURL).origin;
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
690
705
|
/**
|
|
691
706
|
* Fetch CSRF token from server (with deduplication)
|
|
692
707
|
* Required for state-changing requests (POST, PUT, PATCH, DELETE)
|
|
@@ -721,8 +736,11 @@ class HttpService {
|
|
|
721
736
|
this.logger.debug('CSRF fetch response:', response.status, response.ok);
|
|
722
737
|
if (response.ok) {
|
|
723
738
|
const data = await response.json();
|
|
724
|
-
this.logger.debug('CSRF response data:', data);
|
|
725
739
|
const token = data.csrfToken || null;
|
|
740
|
+
this.logger.debug('CSRF response data:', {
|
|
741
|
+
hasCsrfToken: typeof token === 'string' && token.length > 0,
|
|
742
|
+
csrfTokenLength: token?.length,
|
|
743
|
+
});
|
|
726
744
|
this.tokenStore.setCsrfToken(token);
|
|
727
745
|
this.logger.debug('CSRF token fetched');
|
|
728
746
|
return token;
|
|
@@ -232,8 +232,9 @@ class OxyServicesBase {
|
|
|
232
232
|
}
|
|
233
233
|
try {
|
|
234
234
|
const decoded = (0, jwt_decode_1.jwtDecode)(accessToken);
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
const userId = decoded.userId || decoded.id || null;
|
|
236
|
+
this._cachedUserId = userId;
|
|
237
|
+
return userId;
|
|
237
238
|
}
|
|
238
239
|
catch {
|
|
239
240
|
this._cachedUserId = null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical JSON (RFC 8785 / JCS-style) serialization.
|
|
4
|
+
*
|
|
5
|
+
* `canonicalize(value)` produces a deterministic string for any JSON-compatible
|
|
6
|
+
* value so that a client which SIGNS a record and a server which VERIFIES it
|
|
7
|
+
* agree byte-for-byte on the signing input — regardless of the order in which
|
|
8
|
+
* object keys happen to be written, how the value was deserialized, or which
|
|
9
|
+
* runtime built it.
|
|
10
|
+
*
|
|
11
|
+
* This is the load-bearing primitive for the self-sovereign identity layer's
|
|
12
|
+
* signed records (`SignatureService.signRecord` + the API's record-verify path):
|
|
13
|
+
* both sides import THIS function from `@oxyhq/core`, so cross-implementation
|
|
14
|
+
* number/string formatting differences cannot cause a verify mismatch.
|
|
15
|
+
*
|
|
16
|
+
* Rules (the JSON Canonicalization Scheme subset we need):
|
|
17
|
+
* - Objects: keys are sorted (ascending, by UTF-16 code unit — the default
|
|
18
|
+
* `Array.prototype.sort` order) and serialized recursively. Properties whose
|
|
19
|
+
* value is `undefined`, a function, or a symbol are OMITTED (matching
|
|
20
|
+
* `JSON.stringify` object semantics).
|
|
21
|
+
* - Arrays: element order is PRESERVED; `undefined`/function/symbol elements
|
|
22
|
+
* serialize to `null` (matching `JSON.stringify` array semantics).
|
|
23
|
+
* - `null`, booleans, strings, and finite numbers serialize via the standard
|
|
24
|
+
* JSON representation.
|
|
25
|
+
* - Values exposing a `toJSON()` method (e.g. `Date`) are replaced by its
|
|
26
|
+
* result first, then serialized — so a `Date` and its ISO-string equivalent
|
|
27
|
+
* canonicalize identically (the wire always carries the string form).
|
|
28
|
+
* - Non-finite numbers (`NaN`, `Infinity`) and `bigint` are not part of the
|
|
29
|
+
* JSON data model and throw, rather than silently producing `null`.
|
|
30
|
+
*
|
|
31
|
+
* Platform-agnostic — zero dependencies, no `require()`, no react/react-native/
|
|
32
|
+
* expo. Safe in the dual CJS + ESM build.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.canonicalize = canonicalize;
|
|
36
|
+
function hasToJSON(value) {
|
|
37
|
+
return typeof value.toJSON === 'function';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Serialize a single value into its canonical JSON fragment. Recursive; called
|
|
41
|
+
* on each nested member. Object keys are sorted at every level.
|
|
42
|
+
*/
|
|
43
|
+
function serialize(value) {
|
|
44
|
+
if (value === null) {
|
|
45
|
+
return 'null';
|
|
46
|
+
}
|
|
47
|
+
const valueType = typeof value;
|
|
48
|
+
if (valueType === 'number') {
|
|
49
|
+
if (!Number.isFinite(value)) {
|
|
50
|
+
throw new Error('canonicalize: non-finite numbers cannot be serialized');
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
if (valueType === 'string' || valueType === 'boolean') {
|
|
55
|
+
return JSON.stringify(value);
|
|
56
|
+
}
|
|
57
|
+
if (valueType === 'bigint') {
|
|
58
|
+
throw new Error('canonicalize: bigint values cannot be serialized');
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
const items = value.map((item) => {
|
|
62
|
+
const itemType = typeof item;
|
|
63
|
+
// JSON array semantics: undefined / function / symbol become null so the
|
|
64
|
+
// element positions (and therefore the array length) are preserved.
|
|
65
|
+
if (item === undefined || itemType === 'function' || itemType === 'symbol') {
|
|
66
|
+
return 'null';
|
|
67
|
+
}
|
|
68
|
+
return serialize(item);
|
|
69
|
+
});
|
|
70
|
+
return `[${items.join(',')}]`;
|
|
71
|
+
}
|
|
72
|
+
if (valueType === 'object') {
|
|
73
|
+
const obj = value;
|
|
74
|
+
if (hasToJSON(obj)) {
|
|
75
|
+
return serialize(obj.toJSON());
|
|
76
|
+
}
|
|
77
|
+
const record = obj;
|
|
78
|
+
const parts = [];
|
|
79
|
+
for (const key of Object.keys(record).sort()) {
|
|
80
|
+
const member = record[key];
|
|
81
|
+
const memberType = typeof member;
|
|
82
|
+
// JSON object semantics: properties with undefined / function / symbol
|
|
83
|
+
// values are omitted entirely.
|
|
84
|
+
if (member === undefined || memberType === 'function' || memberType === 'symbol') {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
parts.push(`${JSON.stringify(key)}:${serialize(member)}`);
|
|
88
|
+
}
|
|
89
|
+
return `{${parts.join(',')}}`;
|
|
90
|
+
}
|
|
91
|
+
// undefined / function / symbol at the top level have no JSON representation.
|
|
92
|
+
throw new Error(`canonicalize: cannot serialize a value of type ${valueType}`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Produce the canonical JSON string for `value`.
|
|
96
|
+
*
|
|
97
|
+
* Deterministic: two structurally-equal values yield identical strings even if
|
|
98
|
+
* their object keys were written in different orders. Use this — never an
|
|
99
|
+
* ad-hoc `JSON.stringify` of a hand-sorted object — as the signing input for
|
|
100
|
+
* signed records, so client signing and server verification cannot drift.
|
|
101
|
+
*
|
|
102
|
+
* @throws if `value` (or any nested member used as the top-level/primitive)
|
|
103
|
+
* contains a non-finite number or a `bigint`, which have no JSON form.
|
|
104
|
+
*/
|
|
105
|
+
function canonicalize(value) {
|
|
106
|
+
return serialize(value);
|
|
107
|
+
}
|
|
@@ -61,9 +61,9 @@ const STORAGE_KEYS = {
|
|
|
61
61
|
/**
|
|
62
62
|
* iOS Keychain Access Group for sharing identities across Oxy apps
|
|
63
63
|
* All Oxy apps must have this access group enabled in their entitlements
|
|
64
|
-
* Format: [Team ID].
|
|
64
|
+
* Format: [Team ID].so.oxy.shared or group.so.oxy.shared
|
|
65
65
|
*/
|
|
66
|
-
const IOS_KEYCHAIN_GROUP = 'group.
|
|
66
|
+
const IOS_KEYCHAIN_GROUP = 'group.so.oxy.shared';
|
|
67
67
|
/**
|
|
68
68
|
* Android Account Manager type for shared authentication
|
|
69
69
|
* Used with sharedUserId to share sessions across apps
|
|
@@ -695,11 +695,25 @@ class KeyManager {
|
|
|
695
695
|
throw new IdentityPersistError('Stored identity failed crypto self-test', error);
|
|
696
696
|
}
|
|
697
697
|
// Step 3: The new primary is durable and functional. NOW it is safe to
|
|
698
|
-
// refresh the backup to the new key.
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
//
|
|
698
|
+
// refresh the backup to the new key. This is part of the successful write
|
|
699
|
+
// contract: returning success while the backup still belongs to the
|
|
700
|
+
// previous identity would allow a later restore with an absent primary to
|
|
701
|
+
// silently switch the device back to the previous account. Snapshot the
|
|
702
|
+
// backup first so a partial backup refresh can be rolled back along with
|
|
703
|
+
// the primary before surfacing the failure.
|
|
704
|
+
let priorBackupPrivate;
|
|
705
|
+
let priorBackupPublic;
|
|
706
|
+
let priorBackupTimestamp;
|
|
707
|
+
try {
|
|
708
|
+
priorBackupPrivate = await store.getItemAsync(STORAGE_KEYS.BACKUP_PRIVATE_KEY);
|
|
709
|
+
priorBackupPublic = await store.getItemAsync(STORAGE_KEYS.BACKUP_PUBLIC_KEY);
|
|
710
|
+
priorBackupTimestamp = await store.getItemAsync(STORAGE_KEYS.BACKUP_TIMESTAMP);
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
loggerUtils_1.logger.error('Failed to snapshot identity backup before refresh', error, { component: 'KeyManager' });
|
|
714
|
+
await KeyManager._rollbackPrimary(store, priorPrivate, priorPublic);
|
|
715
|
+
throw new IdentityPersistError('Failed to snapshot identity backup before refresh', error);
|
|
716
|
+
}
|
|
703
717
|
try {
|
|
704
718
|
await store.setItemAsync(STORAGE_KEYS.BACKUP_PRIVATE_KEY, canonicalPrivate, {
|
|
705
719
|
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
@@ -708,12 +722,57 @@ class KeyManager {
|
|
|
708
722
|
await store.setItemAsync(STORAGE_KEYS.BACKUP_TIMESTAMP, Date.now().toString());
|
|
709
723
|
}
|
|
710
724
|
catch (error) {
|
|
711
|
-
loggerUtils_1.logger.
|
|
725
|
+
loggerUtils_1.logger.error('Failed to refresh identity backup after primary write', error, { component: 'KeyManager' });
|
|
726
|
+
await KeyManager._rollbackBackup(store, priorBackupPrivate, priorBackupPublic, priorBackupTimestamp);
|
|
727
|
+
await KeyManager._rollbackPrimary(store, priorPrivate, priorPublic);
|
|
728
|
+
throw new IdentityPersistError('Failed to refresh identity backup after primary write', error);
|
|
712
729
|
}
|
|
713
730
|
// Update cache only after we are certain the identity is durable.
|
|
714
731
|
KeyManager.cachedPublicKey = canonicalPublic;
|
|
715
732
|
KeyManager.cachedHasIdentity = true;
|
|
716
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* Restore the backup slot to a previously-snapshotted state. Best-effort so
|
|
736
|
+
* the original persistence error remains the one surfaced to the caller.
|
|
737
|
+
*
|
|
738
|
+
* @internal
|
|
739
|
+
*/
|
|
740
|
+
static async _rollbackBackup(store, priorBackupPrivate, priorBackupPublic, priorBackupTimestamp) {
|
|
741
|
+
try {
|
|
742
|
+
if (priorBackupPrivate) {
|
|
743
|
+
await store.setItemAsync(STORAGE_KEYS.BACKUP_PRIVATE_KEY, priorBackupPrivate, {
|
|
744
|
+
keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
try {
|
|
749
|
+
await store.deleteItemAsync(STORAGE_KEYS.BACKUP_PRIVATE_KEY);
|
|
750
|
+
}
|
|
751
|
+
catch { /* best effort */ }
|
|
752
|
+
}
|
|
753
|
+
if (priorBackupPublic) {
|
|
754
|
+
await store.setItemAsync(STORAGE_KEYS.BACKUP_PUBLIC_KEY, priorBackupPublic);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
try {
|
|
758
|
+
await store.deleteItemAsync(STORAGE_KEYS.BACKUP_PUBLIC_KEY);
|
|
759
|
+
}
|
|
760
|
+
catch { /* best effort */ }
|
|
761
|
+
}
|
|
762
|
+
if (priorBackupTimestamp) {
|
|
763
|
+
await store.setItemAsync(STORAGE_KEYS.BACKUP_TIMESTAMP, priorBackupTimestamp);
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
try {
|
|
767
|
+
await store.deleteItemAsync(STORAGE_KEYS.BACKUP_TIMESTAMP);
|
|
768
|
+
}
|
|
769
|
+
catch { /* best effort */ }
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch (rollbackError) {
|
|
773
|
+
loggerUtils_1.logger.error('Failed to roll back identity backup after a failed refresh', rollbackError, { component: 'KeyManager' });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
717
776
|
/**
|
|
718
777
|
* Restore the primary slot to a previously-snapshotted (privA, pubA) pair,
|
|
719
778
|
* or delete it entirely if there was no prior identity. Best-effort: every
|
|
@@ -7,13 +7,27 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.SignatureService = void 0;
|
|
10
|
+
exports.signedRecordSigningInput = signedRecordSigningInput;
|
|
10
11
|
const elliptic_1 = require("elliptic");
|
|
11
12
|
const keyManager_1 = require("./keyManager");
|
|
13
|
+
const canonicalJson_1 = require("./canonicalJson");
|
|
12
14
|
const platform_1 = require("../utils/platform");
|
|
13
15
|
const platformCrypto_1 = require("../utils/platformCrypto");
|
|
14
16
|
const loggerUtils_1 = require("../utils/loggerUtils");
|
|
15
17
|
const debugUtils_1 = require("../shared/utils/debugUtils");
|
|
16
18
|
const ec = new elliptic_1.ec('secp256k1');
|
|
19
|
+
/**
|
|
20
|
+
* Compute the canonical signing input for a signed-record envelope.
|
|
21
|
+
*
|
|
22
|
+
* This is the single definition of "what the signature covers": the canonical
|
|
23
|
+
* JSON of `{version, type, subject, issuer, record, issuedAt}`. `@oxyhq/core`
|
|
24
|
+
* (client signing) and `@oxyhq/api` (server verification) both call this, so a
|
|
25
|
+
* record signed by a client and verified by the server cannot drift.
|
|
26
|
+
*/
|
|
27
|
+
function signedRecordSigningInput(fields) {
|
|
28
|
+
const { version, type, subject, issuer, record, issuedAt } = fields;
|
|
29
|
+
return (0, canonicalJson_1.canonicalize)({ version, type, subject, issuer, record, issuedAt });
|
|
30
|
+
}
|
|
17
31
|
/**
|
|
18
32
|
* Compute SHA-256 hash of a string
|
|
19
33
|
*/
|
|
@@ -201,6 +215,36 @@ class SignatureService {
|
|
|
201
215
|
timestamp,
|
|
202
216
|
};
|
|
203
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Create a signed authentication challenge response using the SHARED identity
|
|
220
|
+
* key (the cross-app `group.so.oxy.shared` keychain key), not the primary
|
|
221
|
+
* device key.
|
|
222
|
+
*
|
|
223
|
+
* Mirrors {@link signChallenge} exactly — same message format
|
|
224
|
+
* (`auth:${publicKey}:${challenge}:${timestamp}`) so the server verification
|
|
225
|
+
* path is unchanged — but sources the shared public/private key from
|
|
226
|
+
* `KeyManager` and signs with `signWithKey`. Used by "Sign in with Oxy"
|
|
227
|
+
* same-device shared-keychain SSO (Mechanism A): a sibling native app proves
|
|
228
|
+
* control of the shared identity to mint its own session.
|
|
229
|
+
*
|
|
230
|
+
* Throws if no shared identity exists (native-only; the shared keychain is
|
|
231
|
+
* unavailable on web).
|
|
232
|
+
*/
|
|
233
|
+
static async signChallengeWithSharedKey(challenge) {
|
|
234
|
+
const publicKey = await keyManager_1.KeyManager.getSharedPublicKey();
|
|
235
|
+
const privateKey = await keyManager_1.KeyManager.getSharedPrivateKey();
|
|
236
|
+
if (!publicKey || !privateKey) {
|
|
237
|
+
throw new Error('No shared identity found. Cannot sign with the shared key.');
|
|
238
|
+
}
|
|
239
|
+
const timestamp = Date.now();
|
|
240
|
+
const message = `auth:${publicKey}:${challenge}:${timestamp}`;
|
|
241
|
+
const signature = await SignatureService.signWithKey(message, privateKey);
|
|
242
|
+
return {
|
|
243
|
+
challenge: signature,
|
|
244
|
+
publicKey,
|
|
245
|
+
timestamp,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
204
248
|
/**
|
|
205
249
|
* Verify a challenge response
|
|
206
250
|
*/
|
|
@@ -255,6 +299,65 @@ class SignatureService {
|
|
|
255
299
|
timestamp,
|
|
256
300
|
};
|
|
257
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Build a signed-record envelope for a self-issued identity/profile record.
|
|
304
|
+
*
|
|
305
|
+
* The envelope is self-issued: `issuer` equals `subject` (the signer's DID).
|
|
306
|
+
* The signature covers the canonical JSON of every field EXCEPT `publicKey`
|
|
307
|
+
* and `signature` (see {@link signedRecordSigningInput}); `alg` is
|
|
308
|
+
* `ES256K-DER-SHA256` (secp256k1 over the SHA-256 of the canonical bytes,
|
|
309
|
+
* DER-encoded), the same scheme this service uses everywhere else.
|
|
310
|
+
*
|
|
311
|
+
* Requires a stored identity (native secure storage); throws if none exists.
|
|
312
|
+
*
|
|
313
|
+
* @param type - The record category (`'identity'` or `'profile'`).
|
|
314
|
+
* @param subject - The subject DID the record is about (also the issuer).
|
|
315
|
+
* @param record - The arbitrary record payload to attest to.
|
|
316
|
+
*/
|
|
317
|
+
static async signRecord(type, subject, record) {
|
|
318
|
+
const publicKey = await keyManager_1.KeyManager.getPublicKey();
|
|
319
|
+
if (!publicKey) {
|
|
320
|
+
throw new Error('No identity found. Please create or import an identity first.');
|
|
321
|
+
}
|
|
322
|
+
const version = 1;
|
|
323
|
+
const issuer = subject;
|
|
324
|
+
const issuedAt = Date.now();
|
|
325
|
+
const signingInput = signedRecordSigningInput({
|
|
326
|
+
version,
|
|
327
|
+
type,
|
|
328
|
+
subject,
|
|
329
|
+
issuer,
|
|
330
|
+
record,
|
|
331
|
+
issuedAt,
|
|
332
|
+
});
|
|
333
|
+
const signature = await SignatureService.sign(signingInput);
|
|
334
|
+
return {
|
|
335
|
+
version,
|
|
336
|
+
type,
|
|
337
|
+
subject,
|
|
338
|
+
issuer,
|
|
339
|
+
record,
|
|
340
|
+
issuedAt,
|
|
341
|
+
publicKey,
|
|
342
|
+
alg: 'ES256K-DER-SHA256',
|
|
343
|
+
signature,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Verify a signed-record envelope: recompute the canonical signing input from
|
|
348
|
+
* the envelope's own fields and check the signature against the envelope's
|
|
349
|
+
* `publicKey`.
|
|
350
|
+
*
|
|
351
|
+
* Note: this confirms the signature is internally consistent with the
|
|
352
|
+
* embedded `publicKey`. It does NOT establish that `publicKey` is an
|
|
353
|
+
* authorized verification method for `subject` — that authorization check is
|
|
354
|
+
* the server's responsibility (it asserts the key is a current verification
|
|
355
|
+
* method on the subject's DID).
|
|
356
|
+
*/
|
|
357
|
+
static async verifyRecord(envelope) {
|
|
358
|
+
const signingInput = signedRecordSigningInput(envelope);
|
|
359
|
+
return SignatureService.verify(signingInput, envelope.signature, envelope.publicKey);
|
|
360
|
+
}
|
|
258
361
|
}
|
|
259
362
|
exports.SignatureService = SignatureService;
|
|
260
363
|
exports.default = SignatureService;
|
package/dist/cjs/index.js
CHANGED
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* If a symbol does not appear here, it is NOT part of the public API.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.
|
|
22
|
-
exports.
|
|
23
|
-
exports.
|
|
24
|
-
exports.packageInfo = exports.runColdBoot = void 0;
|
|
21
|
+
exports.darkenColor = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.isRTLLocale = exports.normalizeLanguageCode = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.RecoveryPhraseService = exports.canonicalize = exports.signedRecordSigningInput = exports.SignatureService = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.KeyManager = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.buildUserDid = exports.getNormalizedUserHandle = exports.getCanonicalUserHandle = exports.normalizeUserIdentityOrNull = exports.normalizeUserIdentity = exports.getNormalizedUserId = exports.OxyAppDataIdentifierError = exports.ServiceCredentialMismatchError = exports.createCrossDomainAuth = exports.CrossDomainAuth = exports.createAuthManager = exports.AuthManager = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
|
|
22
|
+
exports.isValidUsername = exports.isValidEmail = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.translate = exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = void 0;
|
|
23
|
+
exports.getSsoCallbackBootstrapScript = exports.ssoNavigate = exports.ssoCallbackBootstrapKey = exports.ssoAttemptedKey = exports.ssoNoSessionKey = exports.ssoDestKey = exports.ssoGuardKey = exports.ssoStateKey = exports.SSO_GUARD_TTL_MS = exports.SSO_CALLBACK_PATH = exports.generateSsoState = exports.consumeSsoReturn = exports.parseSsoReturnFragment = exports.resolveCentralAuthUrl = exports.CENTRAL_IDP_APEX = exports.CENTRAL_AUTH_URL = exports.registrableApex = exports.autoDetectAuthWebUrl = exports.getAccountColor = exports.mergeAccountsFromRefreshAll = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.isValidPassword = void 0;
|
|
24
|
+
exports.packageInfo = exports.runColdBoot = exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = void 0;
|
|
25
25
|
// Ensure crypto polyfills are loaded before anything else
|
|
26
26
|
require("./crypto/polyfill");
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
@@ -58,6 +58,14 @@ var userHandle_1 = require("./utils/userHandle");
|
|
|
58
58
|
Object.defineProperty(exports, "getCanonicalUserHandle", { enumerable: true, get: function () { return userHandle_1.getCanonicalUserHandle; } });
|
|
59
59
|
Object.defineProperty(exports, "getNormalizedUserHandle", { enumerable: true, get: function () { return userHandle_1.getNormalizedUserHandle; } });
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
|
+
// Self-sovereign identity (DID, signed records, auth-method ↔ VM mapping,
|
|
62
|
+
// verified domains). Wire shapes (DidDocument, SignedRecordEnvelope,
|
|
63
|
+
// AuthMethodsResponse, VerifiedDomain, DomainVerificationInstructions,
|
|
64
|
+
// ExportBundle) live in `@oxyhq/contracts` — import them directly from there.
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
var OxyServices_identity_1 = require("./mixins/OxyServices.identity");
|
|
67
|
+
Object.defineProperty(exports, "buildUserDid", { enumerable: true, get: function () { return OxyServices_identity_1.buildUserDid; } });
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
61
69
|
// Auth helpers (token refresh, error normalisation, retry policies)
|
|
62
70
|
// ---------------------------------------------------------------------------
|
|
63
71
|
var authHelpers_1 = require("./utils/authHelpers");
|
|
@@ -83,6 +91,9 @@ Object.defineProperty(exports, "IdentityAlreadyExistsError", { enumerable: true,
|
|
|
83
91
|
Object.defineProperty(exports, "IdentityPersistError", { enumerable: true, get: function () { return keyManager_1.IdentityPersistError; } });
|
|
84
92
|
var signatureService_1 = require("./crypto/signatureService");
|
|
85
93
|
Object.defineProperty(exports, "SignatureService", { enumerable: true, get: function () { return signatureService_1.SignatureService; } });
|
|
94
|
+
Object.defineProperty(exports, "signedRecordSigningInput", { enumerable: true, get: function () { return signatureService_1.signedRecordSigningInput; } });
|
|
95
|
+
var canonicalJson_1 = require("./crypto/canonicalJson");
|
|
96
|
+
Object.defineProperty(exports, "canonicalize", { enumerable: true, get: function () { return canonicalJson_1.canonicalize; } });
|
|
86
97
|
var recoveryPhrase_1 = require("./crypto/recoveryPhrase");
|
|
87
98
|
Object.defineProperty(exports, "RecoveryPhraseService", { enumerable: true, get: function () { return recoveryPhrase_1.RecoveryPhraseService; } });
|
|
88
99
|
// ---------------------------------------------------------------------------
|
|
@@ -441,7 +441,9 @@ function OxyServicesAssetsMixin(Base) {
|
|
|
441
441
|
return urlRes?.url || null;
|
|
442
442
|
}
|
|
443
443
|
async fetchAssetContent(url, type) {
|
|
444
|
-
const response = await fetch(url, {
|
|
444
|
+
const response = await fetch(url, {
|
|
445
|
+
credentials: shouldSendAssetCredentials(url, this.getBaseURL()) ? 'include' : 'omit',
|
|
446
|
+
});
|
|
445
447
|
if (!response?.ok) {
|
|
446
448
|
throw new Error(`Failed to fetch asset content (status ${response?.status})`);
|
|
447
449
|
}
|
|
@@ -449,3 +451,16 @@ function OxyServicesAssetsMixin(Base) {
|
|
|
449
451
|
}
|
|
450
452
|
};
|
|
451
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Only send ambient credentials (cookies) when the asset URL is same-origin with
|
|
456
|
+
* the configured API base. Caller-supplied cross-origin asset URLs must not leak
|
|
457
|
+
* the user's cookies to arbitrary hosts.
|
|
458
|
+
*/
|
|
459
|
+
function shouldSendAssetCredentials(url, baseURL) {
|
|
460
|
+
try {
|
|
461
|
+
return new URL(url).origin === new URL(baseURL).origin;
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}
|