@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
@@ -54,17 +54,52 @@ export declare class HttpService {
54
54
  private requestMetrics;
55
55
  constructor(config: OxyConfig);
56
56
  /**
57
- * Robust FormData detection that works in browser and Node.js environments
58
- * Checks multiple conditions to handle different FormData implementations
57
+ * Robust FormData detection that works in browser, React Native, and
58
+ * Node.js polyfill environments.
59
+ *
60
+ * Why we don't use `instanceof FormData` alone:
61
+ * - React Native's FormData is a separate class, not the browser one —
62
+ * `instanceof FormData` is true only inside the JS runtime that
63
+ * instantiated the value (browser-side polyfills also have their own).
64
+ * - The Node.js `form-data` polyfill ships its own constructor.
65
+ *
66
+ * Why we explicitly reject `URLSearchParams`:
67
+ * - `URLSearchParams` ALSO exposes `append` / `get` / `has`, so the
68
+ * duck-type fallback below would have misidentified it as FormData.
69
+ * - We want urlencoded payloads to take the JSON-stringify path so the
70
+ * server receives them as `application/x-www-form-urlencoded` instead
71
+ * of an empty multipart body.
59
72
  */
60
73
  private isFormData;
61
74
  /**
62
75
  * Main request method - handles everything in one place
63
76
  */
64
77
  request<T = unknown>(config: RequestConfig): Promise<T>;
78
+ /**
79
+ * Upload via XMLHttpRequest (React Native FormData workaround).
80
+ *
81
+ * Expo SDK 56's "winter fetch" cannot serialize RN file descriptors
82
+ * (`{uri, type, name}`) — `convertFormDataAsync` rejects them as
83
+ * `Unsupported FormDataPart implementation`. RN's native XHR streams
84
+ * the file from disk correctly, so multipart uploads go through XHR
85
+ * on RN only.
86
+ *
87
+ * Returns a standard `Response` so downstream parsing in `request()`
88
+ * (status checks, 401/403 retries, JSON/blob/text parsing) is identical
89
+ * to the fetch path.
90
+ */
91
+ private uploadViaXHR;
92
+ /**
93
+ * Parse raw header string from `XMLHttpRequest.getAllResponseHeaders()`
94
+ * into a `Headers`-compatible object.
95
+ */
96
+ private static parseXHRHeaders;
65
97
  /**
66
98
  * Generate cache key efficiently
67
- * Uses simple hash for large objects to avoid expensive JSON.stringify
99
+ * Uses a content-addressed hash for large payloads so two requests with
100
+ * the same shape but different values never collide on the same key
101
+ * (which would silently serve stale data — e.g. paginated search results,
102
+ * large object updates).
68
103
  */
69
104
  private generateCacheKey;
70
105
  /**
@@ -104,6 +139,15 @@ export declare class HttpService {
104
139
  getBaseURL(): string;
105
140
  clearCache(): void;
106
141
  clearCacheEntry(key: string): void;
142
+ /**
143
+ * Delete every cache entry whose key starts with `prefix`.
144
+ *
145
+ * Used by mutations that don't know the exact downstream cache keys —
146
+ * e.g. `updateProfile` invalidating all `GET:/session/user/*` entries
147
+ * without having to track every active session ID. Returns the number of
148
+ * deleted entries (for observability in tests).
149
+ */
150
+ clearCacheByPrefix(prefix: string): number;
107
151
  getCacheStats(): {
108
152
  size: number;
109
153
  hits: number;
@@ -45,6 +45,13 @@ export declare class OxyServicesBase {
45
45
  * Clear specific cache entry
46
46
  */
47
47
  clearCacheEntry(key: string): void;
48
+ /**
49
+ * Clear every cache entry whose key starts with `prefix`.
50
+ * Useful for mutations that invalidate a family of GET responses
51
+ * without enumerating each one (e.g. all session-user lookups after
52
+ * a profile update).
53
+ */
54
+ clearCacheByPrefix(prefix: string): number;
48
55
  /**
49
56
  * Get cache statistics
50
57
  */
@@ -63,8 +63,41 @@ import type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
63
63
  import type { PopupAuthOptions } from './mixins/OxyServices.popup';
64
64
  import type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
65
65
  import { composeOxyServices } from './mixins';
66
- declare const OxyServices_base: any;
67
- export declare class OxyServices extends OxyServices_base {
66
+ /**
67
+ * OxyServices - Unified client library for interacting with the Oxy API
68
+ *
69
+ * This class provides all API functionality in one simple, easy-to-use interface.
70
+ *
71
+ * ## Architecture
72
+ * - **HttpService**: Unified HTTP service handling authentication, caching, deduplication, queuing, and retry
73
+ * - **OxyServices**: Provides high-level API methods
74
+ *
75
+ * ## Mixin Composition
76
+ * The class is composed using TypeScript mixins for better code organization:
77
+ * - **Base**: Core infrastructure (HTTP client, request management, error handling)
78
+ * - **Auth**: Authentication and session management
79
+ * - **User**: User profiles, follow, notifications
80
+ * - **Privacy**: Blocked and restricted users
81
+ * - **Language**: Language detection and metadata
82
+ * - **Payment**: Payment processing
83
+ * - **Karma**: Karma system
84
+ * - **Assets**: File upload and asset management
85
+ * - **Developer**: Developer API management
86
+ * - **Location**: Location-based features
87
+ * - **Analytics**: Analytics tracking
88
+ * - **Devices**: Device management
89
+ * - **Utility**: Utility methods and Express middleware
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const oxy = new OxyServices({
94
+ * baseURL: 'https://api.oxy.so',
95
+ * cloudURL: 'https://cloud.oxy.so'
96
+ * });
97
+ * ```
98
+ */
99
+ declare const OxyServicesComposed: import("./mixins").ComposedOxyServicesConstructor;
100
+ export declare class OxyServices extends OxyServicesComposed {
68
101
  constructor(config: OxyConfig);
69
102
  }
70
103
  export interface OxyServices extends InstanceType<ReturnType<typeof composeOxyServices>> {
@@ -79,24 +112,34 @@ export interface OxyServices extends InstanceType<ReturnType<typeof composeOxySe
79
112
  signUpWithRedirect(options?: RedirectAuthOptions): void;
80
113
  auth(options?: {
81
114
  debug?: boolean;
82
- onError?: (error: any) => any;
115
+ onError?: (error: unknown) => unknown;
83
116
  loadUser?: boolean;
84
117
  optional?: boolean;
85
- }): (req: any, res: any, next: any) => Promise<void>;
118
+ jwtSecret?: string;
119
+ expectedIssuer?: string;
120
+ expectedAudience?: string;
121
+ }): (req: unknown, res: unknown, next: (err?: unknown) => void) => Promise<void>;
86
122
  authSocket(options?: {
87
123
  debug?: boolean;
88
- }): (socket: any, next: (err?: Error) => void) => Promise<void>;
124
+ }): (socket: unknown, next: (err?: Error) => void) => Promise<void>;
125
+ serviceAuth(options?: {
126
+ debug?: boolean;
127
+ jwtSecret?: string;
128
+ expectedIssuer?: string;
129
+ expectedAudience?: string;
130
+ }): (req: unknown, res: unknown, next: (err?: unknown) => void) => Promise<void>;
131
+ requireScope(scope: string): (req: unknown, res: unknown, next: (err?: unknown) => void) => void;
89
132
  assetUpdateVisibility(fileId: string, visibility: 'private' | 'public' | 'unlisted'): Promise<unknown>;
90
133
  }
91
134
  export { OxyAuthenticationError, OxyAuthenticationTimeoutError };
92
135
  /**
93
- * Export the default Oxy Cloud URL (for backward compatibility)
136
+ * Default Oxy Cloud URL used when no `cloudURL` is provided to OxyServices.
94
137
  */
95
138
  export declare const OXY_CLOUD_URL = "https://cloud.oxy.so";
96
139
  /**
97
140
  * Export the default Oxy API URL (for documentation)
98
141
  */
99
- export declare const OXY_API_URL: string;
142
+ export declare const OXY_API_URL: any;
100
143
  /**
101
144
  * Pre-configured client instance for easy import
102
145
  * Uses OXY_API_URL as baseURL and OXY_CLOUD_URL as cloudURL
@@ -5,7 +5,7 @@
5
5
  * Handles key generation, secure storage, digital signatures, and recovery phrases.
6
6
  */
7
7
  import './polyfill';
8
- export { KeyManager, type KeyPair } from './keyManager';
8
+ export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, type KeyPair, } from './keyManager';
9
9
  export { SignatureService, type SignedMessage, type AuthChallenge } from './signatureService';
10
10
  export { RecoveryPhraseService, type RecoveryPhraseResult } from './recoveryPhrase';
11
11
  export { KeyManager as default } from './keyManager';
@@ -5,6 +5,31 @@
5
5
  * Private keys are stored securely using expo-secure-store and never leave the device.
6
6
  */
7
7
  import type { ECKeyPair } from 'elliptic';
8
+ /**
9
+ * Thrown when an identity-mutating operation (createIdentity / importKeyPair)
10
+ * is invoked while a valid identity already exists on the device.
11
+ *
12
+ * The local private key IS the user's identity — overwriting it without
13
+ * explicit consent permanently loses access to their account (unless
14
+ * they previously saved their recovery phrase). This error forces callers
15
+ * to make an explicit, audited decision instead of silently clobbering.
16
+ */
17
+ export declare class IdentityAlreadyExistsError extends Error {
18
+ readonly name = "IdentityAlreadyExistsError";
19
+ readonly existingPublicKey: string;
20
+ constructor(existingPublicKey: string);
21
+ }
22
+ /**
23
+ * Thrown when a freshly written identity cannot be read back, parsed, or
24
+ * round-tripped through sign/verify. Indicates a storage failure or
25
+ * corruption that would otherwise silently leave the user with an
26
+ * unusable account.
27
+ */
28
+ export declare class IdentityPersistError extends Error {
29
+ readonly cause?: unknown | undefined;
30
+ readonly name = "IdentityPersistError";
31
+ constructor(message: string, cause?: unknown | undefined);
32
+ }
8
33
  export interface KeyPair {
9
34
  publicKey: string;
10
35
  privateKey: string;
@@ -24,6 +49,19 @@ export declare class KeyManager {
24
49
  * Called internally when shared identity is created/deleted/imported
25
50
  */
26
51
  private static invalidateSharedCache;
52
+ /**
53
+ * Lowercase and pad to canonical 64-hex-char form.
54
+ *
55
+ * Tolerates the 1-in-256 leading-zero-strip that elliptic's
56
+ * `getPrivate('hex')` produces, and the externally-imported uppercase-hex
57
+ * legacy keys. EVERY `ec.keyFromPrivate(...)` call site in this file must
58
+ * canonicalize first so that derivation is stable regardless of storage
59
+ * representation.
60
+ *
61
+ * Private (used only inside KeyManager) — public consumers should not need
62
+ * to think about hex representation.
63
+ */
64
+ private static canonicalPrivateKey;
27
65
  /**
28
66
  * Generate a new ECDSA secp256k1 key pair
29
67
  * Returns the keys in hexadecimal format
@@ -120,14 +158,45 @@ export declare class KeyManager {
120
158
  */
121
159
  static migrateToSharedIdentity(): Promise<boolean>;
122
160
  /**
123
- * Generate and securely store a new key pair on the device
124
- * Returns only the public key (private key is stored securely)
161
+ * Atomically persist a key pair to secure storage with verification + backup.
162
+ *
163
+ * Write order is critical:
164
+ * 1. Backup (BACKUP_PRIVATE_KEY + BACKUP_PUBLIC_KEY + BACKUP_TIMESTAMP)
165
+ * 2. Primary public key
166
+ * 3. Primary private key (last so a partial write leaves us in a known
167
+ * "no identity yet" state — easier to retry than a half-written one)
168
+ * 4. Read back + sign/verify to confirm the storage round-trip works
169
+ *
170
+ * If any step throws, the caller sees the error AND any partial state is
171
+ * cleaned up so the device is left either fully consistent or fully empty.
172
+ * It never leaves an unusable half-identity that would fool `hasIdentity()`.
173
+ *
174
+ * @internal
125
175
  */
126
- static createIdentity(): Promise<string>;
176
+ private static _persistIdentityAtomic;
127
177
  /**
128
- * Import an existing key pair (e.g., from recovery phrase)
178
+ * Generate and securely store a new key pair on the device.
179
+ *
180
+ * Refuses to overwrite an existing identity unless `options.overwrite === true`.
181
+ * Returns the public key. The private key never leaves secure storage.
182
+ *
183
+ * @throws IdentityAlreadyExistsError if an identity already exists and overwrite is not set
184
+ * @throws IdentityPersistError if the key cannot be durably written
129
185
  */
130
- static importKeyPair(privateKey: string): Promise<string>;
186
+ static createIdentity(options?: {
187
+ overwrite?: boolean;
188
+ }): Promise<string>;
189
+ /**
190
+ * Import an existing key pair (e.g., from recovery phrase).
191
+ *
192
+ * Refuses to overwrite an existing identity unless `options.overwrite === true`.
193
+ *
194
+ * @throws IdentityAlreadyExistsError if an identity already exists and overwrite is not set
195
+ * @throws IdentityPersistError if the key cannot be durably written
196
+ */
197
+ static importKeyPair(privateKey: string, options?: {
198
+ overwrite?: boolean;
199
+ }): Promise<string>;
131
200
  /**
132
201
  * Get the stored private key
133
202
  * WARNING: Only use this for signing operations within the app
@@ -138,7 +207,15 @@ export declare class KeyManager {
138
207
  */
139
208
  static getPublicKey(): Promise<string | null>;
140
209
  /**
141
- * Check if an identity (key pair) exists on this device (cached for performance)
210
+ * Check if a complete, parseable identity exists on this device.
211
+ *
212
+ * Returns `true` only when BOTH the private and public keys are present,
213
+ * both are well-formed, AND the public key derives from the private key.
214
+ * A partially-written or corrupted identity returns `false` so that
215
+ * downstream code can resume the create / restore flow correctly.
216
+ *
217
+ * Note: this does NOT perform the full sign/verify roundtrip — call
218
+ * `verifyIdentityIntegrity()` for that.
142
219
  */
143
220
  static hasIdentity(): Promise<boolean>;
144
221
  /**
@@ -156,11 +233,26 @@ export declare class KeyManager {
156
233
  */
157
234
  static backupIdentity(): Promise<boolean>;
158
235
  /**
159
- * Verify identity integrity - checks if keys are valid and accessible
236
+ * Verify identity integrity checks keys are valid, accessible, derive
237
+ * consistently, AND can sign + verify a probe message.
238
+ *
239
+ * Returns true only when the full sign/verify roundtrip succeeds. Use
240
+ * this on app start to detect silent corruption before the user finds
241
+ * out by failing to sign in.
160
242
  */
161
243
  static verifyIdentityIntegrity(): Promise<boolean>;
162
244
  /**
163
- * Restore identity from backup if primary storage is corrupted
245
+ * Restore identity from backup if primary storage is corrupted.
246
+ *
247
+ * SAFETY: this method will NEVER overwrite a verifying primary identity.
248
+ * If the primary passes a sign/verify probe, the backup is left untouched
249
+ * and `false` is returned — this protects against a transient
250
+ * `verifyIdentityIntegrity()` blip clobbering valid keys with stale
251
+ * backup keys (e.g., from a previous account before an import).
252
+ *
253
+ * Additionally, if the backup public key does NOT match the (still-
254
+ * present-but-failing) primary public key, we refuse to overwrite — the
255
+ * backup may belong to a different identity entirely.
164
256
  */
165
257
  static restoreIdentityFromBackup(): Promise<boolean>;
166
258
  /**
@@ -174,10 +266,19 @@ export declare class KeyManager {
174
266
  static derivePublicKey(privateKey: string): string;
175
267
  /**
176
268
  * Validate that a string is a valid public key
269
+ *
270
+ * Returns false on parse errors (invalid input is the expected fail mode here).
271
+ * Errors are logged at debug level so they're available when troubleshooting
272
+ * but don't pollute production logs.
177
273
  */
178
274
  static isValidPublicKey(publicKey: string): boolean;
179
275
  /**
180
- * Validate that a string is a valid private key
276
+ * Validate that a string is a valid private key.
277
+ *
278
+ * secp256k1 private keys are 256-bit, so 64 hex chars. We require strict
279
+ * hex-only input because `elliptic`'s underlying `BN(input, 16)` happily
280
+ * accepts non-hex characters (treating them as zero), which would let
281
+ * "not-hex" pass through as a valid (but compromised, near-zero) key.
181
282
  */
182
283
  static isValidPrivateKey(privateKey: string): boolean;
183
284
  /**
@@ -5,7 +5,9 @@
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 { Buffer } from 'buffer';
11
13
  export { Buffer };
@@ -11,20 +11,44 @@ export interface RecoveryPhraseResult {
11
11
  words: string[];
12
12
  publicKey: string;
13
13
  }
14
+ export interface GenerateIdentityOptions {
15
+ /**
16
+ * Pass `true` to allow overwriting an existing on-device identity.
17
+ *
18
+ * Defaults to `false`. When false, this method throws
19
+ * `IdentityAlreadyExistsError` if a complete identity already exists,
20
+ * preventing accidental account loss. UI flows MUST only set this to
21
+ * `true` after explicitly confirming the user has saved their previous
22
+ * recovery phrase (or has otherwise been warned).
23
+ */
24
+ overwrite?: boolean;
25
+ }
14
26
  export declare class RecoveryPhraseService {
15
27
  /**
16
- * Generate a new identity with a recovery phrase
17
- * Returns the mnemonic phrase (should only be shown once to the user)
28
+ * Generate a new identity with a recovery phrase.
29
+ * The mnemonic phrase MUST be shown to the user exactly once after this
30
+ * call resolves — if it is lost, the account becomes unrecoverable.
31
+ *
32
+ * Refuses to overwrite an existing identity unless `options.overwrite === true`.
33
+ *
34
+ * @throws IdentityAlreadyExistsError if an identity already exists and overwrite is not set
18
35
  */
19
- static generateIdentityWithRecovery(): Promise<RecoveryPhraseResult>;
36
+ static generateIdentityWithRecovery(options?: GenerateIdentityOptions): Promise<RecoveryPhraseResult>;
20
37
  /**
21
- * Generate a 24-word recovery phrase for higher security
38
+ * Generate a 24-word recovery phrase for higher security.
39
+ *
40
+ * Same overwrite-protection semantics as `generateIdentityWithRecovery`.
22
41
  */
23
- static generateIdentityWithRecovery24(): Promise<RecoveryPhraseResult>;
42
+ static generateIdentityWithRecovery24(options?: GenerateIdentityOptions): Promise<RecoveryPhraseResult>;
24
43
  /**
25
- * Restore an identity from a recovery phrase
44
+ * Restore an identity from a recovery phrase.
45
+ *
46
+ * Refuses to overwrite a DIFFERENT existing identity unless
47
+ * `options.overwrite === true`. Re-importing the same phrase that
48
+ * matches the current identity is always allowed (it's a no-op refresh
49
+ * of the backup record).
26
50
  */
27
- static restoreFromPhrase(phrase: string): Promise<string>;
51
+ static restoreFromPhrase(phrase: string, options?: GenerateIdentityOptions): Promise<string>;
28
52
  /**
29
53
  * Validate a recovery phrase without importing it
30
54
  */
@@ -37,6 +37,10 @@ export declare class SignatureService {
37
37
  static signWithKey(message: string, privateKey: string): Promise<string>;
38
38
  /**
39
39
  * Verify a signature against a message and public key
40
+ *
41
+ * Returns false on any error (invalid signature, malformed input, etc.).
42
+ * Errors are logged at debug level so they're available when troubleshooting
43
+ * signature mismatches but don't surface to the caller.
40
44
  */
41
45
  static verify(message: string, signature: string, publicKey: string): Promise<boolean>;
42
46
  /**
@@ -23,10 +23,12 @@ export type { CrossDomainAuthOptions } from './CrossDomainAuth';
23
23
  export type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
24
24
  export type { PopupAuthOptions } from './mixins/OxyServices.popup';
25
25
  export type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
26
+ export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth';
26
27
  export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
27
- export type { ServiceApp } from './mixins/OxyServices.utility';
28
+ export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
28
29
  export type { CreateManagedAccountInput, ManagedAccountManager, ManagedAccount } from './mixins/OxyServices.managedAccounts';
29
- export { KeyManager, SignatureService, RecoveryPhraseService } from './crypto';
30
+ export type { ContactDiscoveryMatch, ContactDiscoveryResponse } from './mixins/OxyServices.contacts';
31
+ export { KeyManager, SignatureService, RecoveryPhraseService, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto';
30
32
  export type { KeyPair, SignedMessage, AuthChallenge, RecoveryPhraseResult } from './crypto';
31
33
  export * from './models/interfaces';
32
34
  export * from './models/session';
@@ -34,7 +36,7 @@ export type { TopicData, TopicTranslation } from './models/Topic';
34
36
  export { TopicType, TopicSource } from './models/Topic';
35
37
  export { DeviceManager } from './utils/deviceManager';
36
38
  export type { DeviceFingerprint, StoredDeviceInfo } from './utils/deviceManager';
37
- export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, } from './utils/languageUtils';
39
+ export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, isRTLLocale, } from './utils/languageUtils';
38
40
  export type { LanguageMetadata } from './utils/languageUtils';
39
41
  export { getPlatformOS, setPlatformOS, isWeb, isNative, isIOS, isAndroid, } from './utils/platform';
40
42
  export type { PlatformOS } from './utils/platform';
@@ -57,7 +59,7 @@ export * from './utils/validationUtils';
57
59
  export { logger, LogLevel, logAuth, logApi, logSession, logUser, logDevice, logPayment, logPerformance, } from './utils/loggerUtils';
58
60
  export type { LogContext } from './utils/loggerUtils';
59
61
  export { updateAvatarVisibility } from './utils/avatarUtils';
60
- export { buildAccountsArray, createQuickAccount } from './utils/accountUtils';
61
- export type { QuickAccount } from './utils/accountUtils';
62
+ export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccountFallbackHandle, formatPublicKeyHandle, } from './utils/accountUtils';
63
+ export type { QuickAccount, DisplayNameUserShape } from './utils/accountUtils';
62
64
  import { OxyServices } from './OxyServices';
63
65
  export default OxyServices;
@@ -36,6 +36,7 @@ export declare function OxyServicesAnalyticsMixin<T extends typeof OxyServicesBa
36
36
  };
37
37
  clearCache(): void;
38
38
  clearCacheEntry(key: string): void;
39
+ clearCacheByPrefix(prefix: string): number;
39
40
  getCacheStats(): {
40
41
  size: number;
41
42
  hits: number;
@@ -1,4 +1,4 @@
1
- import type { AccountStorageUsageResponse, AssetUrlResponse, AssetVariant } from '../models/interfaces';
1
+ import type { AccountStorageUsageResponse, AssetUploadInput, AssetUrlResponse, AssetVariant } from '../models/interfaces';
2
2
  import type { OxyServicesBase } from '../OxyServices.base';
3
3
  export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T): {
4
4
  new (...args: any[]): {
@@ -45,7 +45,7 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
45
45
  /**
46
46
  * Upload raw file data
47
47
  */
48
- uploadRawFile(file: File | Blob, visibility?: "private" | "public" | "unlisted", metadata?: Record<string, any>): Promise<any>;
48
+ uploadRawFile(file: AssetUploadInput, visibility?: "private" | "public" | "unlisted", metadata?: Record<string, any>): Promise<any>;
49
49
  /**
50
50
  * Upload file using Central Asset Service.
51
51
  *
@@ -53,12 +53,7 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
53
53
  * ({uri, type, name, size}). RN descriptors are passed directly to
54
54
  * FormData.append, which handles them natively.
55
55
  */
56
- assetUpload(file: File | {
57
- uri: string;
58
- type?: string;
59
- name?: string;
60
- size?: number;
61
- }, visibility?: "private" | "public" | "unlisted", metadata?: Record<string, any>, onProgress?: (progress: number) => void): Promise<any>;
56
+ assetUpload(file: AssetUploadInput, visibility?: "private" | "public" | "unlisted", metadata?: Record<string, any>, onProgress?: (progress: number) => void): Promise<any>;
62
57
  /**
63
58
  * Link asset to an entity
64
59
  */
@@ -91,8 +86,8 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
91
86
  * Update asset visibility
92
87
  */
93
88
  assetUpdateVisibility(fileId: string, visibility: "private" | "public" | "unlisted"): Promise<any>;
94
- uploadAvatar(file: File, userId: string, app?: string): Promise<any>;
95
- uploadProfileBanner(file: File, userId: string, app?: string): Promise<any>;
89
+ uploadAvatar(file: AssetUploadInput, userId: string, app?: string): Promise<any>;
90
+ uploadProfileBanner(file: AssetUploadInput, userId: string, app?: string): Promise<any>;
96
91
  getAssetUrlCacheTTL(expiresIn?: number): number;
97
92
  fetchAssetDownloadUrl(fileId: string, variant?: string, cacheTTL?: number, expiresIn?: number): Promise<string | null>;
98
93
  fetchAssetContent(url: string, type: "text"): Promise<string>;
@@ -114,6 +109,7 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
114
109
  };
115
110
  clearCache(): void;
116
111
  clearCacheEntry(key: string): void;
112
+ clearCacheByPrefix(prefix: string): number;
117
113
  getCacheStats(): {
118
114
  size: number;
119
115
  hits: number;
@@ -34,29 +34,104 @@ export interface ServiceTokenResponse {
34
34
  expiresIn: number;
35
35
  appName: string;
36
36
  }
37
+ /**
38
+ * One cache entry per (apiKey hash) → issued token + the secret that produced it.
39
+ * The secret is kept around in raw Buffer form so we can perform a
40
+ * constant-time compare against any reused credential pair — this prevents an
41
+ * attacker who learned a victim's apiKey from receiving the victim's cached
42
+ * service token by simply guessing the secret.
43
+ *
44
+ * @internal
45
+ */
46
+ interface ServiceTokenCacheEntry {
47
+ token: string;
48
+ /** Expiry as ms since epoch */
49
+ expiresAt: number;
50
+ /** Raw secret stored as Buffer for constant-time comparison on cache hit */
51
+ secretBuf: Buffer;
52
+ /** In-flight refresh promise (deduplicates concurrent callers) */
53
+ pending: Promise<string> | null;
54
+ }
55
+ /**
56
+ * Sentinel error raised when getServiceToken() is called with a known apiKey
57
+ * but a non-matching secret. Indicates either credential drift in the caller
58
+ * or a cross-tenant cache lookup attempt. Surface as a 401-equivalent.
59
+ */
60
+ export declare class ServiceCredentialMismatchError extends Error {
61
+ constructor();
62
+ }
37
63
  export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T): {
38
64
  new (...args: any[]): {
39
- /** @internal */ _serviceToken: string | null;
40
- /** @internal */ _serviceTokenExp: number;
41
- /** @internal */ _serviceApiKey: string | null;
42
- /** @internal */ _serviceApiSecret: string | null;
65
+ /**
66
+ * Per-credential token cache.
67
+ *
68
+ * Keyed by SHA-256(apiKey). Each entry carries:
69
+ * - the issued service JWT
70
+ * - its expiry timestamp
71
+ * - the secret that produced it (Buffer for constant-time compare)
72
+ * - an optional in-flight promise to deduplicate concurrent refreshes
73
+ *
74
+ * The previous implementation kept ONE token/exp pair per OxyServices
75
+ * instance. That meant calling `getServiceToken(keyA, secretA)` populated
76
+ * the cache, and a subsequent `getServiceToken(keyB, secretB)` (different
77
+ * tenant) would receive tenant A's token. This is fixed by routing every
78
+ * lookup through the Map.
79
+ *
80
+ * @internal
81
+ */
82
+ _serviceTokenCache: Map<string, ServiceTokenCacheEntry>;
83
+ /** @internal Raw apiKey stored by configureServiceAuth() for use by getServiceToken() */
84
+ _serviceApiKey: string | null;
85
+ /** @internal Raw apiSecret stored by configureServiceAuth() for use by getServiceToken() */
86
+ _serviceApiSecret: string | null;
87
+ /**
88
+ * Hash an apiKey into a stable Map cache key. Uses Node's SHA-256 — service
89
+ * tokens are only ever issued by a Node host (the SDK on web/RN never has
90
+ * the apiSecret in the first place), so we can rely on Node crypto here.
91
+ *
92
+ * @internal
93
+ */
94
+ _hashApiKey(apiKey: string): Promise<string>;
43
95
  /**
44
96
  * Configure service credentials for internal service-to-service communication.
45
97
  * Call this once at startup so that getServiceToken() and makeServiceRequest()
46
98
  * can automatically obtain and refresh tokens.
47
99
  *
100
+ * Calling this with credentials that differ from a previously-configured pair
101
+ * is allowed — each `(apiKey, apiSecret)` pair is cached independently, so
102
+ * legitimate multi-tenant hosts that need to switch credentials cannot leak
103
+ * one tenant's token to another tenant on the same instance.
104
+ *
48
105
  * @param apiKey - DeveloperApp API key (oxy_dk_*)
49
106
  * @param apiSecret - DeveloperApp API secret
50
107
  */
51
108
  configureServiceAuth(apiKey: string, apiSecret: string): void;
52
109
  /**
53
110
  * Get a service token for internal service-to-service communication.
54
- * Tokens are short-lived (1h) and automatically cached/refreshed.
111
+ * Tokens are short-lived (1h) and automatically cached/refreshed per
112
+ * `(apiKey, apiSecret)` pair.
113
+ *
114
+ * Concurrent callers for the same credential pair share a single in-flight
115
+ * request to avoid hammering `/auth/service-token` when the cache is empty
116
+ * or expired.
117
+ *
118
+ * **Security guarantee:** if the cache already holds a token for this
119
+ * apiKey but the supplied apiSecret does not constant-time match the
120
+ * secret that originally produced that token, this method throws
121
+ * `ServiceCredentialMismatchError` instead of returning the cached token.
122
+ * This prevents an attacker who learned a peer's apiKey from extracting
123
+ * their service token by polling with a wrong secret.
55
124
  *
56
125
  * @param apiKey - DeveloperApp API key (optional if configureServiceAuth was called)
57
126
  * @param apiSecret - DeveloperApp API secret (optional if configureServiceAuth was called)
58
127
  */
59
128
  getServiceToken(apiKey?: string, apiSecret?: string): Promise<string>;
129
+ /**
130
+ * Perform the actual /auth/service-token request and cache the result.
131
+ * Separated so getServiceToken() can deduplicate concurrent calls.
132
+ * @internal
133
+ */
134
+ _doFetchServiceToken(key: string, secret: string, cacheKey: string, secretBuf: Buffer): Promise<string>;
60
135
  /**
61
136
  * Make an authenticated request on behalf of a user using a service token.
62
137
  * Automatically obtains/refreshes the service token.
@@ -192,6 +267,7 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
192
267
  };
193
268
  clearCache(): void;
194
269
  clearCacheEntry(key: string): void;
270
+ clearCacheByPrefix(prefix: string): number;
195
271
  getCacheStats(): {
196
272
  size: number;
197
273
  hits: number;
@@ -224,3 +300,4 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
224
300
  }>;
225
301
  };
226
302
  } & T;
303
+ export {};