@oxyhq/core 3.10.0 → 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.
Files changed (105) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/AuthManager.js +9 -2
  3. package/dist/cjs/HttpService.js +27 -9
  4. package/dist/cjs/OxyServices.base.js +3 -2
  5. package/dist/cjs/crypto/canonicalJson.js +107 -0
  6. package/dist/cjs/crypto/keyManager.js +67 -8
  7. package/dist/cjs/crypto/signatureService.js +103 -0
  8. package/dist/cjs/i18n/locales/en-US.json +9 -0
  9. package/dist/cjs/i18n/locales/es-ES.json +9 -0
  10. package/dist/cjs/i18n/locales/locales/en-US.json +9 -0
  11. package/dist/cjs/i18n/locales/locales/es-ES.json +9 -0
  12. package/dist/cjs/index.js +15 -5
  13. package/dist/cjs/mixins/OxyServices.assets.js +45 -7
  14. package/dist/cjs/mixins/OxyServices.auth.js +190 -1
  15. package/dist/cjs/mixins/OxyServices.identity.js +291 -0
  16. package/dist/cjs/mixins/OxyServices.sso.js +28 -1
  17. package/dist/cjs/mixins/OxyServices.user.js +1 -0
  18. package/dist/cjs/mixins/OxyServices.utility.js +52 -23
  19. package/dist/cjs/mixins/index.js +3 -0
  20. package/dist/cjs/server/cors.js +20 -21
  21. package/dist/cjs/server/rateLimit.js +32 -8
  22. package/dist/cjs/utils/fapiAutoDetect.js +12 -42
  23. package/dist/cjs/utils/ssoReturn.js +1 -1
  24. package/dist/esm/.tsbuildinfo +1 -1
  25. package/dist/esm/AuthManager.js +9 -2
  26. package/dist/esm/HttpService.js +27 -9
  27. package/dist/esm/OxyServices.base.js +3 -2
  28. package/dist/esm/crypto/canonicalJson.js +104 -0
  29. package/dist/esm/crypto/keyManager.js +67 -8
  30. package/dist/esm/crypto/signatureService.js +102 -0
  31. package/dist/esm/i18n/locales/en-US.json +9 -0
  32. package/dist/esm/i18n/locales/es-ES.json +9 -0
  33. package/dist/esm/i18n/locales/locales/en-US.json +9 -0
  34. package/dist/esm/i18n/locales/locales/es-ES.json +9 -0
  35. package/dist/esm/index.js +10 -2
  36. package/dist/esm/mixins/OxyServices.assets.js +45 -7
  37. package/dist/esm/mixins/OxyServices.auth.js +190 -1
  38. package/dist/esm/mixins/OxyServices.identity.js +287 -0
  39. package/dist/esm/mixins/OxyServices.sso.js +28 -1
  40. package/dist/esm/mixins/OxyServices.user.js +1 -0
  41. package/dist/esm/mixins/OxyServices.utility.js +52 -23
  42. package/dist/esm/mixins/index.js +3 -0
  43. package/dist/esm/server/cors.js +20 -21
  44. package/dist/esm/server/rateLimit.js +32 -8
  45. package/dist/esm/utils/fapiAutoDetect.js +12 -41
  46. package/dist/esm/utils/ssoReturn.js +1 -1
  47. package/dist/types/.tsbuildinfo +1 -1
  48. package/dist/types/HttpService.d.ts +3 -0
  49. package/dist/types/OxyServices.d.ts +2 -2
  50. package/dist/types/crypto/canonicalJson.d.ts +44 -0
  51. package/dist/types/crypto/keyManager.d.ts +7 -0
  52. package/dist/types/crypto/signatureService.d.ts +61 -0
  53. package/dist/types/index.d.ts +7 -3
  54. package/dist/types/mixins/OxyServices.assets.d.ts +6 -1
  55. package/dist/types/mixins/OxyServices.auth.d.ts +136 -0
  56. package/dist/types/mixins/OxyServices.identity.d.ts +249 -0
  57. package/dist/types/mixins/OxyServices.sso.d.ts +4 -1
  58. package/dist/types/mixins/OxyServices.utility.d.ts +3 -3
  59. package/dist/types/mixins/index.d.ts +2 -1
  60. package/dist/types/models/interfaces.d.ts +3 -0
  61. package/dist/types/server/cors.d.ts +5 -5
  62. package/dist/types/utils/fapiAutoDetect.d.ts +6 -23
  63. package/dist/types/utils/ssoReturn.d.ts +1 -1
  64. package/package.json +3 -2
  65. package/src/AuthManager.ts +8 -2
  66. package/src/HttpService.ts +36 -8
  67. package/src/OxyServices.base.ts +3 -2
  68. package/src/OxyServices.ts +1 -1
  69. package/src/__tests__/authManager.security.test.ts +31 -0
  70. package/src/__tests__/authSocket.test.ts +96 -0
  71. package/src/__tests__/httpServiceCsrf.test.ts +75 -0
  72. package/src/crypto/__tests__/canonicalJson.test.ts +116 -0
  73. package/src/crypto/__tests__/keyManager.atomicity.test.ts +41 -2
  74. package/src/crypto/__tests__/signChallengeShared.test.ts +64 -0
  75. package/src/crypto/__tests__/signedRecord.test.ts +125 -0
  76. package/src/crypto/canonicalJson.ts +120 -0
  77. package/src/crypto/keyManager.ts +62 -12
  78. package/src/crypto/signatureService.ts +126 -0
  79. package/src/i18n/locales/en-US.json +9 -0
  80. package/src/i18n/locales/es-ES.json +9 -0
  81. package/src/index.ts +28 -3
  82. package/src/mixins/OxyServices.assets.ts +56 -7
  83. package/src/mixins/OxyServices.auth.ts +309 -1
  84. package/src/mixins/OxyServices.identity.ts +445 -0
  85. package/src/mixins/OxyServices.sso.ts +30 -1
  86. package/src/mixins/OxyServices.user.ts +1 -0
  87. package/src/mixins/OxyServices.utility.ts +57 -23
  88. package/src/mixins/__tests__/OxyServices.identity.test.ts +364 -0
  89. package/src/mixins/__tests__/assetCredentials.test.ts +47 -0
  90. package/src/mixins/__tests__/assetUpload.test.ts +191 -0
  91. package/src/mixins/__tests__/commonsSignIn.test.ts +277 -0
  92. package/src/mixins/__tests__/getFileDownloadUrl.test.ts +13 -0
  93. package/src/mixins/__tests__/serviceAuth.test.ts +49 -2
  94. package/src/mixins/__tests__/sso.test.ts +31 -0
  95. package/src/mixins/index.ts +4 -0
  96. package/src/models/interfaces.ts +3 -0
  97. package/src/server/__tests__/cors.test.ts +5 -1
  98. package/src/server/__tests__/rateLimit.test.ts +116 -0
  99. package/src/server/cors.ts +25 -20
  100. package/src/server/rateLimit.ts +39 -8
  101. package/src/utils/__tests__/consumeSsoReturn.test.ts +1 -1
  102. package/src/utils/__tests__/fapiAutoDetect.test.ts +40 -11
  103. package/src/utils/__tests__/ssoReturn.test.ts +1 -1
  104. package/src/utils/fapiAutoDetect.ts +12 -39
  105. package/src/utils/ssoReturn.ts +2 -2
@@ -340,8 +340,15 @@ class AuthManager {
340
340
  * Get default storage based on environment.
341
341
  */
342
342
  getDefaultStorage() {
343
- if (typeof window !== 'undefined' && window.localStorage) {
344
- return new LocalStorageAdapter();
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
  }
@@ -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: 'include', // Include cookies for cross-origin requests (CSRF, session)
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 (contentType && contentType.includes('application/json')) {
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
- // withCredentials mirrors fetch's `credentials: 'include'` so the
536
- // session cookie and CSRF cookie continue to flow.
537
- xhr.withCredentials = true;
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
- this._cachedUserId = decoded.userId || decoded.id || null;
236
- return this._cachedUserId;
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].com.oxy.shared or group.com.oxy.shared
64
+ * Format: [Team ID].so.oxy.shared or group.so.oxy.shared
65
65
  */
66
- const IOS_KEYCHAIN_GROUP = 'group.com.oxy.shared';
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. If this final backup write fails the
699
- // user still has a fully working primary, and the backup still holds the
700
- // PREVIOUS good identity so we log and continue rather than failing the
701
- // whole operation (failing here would be strictly worse: a working
702
- // primary would be reported as an error to the caller).
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.warn('Primary identity persisted successfully but refreshing the backup failed; primary is usable, backup may be stale', { component: 'KeyManager' }, error);
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;
@@ -129,6 +129,15 @@
129
129
  "title": "Reputation = Trust & Growth",
130
130
  "body": "Oxy Trust is a reputation system that reacts to what you do. Helpful, respectful, constructive actions earn it. Harmful or low‑effort stuff chips it away. More reputation can unlock benefits; low reputation can limit features. It keeps things fair and rewards real contribution."
131
131
  },
132
+ "name": {
133
+ "title": "What's your name?",
134
+ "body": "Add your name so people know who you are.",
135
+ "firstLabel": "First name",
136
+ "firstPlaceholder": "Your first name",
137
+ "lastLabel": "Last name",
138
+ "lastPlaceholder": "Your last name",
139
+ "saveFailed": "Could not save your name"
140
+ },
132
141
  "avatar": {
133
142
  "title": "Make It Yours",
134
143
  "body": "Add an avatar so people recognize you. It will show anywhere you show up here. Skip if you want — you can add it later.",
@@ -849,6 +849,15 @@
849
849
  "title": "Reputación = Confianza y crecimiento",
850
850
  "body": "Oxy Trust es un sistema de reputación que reacciona a lo que haces. Las acciones útiles, respetuosas y constructivas la aumentan. Las acciones dañinas o de poco esfuerzo la reducen. Más reputación puede desbloquear beneficios; poca reputación puede limitar funciones. Mantiene la justicia y recompensa la contribución real."
851
851
  },
852
+ "name": {
853
+ "title": "¿Cuál es tu nombre?",
854
+ "body": "Añade tu nombre para que la gente sepa quién eres.",
855
+ "firstLabel": "Nombre",
856
+ "firstPlaceholder": "Tu nombre",
857
+ "lastLabel": "Apellidos",
858
+ "lastPlaceholder": "Tus apellidos",
859
+ "saveFailed": "No se pudo guardar tu nombre"
860
+ },
852
861
  "avatar": {
853
862
  "title": "Hazlo tuyo",
854
863
  "body": "Añade un avatar para que te reconozcan. Se mostrará donde aparezcas aquí. Puedes omitirlo — puedes añadirlo más tarde.",
@@ -129,6 +129,15 @@
129
129
  "title": "Reputation = Trust & Growth",
130
130
  "body": "Oxy Trust is a reputation system that reacts to what you do. Helpful, respectful, constructive actions earn it. Harmful or low‑effort stuff chips it away. More reputation can unlock benefits; low reputation can limit features. It keeps things fair and rewards real contribution."
131
131
  },
132
+ "name": {
133
+ "title": "What's your name?",
134
+ "body": "Add your name so people know who you are.",
135
+ "firstLabel": "First name",
136
+ "firstPlaceholder": "Your first name",
137
+ "lastLabel": "Last name",
138
+ "lastPlaceholder": "Your last name",
139
+ "saveFailed": "Could not save your name"
140
+ },
132
141
  "avatar": {
133
142
  "title": "Make It Yours",
134
143
  "body": "Add an avatar so people recognize you. It will show anywhere you show up here. Skip if you want — you can add it later.",
@@ -849,6 +849,15 @@
849
849
  "title": "Reputación = Confianza y crecimiento",
850
850
  "body": "Oxy Trust es un sistema de reputación que reacciona a lo que haces. Las acciones útiles, respetuosas y constructivas la aumentan. Las acciones dañinas o de poco esfuerzo la reducen. Más reputación puede desbloquear beneficios; poca reputación puede limitar funciones. Mantiene la justicia y recompensa la contribución real."
851
851
  },
852
+ "name": {
853
+ "title": "¿Cuál es tu nombre?",
854
+ "body": "Añade tu nombre para que la gente sepa quién eres.",
855
+ "firstLabel": "Nombre",
856
+ "firstPlaceholder": "Tu nombre",
857
+ "lastLabel": "Apellidos",
858
+ "lastPlaceholder": "Tus apellidos",
859
+ "saveFailed": "No se pudo guardar tu nombre"
860
+ },
852
861
  "avatar": {
853
862
  "title": "Hazlo tuyo",
854
863
  "body": "Añade un avatar para que te reconozcan. Se mostrará donde aparezcas aquí. Puedes omitirlo — puedes añadirlo más tarde.",
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.rgbToHex = exports.hexToRgb = exports.lightenColor = 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.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.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.isRequiredNumber = exports.isRequiredString = exports.isValidPassword = 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 = void 0;
23
- exports.isCentralIdPOrigin = exports.buildSsoBounceUrl = 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.MULTIPART_TLDS = 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 = void 0;
24
- exports.packageInfo = exports.runColdBoot = exports.guardActive = 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
  // ---------------------------------------------------------------------------
@@ -242,7 +253,6 @@ Object.defineProperty(exports, "getAccountColor", { enumerable: true, get: funct
242
253
  var fapiAutoDetect_1 = require("./utils/fapiAutoDetect");
243
254
  Object.defineProperty(exports, "autoDetectAuthWebUrl", { enumerable: true, get: function () { return fapiAutoDetect_1.autoDetectAuthWebUrl; } });
244
255
  Object.defineProperty(exports, "registrableApex", { enumerable: true, get: function () { return fapiAutoDetect_1.registrableApex; } });
245
- Object.defineProperty(exports, "MULTIPART_TLDS", { enumerable: true, get: function () { return fapiAutoDetect_1.MULTIPART_TLDS; } });
246
256
  // Central cross-domain SSO (opaque single-use code bounce via auth.oxy.so)
247
257
  var authWebUrl_1 = require("./utils/authWebUrl");
248
258
  Object.defineProperty(exports, "CENTRAL_AUTH_URL", { enumerable: true, get: function () { return authWebUrl_1.CENTRAL_AUTH_URL; } });