@oxyhq/core 3.10.1 → 3.12.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 +189 -0
- package/dist/cjs/index.js +30 -4
- package/dist/cjs/mixins/OxyServices.assets.js +16 -1
- package/dist/cjs/mixins/OxyServices.auth.js +190 -1
- package/dist/cjs/mixins/OxyServices.civic.js +611 -0
- 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 +6 -0
- package/dist/cjs/server/cors.js +20 -21
- package/dist/cjs/server/rateLimit.js +32 -8
- package/dist/cjs/utils/profileLinks.js +52 -0
- 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 +187 -0
- package/dist/esm/index.js +19 -1
- package/dist/esm/mixins/OxyServices.assets.js +16 -1
- package/dist/esm/mixins/OxyServices.auth.js +190 -1
- package/dist/esm/mixins/OxyServices.civic.js +605 -0
- 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 +6 -0
- package/dist/esm/server/cors.js +20 -21
- package/dist/esm/server/rateLimit.js +32 -8
- package/dist/esm/utils/profileLinks.js +49 -0
- 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 +112 -0
- package/dist/types/index.d.ts +10 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +136 -0
- package/dist/types/mixins/OxyServices.civic.d.ts +512 -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 +3 -1
- package/dist/types/models/interfaces.d.ts +3 -0
- package/dist/types/server/cors.d.ts +5 -5
- package/dist/types/utils/profileLinks.d.ts +36 -0
- 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 +345 -0
- package/src/crypto/canonicalJson.ts +120 -0
- package/src/crypto/keyManager.ts +62 -12
- package/src/crypto/signatureService.ts +225 -0
- package/src/index.ts +55 -2
- package/src/mixins/OxyServices.assets.ts +16 -1
- package/src/mixins/OxyServices.auth.ts +309 -1
- package/src/mixins/OxyServices.civic.ts +956 -0
- 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.civic.test.ts +1097 -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 +8 -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__/profileLinks.test.ts +126 -0
- package/src/utils/__tests__/ssoReturn.test.ts +1 -1
- package/src/utils/profileLinks.ts +74 -0
- 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,55 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.SignatureService = void 0;
|
|
10
|
+
exports.signedRecordSigningInput = signedRecordSigningInput;
|
|
11
|
+
exports.computeRecordId = computeRecordId;
|
|
10
12
|
const elliptic_1 = require("elliptic");
|
|
11
13
|
const keyManager_1 = require("./keyManager");
|
|
14
|
+
const canonicalJson_1 = require("./canonicalJson");
|
|
12
15
|
const platform_1 = require("../utils/platform");
|
|
13
16
|
const platformCrypto_1 = require("../utils/platformCrypto");
|
|
14
17
|
const loggerUtils_1 = require("../utils/loggerUtils");
|
|
15
18
|
const debugUtils_1 = require("../shared/utils/debugUtils");
|
|
16
19
|
const ec = new elliptic_1.ec('secp256k1');
|
|
20
|
+
/**
|
|
21
|
+
* Compute the canonical signing input for a signed-record envelope.
|
|
22
|
+
*
|
|
23
|
+
* This is the single definition of "what the signature covers". `@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
|
+
* - **v1**: the canonical JSON of `{version, type, subject, issuer, record,
|
|
28
|
+
* issuedAt}` — BYTE-IDENTICAL to the original scheme, so every signature
|
|
29
|
+
* already in production keeps verifying.
|
|
30
|
+
* - **v2**: the canonical JSON additionally includes the hash-chain fields
|
|
31
|
+
* `{seq, prev, collection, rkey}`. Because {@link canonicalize} sorts keys,
|
|
32
|
+
* the on-the-wire field order is irrelevant; the resulting canonical key
|
|
33
|
+
* order is `collection, issuedAt, issuer, prev, record, rkey, seq, subject,
|
|
34
|
+
* type, version`. `prev` is `null` at genesis (serialized as `null`, not
|
|
35
|
+
* omitted), so it is always part of the signed bytes.
|
|
36
|
+
*/
|
|
37
|
+
function signedRecordSigningInput(fields) {
|
|
38
|
+
const { version, type, subject, issuer, record, issuedAt } = fields;
|
|
39
|
+
if (version === 2) {
|
|
40
|
+
const { seq, prev, collection, rkey } = fields;
|
|
41
|
+
return (0, canonicalJson_1.canonicalize)({ version, type, subject, issuer, record, issuedAt, seq, prev, collection, rkey });
|
|
42
|
+
}
|
|
43
|
+
return (0, canonicalJson_1.canonicalize)({ version, type, subject, issuer, record, issuedAt });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Compute the `recordId` (content address) of a signed record: the SHA-256 hex
|
|
47
|
+
* digest of its canonical {@link signedRecordSigningInput}.
|
|
48
|
+
*
|
|
49
|
+
* Deterministic and stable across runtimes (it reuses the same canonicalization
|
|
50
|
+
* + SHA-256 the signature itself is built on). The recordId is what `prev`
|
|
51
|
+
* references in the per-subject hash chain, so `@oxyhq/core` (client) and
|
|
52
|
+
* `@oxyhq/api` (server) MUST compute it identically — both call this function.
|
|
53
|
+
* It is taken over the SIGNING input (excluding `publicKey`/`signature`), so it
|
|
54
|
+
* is a pure content address of the record's meaning, independent of who signed.
|
|
55
|
+
*/
|
|
56
|
+
async function computeRecordId(fields) {
|
|
57
|
+
return sha256(signedRecordSigningInput(fields));
|
|
58
|
+
}
|
|
17
59
|
/**
|
|
18
60
|
* Compute SHA-256 hash of a string
|
|
19
61
|
*/
|
|
@@ -201,6 +243,36 @@ class SignatureService {
|
|
|
201
243
|
timestamp,
|
|
202
244
|
};
|
|
203
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Create a signed authentication challenge response using the SHARED identity
|
|
248
|
+
* key (the cross-app `group.so.oxy.shared` keychain key), not the primary
|
|
249
|
+
* device key.
|
|
250
|
+
*
|
|
251
|
+
* Mirrors {@link signChallenge} exactly — same message format
|
|
252
|
+
* (`auth:${publicKey}:${challenge}:${timestamp}`) so the server verification
|
|
253
|
+
* path is unchanged — but sources the shared public/private key from
|
|
254
|
+
* `KeyManager` and signs with `signWithKey`. Used by "Sign in with Oxy"
|
|
255
|
+
* same-device shared-keychain SSO (Mechanism A): a sibling native app proves
|
|
256
|
+
* control of the shared identity to mint its own session.
|
|
257
|
+
*
|
|
258
|
+
* Throws if no shared identity exists (native-only; the shared keychain is
|
|
259
|
+
* unavailable on web).
|
|
260
|
+
*/
|
|
261
|
+
static async signChallengeWithSharedKey(challenge) {
|
|
262
|
+
const publicKey = await keyManager_1.KeyManager.getSharedPublicKey();
|
|
263
|
+
const privateKey = await keyManager_1.KeyManager.getSharedPrivateKey();
|
|
264
|
+
if (!publicKey || !privateKey) {
|
|
265
|
+
throw new Error('No shared identity found. Cannot sign with the shared key.');
|
|
266
|
+
}
|
|
267
|
+
const timestamp = Date.now();
|
|
268
|
+
const message = `auth:${publicKey}:${challenge}:${timestamp}`;
|
|
269
|
+
const signature = await SignatureService.signWithKey(message, privateKey);
|
|
270
|
+
return {
|
|
271
|
+
challenge: signature,
|
|
272
|
+
publicKey,
|
|
273
|
+
timestamp,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
204
276
|
/**
|
|
205
277
|
* Verify a challenge response
|
|
206
278
|
*/
|
|
@@ -255,6 +327,123 @@ class SignatureService {
|
|
|
255
327
|
timestamp,
|
|
256
328
|
};
|
|
257
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Build a signed-record envelope for a self-issued identity/profile record.
|
|
332
|
+
*
|
|
333
|
+
* The envelope is self-issued: `issuer` equals `subject` (the signer's DID).
|
|
334
|
+
* The signature covers the canonical JSON of every field EXCEPT `publicKey`
|
|
335
|
+
* and `signature` (see {@link signedRecordSigningInput}); `alg` is
|
|
336
|
+
* `ES256K-DER-SHA256` (secp256k1 over the SHA-256 of the canonical bytes,
|
|
337
|
+
* DER-encoded), the same scheme this service uses everywhere else.
|
|
338
|
+
*
|
|
339
|
+
* Requires a stored identity (native secure storage); throws if none exists.
|
|
340
|
+
*
|
|
341
|
+
* @param type - The record category (`'identity'` or `'profile'`).
|
|
342
|
+
* @param subject - The subject DID the record is about (also the issuer).
|
|
343
|
+
* @param record - The arbitrary record payload to attest to.
|
|
344
|
+
*/
|
|
345
|
+
static async signRecord(type, subject, record) {
|
|
346
|
+
const publicKey = await keyManager_1.KeyManager.getPublicKey();
|
|
347
|
+
if (!publicKey) {
|
|
348
|
+
throw new Error('No identity found. Please create or import an identity first.');
|
|
349
|
+
}
|
|
350
|
+
const version = 1;
|
|
351
|
+
const issuer = subject;
|
|
352
|
+
const issuedAt = Date.now();
|
|
353
|
+
const signingInput = signedRecordSigningInput({
|
|
354
|
+
version,
|
|
355
|
+
type,
|
|
356
|
+
subject,
|
|
357
|
+
issuer,
|
|
358
|
+
record,
|
|
359
|
+
issuedAt,
|
|
360
|
+
});
|
|
361
|
+
const signature = await SignatureService.sign(signingInput);
|
|
362
|
+
return {
|
|
363
|
+
version,
|
|
364
|
+
type,
|
|
365
|
+
subject,
|
|
366
|
+
issuer,
|
|
367
|
+
record,
|
|
368
|
+
issuedAt,
|
|
369
|
+
publicKey,
|
|
370
|
+
alg: 'ES256K-DER-SHA256',
|
|
371
|
+
signature,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Build a signed-record envelope (v2) carrying the per-subject hash-chain
|
|
376
|
+
* fields.
|
|
377
|
+
*
|
|
378
|
+
* Identical to {@link signRecord} (self-issued: `issuer === subject`; same
|
|
379
|
+
* `ES256K-DER-SHA256` scheme over {@link signedRecordSigningInput}) but
|
|
380
|
+
* `version` is `2` and the signed bytes additionally cover the chain fields:
|
|
381
|
+
*
|
|
382
|
+
* @param type - The record category.
|
|
383
|
+
* @param subject - The subject DID the record is about (also the issuer).
|
|
384
|
+
* @param record - The arbitrary record payload to attest to.
|
|
385
|
+
* @param chain - The hash-chain coordinates:
|
|
386
|
+
* - `seq` — strictly-increasing sequence number for this subject's chain.
|
|
387
|
+
* - `prev` — the `recordId` of the previous record, or `null` at genesis.
|
|
388
|
+
* - `collection` + `rkey` — the AtProto-style record key.
|
|
389
|
+
*
|
|
390
|
+
* The caller is responsible for fetching the current chain head (so `seq` /
|
|
391
|
+
* `prev` are correct) before signing. Requires a stored identity; throws if
|
|
392
|
+
* none exists.
|
|
393
|
+
*/
|
|
394
|
+
static async signRecordV2(type, subject, record, chain) {
|
|
395
|
+
const publicKey = await keyManager_1.KeyManager.getPublicKey();
|
|
396
|
+
if (!publicKey) {
|
|
397
|
+
throw new Error('No identity found. Please create or import an identity first.');
|
|
398
|
+
}
|
|
399
|
+
const version = 2;
|
|
400
|
+
const issuer = subject;
|
|
401
|
+
const issuedAt = Date.now();
|
|
402
|
+
const { seq, prev, collection, rkey } = chain;
|
|
403
|
+
const signingInput = signedRecordSigningInput({
|
|
404
|
+
version,
|
|
405
|
+
type,
|
|
406
|
+
subject,
|
|
407
|
+
issuer,
|
|
408
|
+
record,
|
|
409
|
+
issuedAt,
|
|
410
|
+
seq,
|
|
411
|
+
prev,
|
|
412
|
+
collection,
|
|
413
|
+
rkey,
|
|
414
|
+
});
|
|
415
|
+
const signature = await SignatureService.sign(signingInput);
|
|
416
|
+
return {
|
|
417
|
+
version,
|
|
418
|
+
type,
|
|
419
|
+
subject,
|
|
420
|
+
issuer,
|
|
421
|
+
record,
|
|
422
|
+
issuedAt,
|
|
423
|
+
seq,
|
|
424
|
+
prev,
|
|
425
|
+
collection,
|
|
426
|
+
rkey,
|
|
427
|
+
publicKey,
|
|
428
|
+
alg: 'ES256K-DER-SHA256',
|
|
429
|
+
signature,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Verify a signed-record envelope: recompute the canonical signing input from
|
|
434
|
+
* the envelope's own fields and check the signature against the envelope's
|
|
435
|
+
* `publicKey`.
|
|
436
|
+
*
|
|
437
|
+
* Note: this confirms the signature is internally consistent with the
|
|
438
|
+
* embedded `publicKey`. It does NOT establish that `publicKey` is an
|
|
439
|
+
* authorized verification method for `subject` — that authorization check is
|
|
440
|
+
* the server's responsibility (it asserts the key is a current verification
|
|
441
|
+
* method on the subject's DID).
|
|
442
|
+
*/
|
|
443
|
+
static async verifyRecord(envelope) {
|
|
444
|
+
const signingInput = signedRecordSigningInput(envelope);
|
|
445
|
+
return SignatureService.verify(signingInput, envelope.signature, envelope.publicKey);
|
|
446
|
+
}
|
|
258
447
|
}
|
|
259
448
|
exports.SignatureService = SignatureService;
|
|
260
449
|
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.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.computeRecordId = 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.verifyPublicCardAttestation = exports.parseAttestPayload = exports.parseIdPayload = exports.buildUserDid = exports.normalizeProfileLinks = 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.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 = exports.darkenColor = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = void 0;
|
|
23
|
+
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 = exports.isValidUsername = exports.isValidEmail = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = void 0;
|
|
24
|
+
exports.packageInfo = exports.runColdBoot = exports.guardActive = exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = exports.getSsoCallbackBootstrapScript = exports.ssoNavigate = exports.ssoCallbackBootstrapKey = exports.ssoAttemptedKey = exports.ssoNoSessionKey = void 0;
|
|
25
25
|
// Ensure crypto polyfills are loaded before anything else
|
|
26
26
|
require("./crypto/polyfill");
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
@@ -57,6 +57,28 @@ Object.defineProperty(exports, "normalizeUserIdentityOrNull", { enumerable: true
|
|
|
57
57
|
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
|
+
var profileLinks_1 = require("./utils/profileLinks");
|
|
61
|
+
Object.defineProperty(exports, "normalizeProfileLinks", { enumerable: true, get: function () { return profileLinks_1.normalizeProfileLinks; } });
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Self-sovereign identity (DID, signed records, auth-method ↔ VM mapping,
|
|
64
|
+
// verified domains). Wire shapes (DidDocument, SignedRecordEnvelope,
|
|
65
|
+
// AuthMethodsResponse, VerifiedDomain, DomainVerificationInstructions,
|
|
66
|
+
// ExportBundle) live in `@oxyhq/contracts` — import them directly from there.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
var OxyServices_identity_1 = require("./mixins/OxyServices.identity");
|
|
69
|
+
Object.defineProperty(exports, "buildUserDid", { enumerable: true, get: function () { return OxyServices_identity_1.buildUserDid; } });
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Civic / Commons "Oxy ID" (public signed cards + Oxy ID QR payload) and Fase 2
|
|
72
|
+
// anti-gaming (real-life attestation QR + validator/jury). Wire shapes
|
|
73
|
+
// (PublicCard, SignedPublicCard, RealLifeAttestationResult,
|
|
74
|
+
// ValidationRequestSummary, ValidationVoteResult, ValidationVerdict, …) live in
|
|
75
|
+
// `@oxyhq/contracts` — import them from there. The SDK adds the client verdict
|
|
76
|
+
// wrapper, the QR payload parsers/builders, and the submit inputs/results.
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
var OxyServices_civic_1 = require("./mixins/OxyServices.civic");
|
|
79
|
+
Object.defineProperty(exports, "parseIdPayload", { enumerable: true, get: function () { return OxyServices_civic_1.parseIdPayload; } });
|
|
80
|
+
Object.defineProperty(exports, "parseAttestPayload", { enumerable: true, get: function () { return OxyServices_civic_1.parseAttestPayload; } });
|
|
81
|
+
Object.defineProperty(exports, "verifyPublicCardAttestation", { enumerable: true, get: function () { return OxyServices_civic_1.verifyPublicCardAttestation; } });
|
|
60
82
|
// ---------------------------------------------------------------------------
|
|
61
83
|
// Auth helpers (token refresh, error normalisation, retry policies)
|
|
62
84
|
// ---------------------------------------------------------------------------
|
|
@@ -83,6 +105,10 @@ Object.defineProperty(exports, "IdentityAlreadyExistsError", { enumerable: true,
|
|
|
83
105
|
Object.defineProperty(exports, "IdentityPersistError", { enumerable: true, get: function () { return keyManager_1.IdentityPersistError; } });
|
|
84
106
|
var signatureService_1 = require("./crypto/signatureService");
|
|
85
107
|
Object.defineProperty(exports, "SignatureService", { enumerable: true, get: function () { return signatureService_1.SignatureService; } });
|
|
108
|
+
Object.defineProperty(exports, "signedRecordSigningInput", { enumerable: true, get: function () { return signatureService_1.signedRecordSigningInput; } });
|
|
109
|
+
Object.defineProperty(exports, "computeRecordId", { enumerable: true, get: function () { return signatureService_1.computeRecordId; } });
|
|
110
|
+
var canonicalJson_1 = require("./crypto/canonicalJson");
|
|
111
|
+
Object.defineProperty(exports, "canonicalize", { enumerable: true, get: function () { return canonicalJson_1.canonicalize; } });
|
|
86
112
|
var recoveryPhrase_1 = require("./crypto/recoveryPhrase");
|
|
87
113
|
Object.defineProperty(exports, "RecoveryPhraseService", { enumerable: true, get: function () { return recoveryPhrase_1.RecoveryPhraseService; } });
|
|
88
114
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
+
}
|