@oxyhq/core 1.11.12 → 1.11.14

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 (130) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/CrossDomainAuth.js +3 -1
  3. package/dist/cjs/HttpService.js +214 -33
  4. package/dist/cjs/OxyServices.base.js +9 -0
  5. package/dist/cjs/OxyServices.js +8 -3
  6. package/dist/cjs/crypto/index.js +3 -1
  7. package/dist/cjs/crypto/keyManager.js +476 -172
  8. package/dist/cjs/crypto/polyfill.js +14 -65
  9. package/dist/cjs/crypto/recoveryPhrase.js +30 -11
  10. package/dist/cjs/crypto/signatureService.js +25 -60
  11. package/dist/cjs/i18n/locales/en-US.json +46 -1
  12. package/dist/cjs/i18n/locales/es-ES.json +46 -1
  13. package/dist/cjs/i18n/locales/locales/en-US.json +46 -1
  14. package/dist/cjs/i18n/locales/locales/es-ES.json +46 -1
  15. package/dist/cjs/index.js +10 -2
  16. package/dist/cjs/mixins/OxyServices.assets.js +9 -4
  17. package/dist/cjs/mixins/OxyServices.auth.js +147 -14
  18. package/dist/cjs/mixins/OxyServices.contacts.js +50 -0
  19. package/dist/cjs/mixins/OxyServices.features.js +0 -11
  20. package/dist/cjs/mixins/OxyServices.fedcm.js +4 -3
  21. package/dist/cjs/mixins/OxyServices.language.js +5 -36
  22. package/dist/cjs/mixins/OxyServices.redirect.js +6 -2
  23. package/dist/cjs/mixins/OxyServices.security.js +13 -2
  24. package/dist/cjs/mixins/OxyServices.user.js +59 -38
  25. package/dist/cjs/mixins/OxyServices.utility.js +416 -110
  26. package/dist/cjs/mixins/index.js +11 -3
  27. package/dist/cjs/utils/accountUtils.js +71 -2
  28. package/dist/cjs/utils/deviceManager.js +5 -36
  29. package/dist/cjs/utils/languageUtils.js +22 -0
  30. package/dist/cjs/utils/platformCrypto.js +165 -0
  31. package/dist/cjs/utils/platformCrypto.native.js +123 -0
  32. package/dist/esm/.tsbuildinfo +1 -1
  33. package/dist/esm/CrossDomainAuth.js +3 -1
  34. package/dist/esm/HttpService.js +215 -34
  35. package/dist/esm/OxyServices.base.js +9 -0
  36. package/dist/esm/OxyServices.js +8 -3
  37. package/dist/esm/crypto/index.js +1 -1
  38. package/dist/esm/crypto/keyManager.js +473 -138
  39. package/dist/esm/crypto/polyfill.js +14 -32
  40. package/dist/esm/crypto/recoveryPhrase.js +30 -11
  41. package/dist/esm/crypto/signatureService.js +25 -27
  42. package/dist/esm/i18n/locales/en-US.json +46 -1
  43. package/dist/esm/i18n/locales/es-ES.json +46 -1
  44. package/dist/esm/i18n/locales/locales/en-US.json +46 -1
  45. package/dist/esm/i18n/locales/locales/es-ES.json +46 -1
  46. package/dist/esm/index.js +4 -3
  47. package/dist/esm/mixins/OxyServices.assets.js +9 -4
  48. package/dist/esm/mixins/OxyServices.auth.js +145 -14
  49. package/dist/esm/mixins/OxyServices.contacts.js +47 -0
  50. package/dist/esm/mixins/OxyServices.features.js +0 -11
  51. package/dist/esm/mixins/OxyServices.fedcm.js +4 -3
  52. package/dist/esm/mixins/OxyServices.language.js +5 -3
  53. package/dist/esm/mixins/OxyServices.redirect.js +6 -2
  54. package/dist/esm/mixins/OxyServices.security.js +13 -2
  55. package/dist/esm/mixins/OxyServices.user.js +59 -38
  56. package/dist/esm/mixins/OxyServices.utility.js +416 -77
  57. package/dist/esm/mixins/index.js +11 -3
  58. package/dist/esm/utils/accountUtils.js +67 -1
  59. package/dist/esm/utils/deviceManager.js +5 -3
  60. package/dist/esm/utils/languageUtils.js +21 -0
  61. package/dist/esm/utils/platformCrypto.js +125 -0
  62. package/dist/esm/utils/platformCrypto.native.js +80 -0
  63. package/dist/types/.tsbuildinfo +1 -1
  64. package/dist/types/HttpService.d.ts +47 -3
  65. package/dist/types/OxyServices.base.d.ts +7 -0
  66. package/dist/types/OxyServices.d.ts +50 -7
  67. package/dist/types/crypto/index.d.ts +1 -1
  68. package/dist/types/crypto/keyManager.d.ts +110 -9
  69. package/dist/types/crypto/polyfill.d.ts +3 -1
  70. package/dist/types/crypto/recoveryPhrase.d.ts +31 -7
  71. package/dist/types/crypto/signatureService.d.ts +4 -0
  72. package/dist/types/index.d.ts +7 -5
  73. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  74. package/dist/types/mixins/OxyServices.assets.d.ts +6 -10
  75. package/dist/types/mixins/OxyServices.auth.d.ts +82 -5
  76. package/dist/types/mixins/OxyServices.contacts.d.ts +99 -0
  77. package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
  78. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  79. package/dist/types/mixins/OxyServices.features.d.ts +2 -7
  80. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
  81. package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
  82. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  83. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  84. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
  85. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  86. package/dist/types/mixins/OxyServices.popup.d.ts +1 -0
  87. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  88. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  89. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  90. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  91. package/dist/types/mixins/OxyServices.user.d.ts +28 -11
  92. package/dist/types/mixins/OxyServices.utility.d.ts +145 -10
  93. package/dist/types/mixins/index.d.ts +52 -4
  94. package/dist/types/models/interfaces.d.ts +62 -3
  95. package/dist/types/utils/accountUtils.d.ts +41 -1
  96. package/dist/types/utils/languageUtils.d.ts +1 -0
  97. package/dist/types/utils/platformCrypto.d.ts +87 -0
  98. package/dist/types/utils/platformCrypto.native.d.ts +54 -0
  99. package/package.json +45 -2
  100. package/src/CrossDomainAuth.ts +12 -10
  101. package/src/HttpService.ts +251 -40
  102. package/src/OxyServices.base.ts +10 -0
  103. package/src/OxyServices.ts +26 -7
  104. package/src/crypto/__tests__/keyManager.test.ts +336 -0
  105. package/src/crypto/index.ts +6 -1
  106. package/src/crypto/keyManager.ts +529 -151
  107. package/src/crypto/polyfill.ts +14 -34
  108. package/src/crypto/recoveryPhrase.ts +56 -17
  109. package/src/crypto/signatureService.ts +25 -30
  110. package/src/i18n/locales/en-US.json +46 -1
  111. package/src/i18n/locales/es-ES.json +46 -1
  112. package/src/index.ts +19 -4
  113. package/src/mixins/OxyServices.assets.ts +15 -11
  114. package/src/mixins/OxyServices.auth.ts +175 -15
  115. package/src/mixins/OxyServices.contacts.ts +73 -0
  116. package/src/mixins/OxyServices.features.ts +2 -12
  117. package/src/mixins/OxyServices.fedcm.ts +4 -3
  118. package/src/mixins/OxyServices.language.ts +6 -4
  119. package/src/mixins/OxyServices.redirect.ts +6 -2
  120. package/src/mixins/OxyServices.security.ts +18 -8
  121. package/src/mixins/OxyServices.user.ts +72 -49
  122. package/src/mixins/OxyServices.utility.ts +562 -89
  123. package/src/mixins/__tests__/serviceAuth.test.ts +623 -0
  124. package/src/mixins/index.ts +58 -7
  125. package/src/models/interfaces.ts +65 -3
  126. package/src/utils/accountUtils.ts +82 -2
  127. package/src/utils/deviceManager.ts +7 -4
  128. package/src/utils/languageUtils.ts +23 -2
  129. package/src/utils/platformCrypto.native.ts +101 -0
  130. package/src/utils/platformCrypto.ts +145 -0
@@ -5,10 +5,13 @@
5
5
  * across all platforms (Node.js, Browser, React Native).
6
6
  *
7
7
  * - Browser/Node.js: Uses native crypto
8
- * - React Native: Falls back to expo-crypto if native crypto unavailable
8
+ * - React Native: Uses expo-crypto (statically imported via the
9
+ * per-platform `platformCrypto` module — see that file's doc-comment for
10
+ * how platform routing works).
9
11
  */
10
12
  import _cjs_buffer from 'buffer';
11
13
  const { Buffer } = _cjs_buffer;
14
+ import { getRandomBytesRN } from '../utils/platformCrypto.js';
12
15
  const getGlobalObject = () => {
13
16
  if (typeof globalThis !== 'undefined')
14
17
  return globalThis;
@@ -25,37 +28,19 @@ const globalObject = getGlobalObject();
25
28
  if (!globalObject.Buffer) {
26
29
  globalObject.Buffer = Buffer;
27
30
  }
28
- // Cache for expo-crypto module (lazy loaded only in React Native)
29
- let expoCryptoModule = null;
30
- let expoCryptoLoadPromise = null;
31
31
  /**
32
- * Eagerly start loading expo-crypto. The module is cached once resolved so
33
- * the synchronous getRandomValues shim can read from it immediately.
34
- * Uses dynamic import with variable indirection to prevent ESM bundlers
35
- * (Vite, webpack) from statically resolving the specifier.
32
+ * Synchronous random-bytes shim. On RN, this delegates to
33
+ * `expo-crypto.getRandomBytes` (statically imported by the RN variant of
34
+ * `platformCrypto`, so available without any async warm-up). On Node /
35
+ * browser, this throws but is never called there because both platforms
36
+ * already provide `globalThis.crypto.getRandomValues` natively.
36
37
  */
37
- function startExpoCryptoLoad() {
38
- if (expoCryptoLoadPromise)
39
- return;
40
- expoCryptoLoadPromise = (async () => {
41
- try {
42
- const moduleName = 'expo-crypto';
43
- expoCryptoModule = await import(/* @vite-ignore */ moduleName);
44
- }
45
- catch {
46
- // expo-crypto not available — expected in non-RN environments
47
- }
48
- })();
49
- }
50
38
  function getRandomBytesSync(byteCount) {
51
- // Kick off loading if not already started (should have been started at module init)
52
- startExpoCryptoLoad();
53
- if (expoCryptoModule) {
54
- return expoCryptoModule.getRandomBytes(byteCount);
55
- }
56
- throw new Error('No crypto.getRandomValues implementation available. ' +
57
- 'In React Native, install expo-crypto. ' +
58
- 'If expo-crypto is installed, ensure the polyfill module is imported early enough for the async load to complete.');
39
+ // `getRandomBytesRN` throws on non-RN platforms. That's fine: this
40
+ // function is only ever called as a fallback when the native
41
+ // `globalThis.crypto.getRandomValues` is missing, which on a normal
42
+ // Node/browser host never happens.
43
+ return getRandomBytesRN(byteCount);
59
44
  }
60
45
  const cryptoPolyfill = {
61
46
  getRandomValues(array) {
@@ -67,12 +52,9 @@ const cryptoPolyfill = {
67
52
  };
68
53
  // Only polyfill if crypto or crypto.getRandomValues is not available
69
54
  if (typeof globalObject.crypto === 'undefined') {
70
- // Start loading expo-crypto eagerly so it is ready by the time getRandomValues is called
71
- startExpoCryptoLoad();
72
55
  globalObject.crypto = cryptoPolyfill;
73
56
  }
74
57
  else if (typeof globalObject.crypto.getRandomValues !== 'function') {
75
- startExpoCryptoLoad();
76
58
  globalObject.crypto.getRandomValues = cryptoPolyfill.getRandomValues;
77
59
  }
78
60
  export { Buffer };
@@ -21,10 +21,15 @@ function toHex(data) {
21
21
  }
22
22
  export class RecoveryPhraseService {
23
23
  /**
24
- * Generate a new identity with a recovery phrase
25
- * Returns the mnemonic phrase (should only be shown once to the user)
24
+ * Generate a new identity with a recovery phrase.
25
+ * The mnemonic phrase MUST be shown to the user exactly once after this
26
+ * call resolves — if it is lost, the account becomes unrecoverable.
27
+ *
28
+ * Refuses to overwrite an existing identity unless `options.overwrite === true`.
29
+ *
30
+ * @throws IdentityAlreadyExistsError if an identity already exists and overwrite is not set
26
31
  */
27
- static async generateIdentityWithRecovery() {
32
+ static async generateIdentityWithRecovery(options) {
28
33
  // Generate 128-bit entropy for 12-word mnemonic
29
34
  const mnemonic = bip39.generateMnemonic(128);
30
35
  // Derive private key from mnemonic
@@ -33,8 +38,11 @@ export class RecoveryPhraseService {
33
38
  // Use first 32 bytes of seed as private key
34
39
  const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
35
40
  const privateKeyHex = toHex(seedSlice);
36
- // Import the derived key pair
37
- const publicKey = await KeyManager.importKeyPair(privateKeyHex);
41
+ // Import the derived key pair. KeyManager.importKeyPair will refuse to
42
+ // clobber an existing identity unless overwrite is explicitly requested.
43
+ const publicKey = await KeyManager.importKeyPair(privateKeyHex, {
44
+ overwrite: options?.overwrite === true,
45
+ });
38
46
  return {
39
47
  phrase: mnemonic,
40
48
  words: mnemonic.split(' '),
@@ -42,15 +50,19 @@ export class RecoveryPhraseService {
42
50
  };
43
51
  }
44
52
  /**
45
- * Generate a 24-word recovery phrase for higher security
53
+ * Generate a 24-word recovery phrase for higher security.
54
+ *
55
+ * Same overwrite-protection semantics as `generateIdentityWithRecovery`.
46
56
  */
47
- static async generateIdentityWithRecovery24() {
57
+ static async generateIdentityWithRecovery24(options) {
48
58
  // Generate 256-bit entropy for 24-word mnemonic
49
59
  const mnemonic = bip39.generateMnemonic(256);
50
60
  const seed = await bip39.mnemonicToSeed(mnemonic);
51
61
  const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
52
62
  const privateKeyHex = toHex(seedSlice);
53
- const publicKey = await KeyManager.importKeyPair(privateKeyHex);
63
+ const publicKey = await KeyManager.importKeyPair(privateKeyHex, {
64
+ overwrite: options?.overwrite === true,
65
+ });
54
66
  return {
55
67
  phrase: mnemonic,
56
68
  words: mnemonic.split(' '),
@@ -58,9 +70,14 @@ export class RecoveryPhraseService {
58
70
  };
59
71
  }
60
72
  /**
61
- * Restore an identity from a recovery phrase
73
+ * Restore an identity from a recovery phrase.
74
+ *
75
+ * Refuses to overwrite a DIFFERENT existing identity unless
76
+ * `options.overwrite === true`. Re-importing the same phrase that
77
+ * matches the current identity is always allowed (it's a no-op refresh
78
+ * of the backup record).
62
79
  */
63
- static async restoreFromPhrase(phrase) {
80
+ static async restoreFromPhrase(phrase, options) {
64
81
  // Normalize and validate the phrase
65
82
  const normalizedPhrase = phrase.trim().toLowerCase();
66
83
  if (!bip39.validateMnemonic(normalizedPhrase)) {
@@ -71,7 +88,9 @@ export class RecoveryPhraseService {
71
88
  const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
72
89
  const privateKeyHex = toHex(seedSlice);
73
90
  // Import and store the key pair
74
- const publicKey = await KeyManager.importKeyPair(privateKeyHex);
91
+ const publicKey = await KeyManager.importKeyPair(privateKeyHex, {
92
+ overwrite: options?.overwrite === true,
93
+ });
75
94
  return publicKey;
76
95
  }
77
96
  /**
@@ -8,40 +8,27 @@ import _cjs_elliptic from 'elliptic';
8
8
  const { ec: EC } = _cjs_elliptic;
9
9
  import { KeyManager } from './keyManager.js';
10
10
  import { isReactNative, isNodeJS } from '../utils/platform.js';
11
- // Lazy imports for platform-specific crypto
12
- let ExpoCrypto = null;
13
- let NodeCrypto = null;
11
+ import { loadExpoCrypto, loadNodeCrypto } from '../utils/platformCrypto.js';
12
+ import { logger } from '../utils/loggerUtils.js';
13
+ import { isDev } from '../shared/utils/debugUtils.js';
14
14
  const ec = new EC('secp256k1');
15
- async function initExpoCrypto() {
16
- if (!ExpoCrypto) {
17
- const moduleName = 'expo-crypto';
18
- ExpoCrypto = await import(/* @vite-ignore */ moduleName);
19
- }
20
- return ExpoCrypto;
21
- }
22
- async function initNodeCrypto() {
23
- if (!NodeCrypto) {
24
- const moduleName = 'crypto';
25
- NodeCrypto = await import(/* @vite-ignore */ moduleName);
26
- }
27
- return NodeCrypto;
28
- }
29
15
  /**
30
16
  * Compute SHA-256 hash of a string
31
17
  */
32
18
  async function sha256(message) {
33
19
  // In React Native, use expo-crypto
34
20
  if (isReactNative()) {
35
- const Crypto = await initExpoCrypto();
21
+ const Crypto = await loadExpoCrypto();
36
22
  return Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, message);
37
23
  }
38
24
  if (isNodeJS()) {
39
25
  try {
40
- const nodeCrypto = await initNodeCrypto();
26
+ const nodeCrypto = await loadNodeCrypto();
41
27
  return nodeCrypto.createHash('sha256').update(message).digest('hex');
42
28
  }
43
- catch {
44
- // Fall through to Web Crypto API
29
+ catch (error) {
30
+ // Node crypto failed to load — log and fall through to Web Crypto API
31
+ logger.warn('[oxy.crypto] Node crypto unavailable, falling back to Web Crypto', { component: 'SignatureService' }, error);
45
32
  }
46
33
  }
47
34
  // Browser: use Web Crypto API
@@ -59,7 +46,7 @@ export class SignatureService {
59
46
  static async generateChallenge() {
60
47
  // In React Native, use expo-crypto
61
48
  if (isReactNative()) {
62
- const Crypto = await initExpoCrypto();
49
+ const Crypto = await loadExpoCrypto();
63
50
  const randomBytes = await Crypto.getRandomBytesAsync(32);
64
51
  return Array.from(new Uint8Array(randomBytes))
65
52
  .map((b) => b.toString(16).padStart(2, '0'))
@@ -67,11 +54,12 @@ export class SignatureService {
67
54
  }
68
55
  if (isNodeJS()) {
69
56
  try {
70
- const nodeCrypto = await initNodeCrypto();
57
+ const nodeCrypto = await loadNodeCrypto();
71
58
  return nodeCrypto.randomBytes(32).toString('hex');
72
59
  }
73
- catch {
74
- // Fall through to Web Crypto API
60
+ catch (error) {
61
+ // Node crypto failed to load — log and fall through to Web Crypto API
62
+ logger.warn('[oxy.crypto] Node crypto unavailable, falling back to Web Crypto', { component: 'SignatureService' }, error);
75
63
  }
76
64
  }
77
65
  // Browser: use Web Crypto API
@@ -112,6 +100,10 @@ export class SignatureService {
112
100
  }
113
101
  /**
114
102
  * Verify a signature against a message and public key
103
+ *
104
+ * Returns false on any error (invalid signature, malformed input, etc.).
105
+ * Errors are logged at debug level so they're available when troubleshooting
106
+ * signature mismatches but don't surface to the caller.
115
107
  */
116
108
  static async verify(message, signature, publicKey) {
117
109
  try {
@@ -119,7 +111,10 @@ export class SignatureService {
119
111
  const messageHash = await sha256(message);
120
112
  return key.verify(messageHash, signature);
121
113
  }
122
- catch {
114
+ catch (error) {
115
+ if (isDev()) {
116
+ logger.debug('[oxy.crypto] verify() returned false', { component: 'SignatureService' }, error);
117
+ }
123
118
  return false;
124
119
  }
125
120
  }
@@ -145,7 +140,10 @@ export class SignatureService {
145
140
  const messageHash = crypto.createHash('sha256').update(message).digest('hex');
146
141
  return key.verify(messageHash, signature);
147
142
  }
148
- catch {
143
+ catch (error) {
144
+ if (isDev()) {
145
+ logger.debug('[oxy.crypto] verifySync() returned false', { component: 'SignatureService' }, error);
146
+ }
149
147
  return false;
150
148
  }
151
149
  }
@@ -655,6 +655,21 @@
655
655
  "confirms": {
656
656
  "removeAvatar": "Remove your profile picture?"
657
657
  },
658
+ "crop": {
659
+ "title": "Crop avatar",
660
+ "subtitle": "Pinch to zoom, drag to position",
661
+ "noImage": "No image to crop",
662
+ "reset": "Reset",
663
+ "resetToCenter": "Reset to center",
664
+ "confirm": "Use photo",
665
+ "cancel": "Cancel",
666
+ "saving": "Saving…",
667
+ "helper": "The cropped circle is what will appear on your profile. Pinch to zoom, drag to position.",
668
+ "zoom": "{{value}}×",
669
+ "a11yImage": "Crop preview. Pinch to zoom and drag to reposition the image.",
670
+ "a11yReset": "Reset crop to default position",
671
+ "a11yResetAnnouncement": "Crop reset"
672
+ },
658
673
  "toasts": {
659
674
  "profileUpdated": "Profile updated successfully",
660
675
  "updateFailed": "Failed to update profile",
@@ -664,7 +679,10 @@
664
679
  "avatarSelected": "Avatar selected",
665
680
  "avatarUpdated": "Avatar updated",
666
681
  "updateAvatarFailed": "Failed to update avatar",
667
- "noActiveSession": "No active session"
682
+ "noActiveSession": "No active session",
683
+ "cropMeasureFailed": "Could not measure the image",
684
+ "cropNotReady": "Image not ready yet",
685
+ "cropFailed": "Failed to crop image"
668
686
  }
669
687
  },
670
688
  "accountOverview": {
@@ -1046,6 +1064,8 @@
1046
1064
  "save": "Save",
1047
1065
  "saved": "Saved successfully",
1048
1066
  "saving": "Saving...",
1067
+ "unnamed": "Unnamed",
1068
+ "accountFallback": "Account {{handle}}",
1049
1069
  "links": {
1050
1070
  "recoverAccount": "Recover your account",
1051
1071
  "signUp": "Sign Up"
@@ -1308,9 +1328,34 @@
1308
1328
  "loadingMore": "Loading more...",
1309
1329
  "loadingPhotoLayout": "Loading photo layout...",
1310
1330
  "uploading": "Uploading",
1331
+ "upload": "Upload",
1332
+ "uploadPhoto": "Upload Photo",
1311
1333
  "uploadPhotos": "Upload Photos",
1312
1334
  "uploadFiles": "Upload Files",
1313
1335
  "clearSearch": "Clear Search",
1336
+ "choosePhoto": "Choose Photo",
1337
+ "done": "Done",
1338
+ "doneWithCount": "Done ({{count}})",
1339
+ "photoPicker": {
1340
+ "emptyTitle": "No photos yet",
1341
+ "emptySubtitle": "Upload from your device to get started"
1342
+ },
1343
+ "a11y": {
1344
+ "viewAll": "Show all files",
1345
+ "viewPhotos": "Show photos only",
1346
+ "viewVideos": "Show videos only",
1347
+ "viewDocuments": "Show documents only",
1348
+ "viewAudio": "Show audio only",
1349
+ "sortBy": "Sort by {{field}}, {{order}}",
1350
+ "uploadFile": "Upload file from device",
1351
+ "uploadFromDevice": "Upload photo from device",
1352
+ "photoCellSelected": "Photo {{name}}, selected",
1353
+ "photoCellUnselected": "Photo {{name}}, not selected",
1354
+ "selectionCount": "{{count}} photo selected",
1355
+ "selectionCount_plural": "{{count}} photos selected",
1356
+ "cancelPicker": "Cancel photo selection",
1357
+ "confirmSelection": "Confirm selection"
1358
+ },
1314
1359
  "emptyPhotos": {
1315
1360
  "title": "No Photos Yet",
1316
1361
  "ownDescription": "Upload photos to get started. You can select multiple photos at once.",
@@ -241,6 +241,21 @@
241
241
  "confirms": {
242
242
  "removeAvatar": "¿Eliminar tu foto de perfil?"
243
243
  },
244
+ "crop": {
245
+ "title": "Recortar avatar",
246
+ "subtitle": "Pellizca para acercar, arrastra para colocar",
247
+ "noImage": "No hay imagen para recortar",
248
+ "reset": "Restablecer",
249
+ "resetToCenter": "Restablecer al centro",
250
+ "confirm": "Usar foto",
251
+ "cancel": "Cancelar",
252
+ "saving": "Guardando…",
253
+ "helper": "El círculo recortado es lo que aparecerá en tu perfil. Pellizca para acercar, arrastra para colocar.",
254
+ "zoom": "{{value}}×",
255
+ "a11yImage": "Vista previa del recorte. Pellizca para acercar y arrastra para reposicionar la imagen.",
256
+ "a11yReset": "Restablecer el recorte a la posición predeterminada",
257
+ "a11yResetAnnouncement": "Recorte restablecido"
258
+ },
244
259
  "toasts": {
245
260
  "profileUpdated": "Perfil actualizado correctamente",
246
261
  "updateFailed": "Error al actualizar el perfil",
@@ -250,7 +265,10 @@
250
265
  "avatarSelected": "Avatar seleccionado",
251
266
  "avatarUpdated": "Avatar actualizado",
252
267
  "updateAvatarFailed": "Error al actualizar el avatar",
253
- "noActiveSession": "No hay sesión activa"
268
+ "noActiveSession": "No hay sesión activa",
269
+ "cropMeasureFailed": "No se pudo medir la imagen",
270
+ "cropNotReady": "La imagen aún no está lista",
271
+ "cropFailed": "No se pudo recortar la imagen"
254
272
  }
255
273
  },
256
274
  "common": {
@@ -270,6 +288,8 @@
270
288
  "save": "Guardar",
271
289
  "saved": "Guardado correctamente",
272
290
  "saving": "Guardando...",
291
+ "unnamed": "Sin nombre",
292
+ "accountFallback": "Cuenta {{handle}}",
273
293
  "links": {
274
294
  "recoverAccount": "Recuperar tu cuenta",
275
295
  "signUp": "Registrarse"
@@ -1296,9 +1316,34 @@
1296
1316
  "loadingMore": "Cargando más...",
1297
1317
  "loadingPhotoLayout": "Cargando diseño de fotos...",
1298
1318
  "uploading": "Subiendo",
1319
+ "upload": "Subir",
1320
+ "uploadPhoto": "Subir foto",
1299
1321
  "uploadPhotos": "Subir fotos",
1300
1322
  "uploadFiles": "Subir archivos",
1301
1323
  "clearSearch": "Limpiar búsqueda",
1324
+ "choosePhoto": "Elegir foto",
1325
+ "done": "Listo",
1326
+ "doneWithCount": "Listo ({{count}})",
1327
+ "photoPicker": {
1328
+ "emptyTitle": "Aún no hay fotos",
1329
+ "emptySubtitle": "Sube desde tu dispositivo para empezar"
1330
+ },
1331
+ "a11y": {
1332
+ "viewAll": "Mostrar todos los archivos",
1333
+ "viewPhotos": "Mostrar solo fotos",
1334
+ "viewVideos": "Mostrar solo vídeos",
1335
+ "viewDocuments": "Mostrar solo documentos",
1336
+ "viewAudio": "Mostrar solo audio",
1337
+ "sortBy": "Ordenar por {{field}}, {{order}}",
1338
+ "uploadFile": "Subir archivo desde el dispositivo",
1339
+ "uploadFromDevice": "Subir foto desde el dispositivo",
1340
+ "photoCellSelected": "Foto {{name}}, seleccionada",
1341
+ "photoCellUnselected": "Foto {{name}}, no seleccionada",
1342
+ "selectionCount": "{{count}} foto seleccionada",
1343
+ "selectionCount_plural": "{{count}} fotos seleccionadas",
1344
+ "cancelPicker": "Cancelar selección de foto",
1345
+ "confirmSelection": "Confirmar selección"
1346
+ },
1302
1347
  "emptyPhotos": {
1303
1348
  "title": "Aún no hay fotos",
1304
1349
  "ownDescription": "Sube fotos para empezar. Puedes seleccionar varias fotos a la vez.",
@@ -655,6 +655,21 @@
655
655
  "confirms": {
656
656
  "removeAvatar": "Remove your profile picture?"
657
657
  },
658
+ "crop": {
659
+ "title": "Crop avatar",
660
+ "subtitle": "Pinch to zoom, drag to position",
661
+ "noImage": "No image to crop",
662
+ "reset": "Reset",
663
+ "resetToCenter": "Reset to center",
664
+ "confirm": "Use photo",
665
+ "cancel": "Cancel",
666
+ "saving": "Saving…",
667
+ "helper": "The cropped circle is what will appear on your profile. Pinch to zoom, drag to position.",
668
+ "zoom": "{{value}}×",
669
+ "a11yImage": "Crop preview. Pinch to zoom and drag to reposition the image.",
670
+ "a11yReset": "Reset crop to default position",
671
+ "a11yResetAnnouncement": "Crop reset"
672
+ },
658
673
  "toasts": {
659
674
  "profileUpdated": "Profile updated successfully",
660
675
  "updateFailed": "Failed to update profile",
@@ -664,7 +679,10 @@
664
679
  "avatarSelected": "Avatar selected",
665
680
  "avatarUpdated": "Avatar updated",
666
681
  "updateAvatarFailed": "Failed to update avatar",
667
- "noActiveSession": "No active session"
682
+ "noActiveSession": "No active session",
683
+ "cropMeasureFailed": "Could not measure the image",
684
+ "cropNotReady": "Image not ready yet",
685
+ "cropFailed": "Failed to crop image"
668
686
  }
669
687
  },
670
688
  "accountOverview": {
@@ -1046,6 +1064,8 @@
1046
1064
  "save": "Save",
1047
1065
  "saved": "Saved successfully",
1048
1066
  "saving": "Saving...",
1067
+ "unnamed": "Unnamed",
1068
+ "accountFallback": "Account {{handle}}",
1049
1069
  "links": {
1050
1070
  "recoverAccount": "Recover your account",
1051
1071
  "signUp": "Sign Up"
@@ -1308,9 +1328,34 @@
1308
1328
  "loadingMore": "Loading more...",
1309
1329
  "loadingPhotoLayout": "Loading photo layout...",
1310
1330
  "uploading": "Uploading",
1331
+ "upload": "Upload",
1332
+ "uploadPhoto": "Upload Photo",
1311
1333
  "uploadPhotos": "Upload Photos",
1312
1334
  "uploadFiles": "Upload Files",
1313
1335
  "clearSearch": "Clear Search",
1336
+ "choosePhoto": "Choose Photo",
1337
+ "done": "Done",
1338
+ "doneWithCount": "Done ({{count}})",
1339
+ "photoPicker": {
1340
+ "emptyTitle": "No photos yet",
1341
+ "emptySubtitle": "Upload from your device to get started"
1342
+ },
1343
+ "a11y": {
1344
+ "viewAll": "Show all files",
1345
+ "viewPhotos": "Show photos only",
1346
+ "viewVideos": "Show videos only",
1347
+ "viewDocuments": "Show documents only",
1348
+ "viewAudio": "Show audio only",
1349
+ "sortBy": "Sort by {{field}}, {{order}}",
1350
+ "uploadFile": "Upload file from device",
1351
+ "uploadFromDevice": "Upload photo from device",
1352
+ "photoCellSelected": "Photo {{name}}, selected",
1353
+ "photoCellUnselected": "Photo {{name}}, not selected",
1354
+ "selectionCount": "{{count}} photo selected",
1355
+ "selectionCount_plural": "{{count}} photos selected",
1356
+ "cancelPicker": "Cancel photo selection",
1357
+ "confirmSelection": "Confirm selection"
1358
+ },
1314
1359
  "emptyPhotos": {
1315
1360
  "title": "No Photos Yet",
1316
1361
  "ownDescription": "Upload photos to get started. You can select multiple photos at once.",
@@ -241,6 +241,21 @@
241
241
  "confirms": {
242
242
  "removeAvatar": "¿Eliminar tu foto de perfil?"
243
243
  },
244
+ "crop": {
245
+ "title": "Recortar avatar",
246
+ "subtitle": "Pellizca para acercar, arrastra para colocar",
247
+ "noImage": "No hay imagen para recortar",
248
+ "reset": "Restablecer",
249
+ "resetToCenter": "Restablecer al centro",
250
+ "confirm": "Usar foto",
251
+ "cancel": "Cancelar",
252
+ "saving": "Guardando…",
253
+ "helper": "El círculo recortado es lo que aparecerá en tu perfil. Pellizca para acercar, arrastra para colocar.",
254
+ "zoom": "{{value}}×",
255
+ "a11yImage": "Vista previa del recorte. Pellizca para acercar y arrastra para reposicionar la imagen.",
256
+ "a11yReset": "Restablecer el recorte a la posición predeterminada",
257
+ "a11yResetAnnouncement": "Recorte restablecido"
258
+ },
244
259
  "toasts": {
245
260
  "profileUpdated": "Perfil actualizado correctamente",
246
261
  "updateFailed": "Error al actualizar el perfil",
@@ -250,7 +265,10 @@
250
265
  "avatarSelected": "Avatar seleccionado",
251
266
  "avatarUpdated": "Avatar actualizado",
252
267
  "updateAvatarFailed": "Error al actualizar el avatar",
253
- "noActiveSession": "No hay sesión activa"
268
+ "noActiveSession": "No hay sesión activa",
269
+ "cropMeasureFailed": "No se pudo medir la imagen",
270
+ "cropNotReady": "La imagen aún no está lista",
271
+ "cropFailed": "No se pudo recortar la imagen"
254
272
  }
255
273
  },
256
274
  "common": {
@@ -270,6 +288,8 @@
270
288
  "save": "Guardar",
271
289
  "saved": "Guardado correctamente",
272
290
  "saving": "Guardando...",
291
+ "unnamed": "Sin nombre",
292
+ "accountFallback": "Cuenta {{handle}}",
273
293
  "links": {
274
294
  "recoverAccount": "Recuperar tu cuenta",
275
295
  "signUp": "Registrarse"
@@ -1296,9 +1316,34 @@
1296
1316
  "loadingMore": "Cargando más...",
1297
1317
  "loadingPhotoLayout": "Cargando diseño de fotos...",
1298
1318
  "uploading": "Subiendo",
1319
+ "upload": "Subir",
1320
+ "uploadPhoto": "Subir foto",
1299
1321
  "uploadPhotos": "Subir fotos",
1300
1322
  "uploadFiles": "Subir archivos",
1301
1323
  "clearSearch": "Limpiar búsqueda",
1324
+ "choosePhoto": "Elegir foto",
1325
+ "done": "Listo",
1326
+ "doneWithCount": "Listo ({{count}})",
1327
+ "photoPicker": {
1328
+ "emptyTitle": "Aún no hay fotos",
1329
+ "emptySubtitle": "Sube desde tu dispositivo para empezar"
1330
+ },
1331
+ "a11y": {
1332
+ "viewAll": "Mostrar todos los archivos",
1333
+ "viewPhotos": "Mostrar solo fotos",
1334
+ "viewVideos": "Mostrar solo vídeos",
1335
+ "viewDocuments": "Mostrar solo documentos",
1336
+ "viewAudio": "Mostrar solo audio",
1337
+ "sortBy": "Ordenar por {{field}}, {{order}}",
1338
+ "uploadFile": "Subir archivo desde el dispositivo",
1339
+ "uploadFromDevice": "Subir foto desde el dispositivo",
1340
+ "photoCellSelected": "Foto {{name}}, seleccionada",
1341
+ "photoCellUnselected": "Foto {{name}}, no seleccionada",
1342
+ "selectionCount": "{{count}} foto seleccionada",
1343
+ "selectionCount_plural": "{{count}} fotos seleccionadas",
1344
+ "cancelPicker": "Cancelar selección de foto",
1345
+ "confirmSelection": "Confirmar selección"
1346
+ },
1302
1347
  "emptyPhotos": {
1303
1348
  "title": "Aún no hay fotos",
1304
1349
  "ownDescription": "Sube fotos para empezar. Puedes seleccionar varias fotos a la vez.",
package/dist/esm/index.js CHANGED
@@ -21,8 +21,9 @@ export { OXY_CLOUD_URL, oxyClient } from './OxyServices.js';
21
21
  // --- Authentication ---
22
22
  export { AuthManager, createAuthManager } from './AuthManager.js';
23
23
  export { CrossDomainAuth, createCrossDomainAuth } from './CrossDomainAuth.js';
24
+ export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth.js';
24
25
  // --- Crypto / Identity ---
25
- export { KeyManager, SignatureService, RecoveryPhraseService } from './crypto/index.js';
26
+ export { KeyManager, SignatureService, RecoveryPhraseService, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto/index.js';
26
27
  // --- Models & Types ---
27
28
  export * from './models/interfaces.js';
28
29
  export * from './models/session.js';
@@ -30,7 +31,7 @@ export { TopicType, TopicSource } from './models/Topic.js';
30
31
  // --- Device Management ---
31
32
  export { DeviceManager } from './utils/deviceManager.js';
32
33
  // --- Language Utilities ---
33
- export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, } from './utils/languageUtils.js';
34
+ export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, isRTLLocale, } from './utils/languageUtils.js';
34
35
  // --- Platform Detection ---
35
36
  export { getPlatformOS, setPlatformOS, isWeb, isNative, isIOS, isAndroid, } from './utils/platform.js';
36
37
  // --- Shared Utilities ---
@@ -56,7 +57,7 @@ export { logger, LogLevel, logAuth, logApi, logSession, logUser, logDevice, logP
56
57
  // --- Avatar Utilities ---
57
58
  export { updateAvatarVisibility } from './utils/avatarUtils.js';
58
59
  // --- Account Utilities ---
59
- export { buildAccountsArray, createQuickAccount } from './utils/accountUtils.js';
60
+ export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccountFallbackHandle, formatPublicKeyHandle, } from './utils/accountUtils.js';
60
61
  // Default export
61
62
  import { OxyServices } from './OxyServices.js';
62
63
  export default OxyServices;
@@ -150,15 +150,20 @@ export function OxyServicesAssetsMixin(Base) {
150
150
  const fileSize = 'size' in file && file.size ? file.size : 0;
151
151
  try {
152
152
  const formData = new FormData();
153
- if ('uri' in file && typeof file.uri === 'string') {
154
- // React Native file descriptor — RN's FormData handles {uri, type, name} natively
153
+ if (typeof File !== 'undefined' && file instanceof File) {
155
154
  formData.append('file', file, fileName);
156
155
  }
157
- else if (file instanceof Blob) {
156
+ else if (typeof Blob !== 'undefined' && file instanceof Blob) {
157
+ formData.append('file', file, fileName);
158
+ }
159
+ else if ('uri' in file && typeof file.uri === 'string') {
160
+ // React Native file descriptor — RN's FormData handles {uri, type, name} natively.
161
+ // It reads the file from disk during the multipart request — no in-JS Blob
162
+ // conversion (which would fail on Hermes for ArrayBuffer-backed Blobs).
158
163
  formData.append('file', file, fileName);
159
164
  }
160
165
  else {
161
- formData.append('file', new Blob([file], { type: 'application/octet-stream' }), fileName);
166
+ throw new Error('Unsupported file input: expected File, Blob, or { uri, type?, name?, size? } descriptor');
162
167
  }
163
168
  if (visibility) {
164
169
  formData.append('visibility', visibility);