@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
package/dist/cjs/index.js CHANGED
@@ -29,8 +29,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
29
29
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
30
30
  };
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.normalizeLanguageCode = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.SUPPORTED_LANGUAGES = exports.DeviceManager = exports.TopicSource = exports.TopicType = exports.RecoveryPhraseService = exports.SignatureService = exports.KeyManager = exports.createCrossDomainAuth = exports.CrossDomainAuth = exports.createAuthManager = exports.AuthManager = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
33
- 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.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.packageInfo = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = 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 = void 0;
32
+ exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.isRTLLocale = exports.normalizeLanguageCode = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.SUPPORTED_LANGUAGES = exports.DeviceManager = exports.TopicSource = exports.TopicType = exports.IdentityPersistError = exports.IdentityAlreadyExistsError = exports.RecoveryPhraseService = exports.SignatureService = exports.KeyManager = exports.ServiceCredentialMismatchError = exports.createCrossDomainAuth = exports.CrossDomainAuth = exports.createAuthManager = exports.AuthManager = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
33
+ 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.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.packageInfo = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = 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 = void 0;
34
34
  // Ensure crypto polyfills are loaded before anything else
35
35
  require("./crypto/polyfill");
36
36
  // --- Core API Client ---
@@ -48,11 +48,15 @@ Object.defineProperty(exports, "createAuthManager", { enumerable: true, get: fun
48
48
  var CrossDomainAuth_1 = require("./CrossDomainAuth");
49
49
  Object.defineProperty(exports, "CrossDomainAuth", { enumerable: true, get: function () { return CrossDomainAuth_1.CrossDomainAuth; } });
50
50
  Object.defineProperty(exports, "createCrossDomainAuth", { enumerable: true, get: function () { return CrossDomainAuth_1.createCrossDomainAuth; } });
51
+ var OxyServices_auth_1 = require("./mixins/OxyServices.auth");
52
+ Object.defineProperty(exports, "ServiceCredentialMismatchError", { enumerable: true, get: function () { return OxyServices_auth_1.ServiceCredentialMismatchError; } });
51
53
  // --- Crypto / Identity ---
52
54
  var crypto_1 = require("./crypto");
53
55
  Object.defineProperty(exports, "KeyManager", { enumerable: true, get: function () { return crypto_1.KeyManager; } });
54
56
  Object.defineProperty(exports, "SignatureService", { enumerable: true, get: function () { return crypto_1.SignatureService; } });
55
57
  Object.defineProperty(exports, "RecoveryPhraseService", { enumerable: true, get: function () { return crypto_1.RecoveryPhraseService; } });
58
+ Object.defineProperty(exports, "IdentityAlreadyExistsError", { enumerable: true, get: function () { return crypto_1.IdentityAlreadyExistsError; } });
59
+ Object.defineProperty(exports, "IdentityPersistError", { enumerable: true, get: function () { return crypto_1.IdentityPersistError; } });
56
60
  // --- Models & Types ---
57
61
  __exportStar(require("./models/interfaces"), exports);
58
62
  __exportStar(require("./models/session"), exports);
@@ -69,6 +73,7 @@ Object.defineProperty(exports, "getLanguageMetadata", { enumerable: true, get: f
69
73
  Object.defineProperty(exports, "getLanguageName", { enumerable: true, get: function () { return languageUtils_1.getLanguageName; } });
70
74
  Object.defineProperty(exports, "getNativeLanguageName", { enumerable: true, get: function () { return languageUtils_1.getNativeLanguageName; } });
71
75
  Object.defineProperty(exports, "normalizeLanguageCode", { enumerable: true, get: function () { return languageUtils_1.normalizeLanguageCode; } });
76
+ Object.defineProperty(exports, "isRTLLocale", { enumerable: true, get: function () { return languageUtils_1.isRTLLocale; } });
72
77
  // --- Platform Detection ---
73
78
  var platform_1 = require("./utils/platform");
74
79
  Object.defineProperty(exports, "getPlatformOS", { enumerable: true, get: function () { return platform_1.getPlatformOS; } });
@@ -165,6 +170,9 @@ Object.defineProperty(exports, "updateAvatarVisibility", { enumerable: true, get
165
170
  var accountUtils_1 = require("./utils/accountUtils");
166
171
  Object.defineProperty(exports, "buildAccountsArray", { enumerable: true, get: function () { return accountUtils_1.buildAccountsArray; } });
167
172
  Object.defineProperty(exports, "createQuickAccount", { enumerable: true, get: function () { return accountUtils_1.createQuickAccount; } });
173
+ Object.defineProperty(exports, "getAccountDisplayName", { enumerable: true, get: function () { return accountUtils_1.getAccountDisplayName; } });
174
+ Object.defineProperty(exports, "getAccountFallbackHandle", { enumerable: true, get: function () { return accountUtils_1.getAccountFallbackHandle; } });
175
+ Object.defineProperty(exports, "formatPublicKeyHandle", { enumerable: true, get: function () { return accountUtils_1.formatPublicKeyHandle; } });
168
176
  // Default export
169
177
  const OxyServices_3 = require("./OxyServices");
170
178
  exports.default = OxyServices_3.OxyServices;
@@ -153,15 +153,20 @@ function OxyServicesAssetsMixin(Base) {
153
153
  const fileSize = 'size' in file && file.size ? file.size : 0;
154
154
  try {
155
155
  const formData = new FormData();
156
- if ('uri' in file && typeof file.uri === 'string') {
157
- // React Native file descriptor — RN's FormData handles {uri, type, name} natively
156
+ if (typeof File !== 'undefined' && file instanceof File) {
158
157
  formData.append('file', file, fileName);
159
158
  }
160
- else if (file instanceof Blob) {
159
+ else if (typeof Blob !== 'undefined' && file instanceof Blob) {
160
+ formData.append('file', file, fileName);
161
+ }
162
+ else if ('uri' in file && typeof file.uri === 'string') {
163
+ // React Native file descriptor — RN's FormData handles {uri, type, name} natively.
164
+ // It reads the file from disk during the multipart request — no in-JS Blob
165
+ // conversion (which would fail on Hermes for ArrayBuffer-backed Blobs).
161
166
  formData.append('file', file, fileName);
162
167
  }
163
168
  else {
164
- formData.append('file', new Blob([file], { type: 'application/octet-stream' }), fileName);
169
+ throw new Error('Unsupported file input: expected File, Blob, or { uri, type?, name?, size? } descriptor');
165
170
  }
166
171
  if (visibility) {
167
172
  formData.append('visibility', visibility);
@@ -1,34 +1,92 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ServiceCredentialMismatchError = void 0;
3
4
  exports.OxyServicesAuthMixin = OxyServicesAuthMixin;
4
5
  const OxyServices_errors_1 = require("../OxyServices.errors");
6
+ const platformCrypto_1 = require("../utils/platformCrypto");
7
+ const loggerUtils_1 = require("../utils/loggerUtils");
8
+ /**
9
+ * Sentinel error raised when getServiceToken() is called with a known apiKey
10
+ * but a non-matching secret. Indicates either credential drift in the caller
11
+ * or a cross-tenant cache lookup attempt. Surface as a 401-equivalent.
12
+ */
13
+ class ServiceCredentialMismatchError extends Error {
14
+ constructor() {
15
+ super('Service credential mismatch: provided secret does not match the secret stored for this apiKey');
16
+ this.name = 'ServiceCredentialMismatchError';
17
+ }
18
+ }
19
+ exports.ServiceCredentialMismatchError = ServiceCredentialMismatchError;
5
20
  function OxyServicesAuthMixin(Base) {
6
21
  return class extends Base {
7
22
  constructor(...args) {
8
23
  super(...args);
9
- /** @internal */ this._serviceToken = null;
10
- /** @internal */ this._serviceTokenExp = 0;
11
- /** @internal */ this._serviceApiKey = null;
12
- /** @internal */ this._serviceApiSecret = null;
24
+ /**
25
+ * Per-credential token cache.
26
+ *
27
+ * Keyed by SHA-256(apiKey). Each entry carries:
28
+ * - the issued service JWT
29
+ * - its expiry timestamp
30
+ * - the secret that produced it (Buffer for constant-time compare)
31
+ * - an optional in-flight promise to deduplicate concurrent refreshes
32
+ *
33
+ * The previous implementation kept ONE token/exp pair per OxyServices
34
+ * instance. That meant calling `getServiceToken(keyA, secretA)` populated
35
+ * the cache, and a subsequent `getServiceToken(keyB, secretB)` (different
36
+ * tenant) would receive tenant A's token. This is fixed by routing every
37
+ * lookup through the Map.
38
+ *
39
+ * @internal
40
+ */
41
+ this._serviceTokenCache = new Map();
42
+ /** @internal Raw apiKey stored by configureServiceAuth() for use by getServiceToken() */
43
+ this._serviceApiKey = null;
44
+ /** @internal Raw apiSecret stored by configureServiceAuth() for use by getServiceToken() */
45
+ this._serviceApiSecret = null;
46
+ }
47
+ /**
48
+ * Hash an apiKey into a stable Map cache key. Uses Node's SHA-256 — service
49
+ * tokens are only ever issued by a Node host (the SDK on web/RN never has
50
+ * the apiSecret in the first place), so we can rely on Node crypto here.
51
+ *
52
+ * @internal
53
+ */
54
+ async _hashApiKey(apiKey) {
55
+ const nodeCrypto = await (0, platformCrypto_1.loadNodeCrypto)();
56
+ return nodeCrypto.createHash('sha256').update(apiKey).digest('hex');
13
57
  }
14
58
  /**
15
59
  * Configure service credentials for internal service-to-service communication.
16
60
  * Call this once at startup so that getServiceToken() and makeServiceRequest()
17
61
  * can automatically obtain and refresh tokens.
18
62
  *
63
+ * Calling this with credentials that differ from a previously-configured pair
64
+ * is allowed — each `(apiKey, apiSecret)` pair is cached independently, so
65
+ * legitimate multi-tenant hosts that need to switch credentials cannot leak
66
+ * one tenant's token to another tenant on the same instance.
67
+ *
19
68
  * @param apiKey - DeveloperApp API key (oxy_dk_*)
20
69
  * @param apiSecret - DeveloperApp API secret
21
70
  */
22
71
  configureServiceAuth(apiKey, apiSecret) {
23
72
  this._serviceApiKey = apiKey;
24
73
  this._serviceApiSecret = apiSecret;
25
- // Invalidate any cached token
26
- this._serviceToken = null;
27
- this._serviceTokenExp = 0;
28
74
  }
29
75
  /**
30
76
  * Get a service token for internal service-to-service communication.
31
- * Tokens are short-lived (1h) and automatically cached/refreshed.
77
+ * Tokens are short-lived (1h) and automatically cached/refreshed per
78
+ * `(apiKey, apiSecret)` pair.
79
+ *
80
+ * Concurrent callers for the same credential pair share a single in-flight
81
+ * request to avoid hammering `/auth/service-token` when the cache is empty
82
+ * or expired.
83
+ *
84
+ * **Security guarantee:** if the cache already holds a token for this
85
+ * apiKey but the supplied apiSecret does not constant-time match the
86
+ * secret that originally produced that token, this method throws
87
+ * `ServiceCredentialMismatchError` instead of returning the cached token.
88
+ * This prevents an attacker who learned a peer's apiKey from extracting
89
+ * their service token by polling with a wrong secret.
32
90
  *
33
91
  * @param apiKey - DeveloperApp API key (optional if configureServiceAuth was called)
34
92
  * @param apiSecret - DeveloperApp API secret (optional if configureServiceAuth was called)
@@ -39,14 +97,89 @@ function OxyServicesAuthMixin(Base) {
39
97
  if (!key || !secret) {
40
98
  throw new Error('Service credentials not provided. Call configureServiceAuth() or pass apiKey and apiSecret.');
41
99
  }
42
- // Return cached token if still valid (with 60s buffer)
43
- if (this._serviceToken && this._serviceTokenExp > Date.now() + 60000) {
44
- return this._serviceToken;
100
+ const cacheKey = await this._hashApiKey(key);
101
+ const now = Date.now();
102
+ const providedSecretBuf = Buffer.from(secret, 'utf8');
103
+ let entry = this._serviceTokenCache.get(cacheKey);
104
+ // Verify the secret on every cache hit, regardless of token freshness.
105
+ // Constant-time compare prevents timing oracles on the stored secret.
106
+ if (entry) {
107
+ const nodeCrypto = await (0, platformCrypto_1.loadNodeCrypto)();
108
+ const storedSecretBuf = entry.secretBuf;
109
+ const lengthMatch = storedSecretBuf.length === providedSecretBuf.length;
110
+ // Always run timingSafeEqual on equal-length inputs to keep timing flat.
111
+ // When lengths differ, run against a zero-padded copy of the same length
112
+ // to avoid an early-return timing signal.
113
+ const compareBuf = lengthMatch
114
+ ? providedSecretBuf
115
+ : Buffer.alloc(storedSecretBuf.length);
116
+ const compareResult = nodeCrypto.timingSafeEqual(storedSecretBuf, compareBuf);
117
+ if (!lengthMatch || !compareResult) {
118
+ loggerUtils_1.logger.warn('[oxy.auth] Service token cache hit with mismatched secret', {
119
+ component: 'auth',
120
+ method: 'getServiceToken',
121
+ });
122
+ throw new ServiceCredentialMismatchError();
123
+ }
124
+ // Return cached token if still valid (with 60s buffer for clock drift)
125
+ if (entry.token && entry.expiresAt > now + 60000) {
126
+ return entry.token;
127
+ }
128
+ // If a fetch is already in-flight for this credential, share its result
129
+ if (entry.pending) {
130
+ return entry.pending;
131
+ }
45
132
  }
133
+ else {
134
+ // First time seeing this apiKey on this instance — seed an empty entry
135
+ // so concurrent callers serialize on the same promise.
136
+ entry = {
137
+ token: '',
138
+ expiresAt: 0,
139
+ secretBuf: providedSecretBuf,
140
+ pending: null,
141
+ };
142
+ this._serviceTokenCache.set(cacheKey, entry);
143
+ }
144
+ const pending = this._doFetchServiceToken(key, secret, cacheKey, providedSecretBuf);
145
+ entry.pending = pending;
146
+ try {
147
+ return await pending;
148
+ }
149
+ finally {
150
+ // Clear the in-flight slot; the entry itself (with fresh token / expiry)
151
+ // is updated inside _doFetchServiceToken before we land here.
152
+ const settled = this._serviceTokenCache.get(cacheKey);
153
+ if (settled) {
154
+ settled.pending = null;
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Perform the actual /auth/service-token request and cache the result.
160
+ * Separated so getServiceToken() can deduplicate concurrent calls.
161
+ * @internal
162
+ */
163
+ async _doFetchServiceToken(key, secret, cacheKey, secretBuf) {
46
164
  const response = await this.makeRequest('POST', '/auth/service-token', { apiKey: key, apiSecret: secret }, { cache: false, retry: false });
47
- this._serviceToken = response.token;
48
- this._serviceTokenExp = Date.now() + response.expiresIn * 1000;
49
- return this._serviceToken;
165
+ const expiresAt = Date.now() + response.expiresIn * 1000;
166
+ // Update the entry in-place so any caller that already grabbed a reference
167
+ // (via `_serviceTokenCache.get(...)`) sees the fresh state.
168
+ const entry = this._serviceTokenCache.get(cacheKey);
169
+ if (entry) {
170
+ entry.token = response.token;
171
+ entry.expiresAt = expiresAt;
172
+ entry.secretBuf = secretBuf;
173
+ }
174
+ else {
175
+ this._serviceTokenCache.set(cacheKey, {
176
+ token: response.token,
177
+ expiresAt,
178
+ secretBuf,
179
+ pending: null,
180
+ });
181
+ }
182
+ return response.token;
50
183
  }
51
184
  /**
52
185
  * Make an authenticated request on behalf of a user using a service token.
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * Contact Discovery Mixin
4
+ *
5
+ * Privacy-preserving discovery of which address-book contacts are on Oxy.
6
+ *
7
+ * The client hashes emails and phones locally before calling the API.
8
+ * The server responds with only Oxy user IDs and the hashes that matched,
9
+ * so the consumer can map each match back to the local contact that
10
+ * produced it.
11
+ *
12
+ * Hashing rules (must match the server `utils/contactHash.ts` exactly):
13
+ * - SHA-256, hex-encoded, lowercase
14
+ * - Email: `value.trim().toLowerCase()` then digest
15
+ * - Phone: trim → keep a single leading "+" → strip non-digits → prepend "+"
16
+ * if missing → digest
17
+ *
18
+ * Mobile clients can compute these digests with `expo-crypto`'s
19
+ * `digestStringAsync(SHA256, value, { encoding: HEX })`. Web clients should
20
+ * use `SubtleCrypto.digest('SHA-256', ...)`.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.OxyServicesContactsMixin = OxyServicesContactsMixin;
24
+ function OxyServicesContactsMixin(Base) {
25
+ return class extends Base {
26
+ constructor(...args) {
27
+ super(...args);
28
+ }
29
+ /**
30
+ * Discover which of the caller's contacts are on Oxy.
31
+ *
32
+ * @param hashedEmails - SHA-256 hex digests of normalized emails.
33
+ * @param hashedPhones - SHA-256 hex digests of normalized phone numbers.
34
+ * @returns Matches mapping each hashed identifier to the Oxy user ID it
35
+ * resolved to. Empty arrays are valid for either parameter, but at
36
+ * least one must be non-empty.
37
+ *
38
+ * The server enforces a 200-hash cap per channel per request — callers
39
+ * should batch larger address books client-side.
40
+ */
41
+ async discoverContacts(hashedEmails, hashedPhones) {
42
+ try {
43
+ return await this.makeRequest('POST', '/contacts/discover', { hashedEmails, hashedPhones }, { cache: false });
44
+ }
45
+ catch (error) {
46
+ throw this.handleError(error);
47
+ }
48
+ }
49
+ };
50
+ }
@@ -294,16 +294,5 @@ function OxyServicesFeaturesMixin(Base) {
294
294
  throw this.handleError(error);
295
295
  }
296
296
  }
297
- // ==================
298
- // ACCOUNT
299
- // ==================
300
- /**
301
- * Delete user account (requires password confirmation)
302
- */
303
- async deleteAccount(password) {
304
- return this.withAuthRetry(async () => {
305
- await this.makeRequest('DELETE', '/account', { password }, { cache: false });
306
- }, 'deleteAccount');
307
- }
308
297
  };
309
298
  }
@@ -315,10 +315,11 @@ function OxyServicesFedCMMixin(Base) {
315
315
  {
316
316
  configURL: options.configURL,
317
317
  clientId: options.clientId,
318
- // Send nonce at both levels for backward compatibility
319
- nonce: options.nonce, // For older browsers
318
+ // Older browsers read `nonce` at the top level; Chrome 145+
319
+ // expects it inside `params`. Send both for full coverage.
320
+ nonce: options.nonce,
320
321
  params: {
321
- nonce: options.nonce, // For Chrome 145+
322
+ nonce: options.nonce,
322
323
  },
323
324
  ...(options.loginHint && { loginHint: options.loginHint }),
324
325
  },
@@ -1,43 +1,11 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.OxyServicesLanguageMixin = OxyServicesLanguageMixin;
37
4
  /**
38
5
  * Language Methods Mixin
39
6
  */
40
7
  const languageUtils_1 = require("../utils/languageUtils");
8
+ const platformCrypto_1 = require("../utils/platformCrypto");
41
9
  const debugUtils_1 = require("../shared/utils/debugUtils");
42
10
  function OxyServicesLanguageMixin(Base) {
43
11
  return class extends Base {
@@ -51,9 +19,10 @@ function OxyServicesLanguageMixin(Base) {
51
19
  const isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
52
20
  if (isReactNative) {
53
21
  try {
54
- // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
55
- const moduleName = '@react-native-async-storage/async-storage';
56
- const asyncStorageModule = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
22
+ // `loadAsyncStorage` is per-platform: the RN variant statically imports
23
+ // @react-native-async-storage/async-storage, the default variant throws
24
+ // (never called outside RN because of the `isReactNative` gate above).
25
+ const asyncStorageModule = await (0, platformCrypto_1.loadAsyncStorage)();
57
26
  const storage = asyncStorageModule.default;
58
27
  return {
59
28
  getItem: storage.getItem.bind(storage),
@@ -153,12 +153,16 @@ function OxyServicesRedirectAuthMixin(Base) {
153
153
  // Store tokens
154
154
  this.storeTokens(accessToken, sessionId);
155
155
  this.httpService.setTokens(accessToken);
156
- // Build session response (minimal - we'll fetch full user data separately)
156
+ // Build session response (minimal full user data is fetched separately
157
+ // by the caller via getCurrentUser() once tokens are stored).
157
158
  const session = {
158
159
  sessionId,
159
160
  deviceId: '', // Not available in redirect flow
160
161
  expiresAt: expiresAt || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
161
- user: {}, // Will be fetched separately
162
+ // Placeholder user caller MUST fetch real user data via getCurrentUser()
163
+ // before exposing this session to the application. The empty id signals
164
+ // that the user payload has not yet been populated.
165
+ user: { id: '', username: '' },
162
166
  };
163
167
  // Clean up URL (remove auth parameters)
164
168
  this.cleanAuthCallbackUrl(url);
@@ -23,8 +23,19 @@ function OxyServicesSecurityMixin(Base) {
23
23
  params.offset = offset;
24
24
  if (eventType)
25
25
  params.eventType = eventType;
26
- const response = await this.makeRequest('GET', '/security/activity', params, { cache: false });
27
- return response;
26
+ // The API responds with the standard paginated envelope:
27
+ // { data: SecurityActivity[], pagination: { total, limit, offset, hasMore } }
28
+ // SecurityActivityResponse is the flattened shape consumers expect.
29
+ const raw = await this.makeRequest('GET', '/security/activity', params, { cache: false });
30
+ const requestedLimit = typeof params.limit === 'number' ? params.limit : 0;
31
+ const requestedOffset = typeof params.offset === 'number' ? params.offset : 0;
32
+ return {
33
+ data: raw.data ?? [],
34
+ total: raw.pagination?.total ?? raw.data?.length ?? 0,
35
+ limit: raw.pagination?.limit ?? requestedLimit,
36
+ offset: raw.pagination?.offset ?? requestedOffset,
37
+ hasMore: raw.pagination?.hasMore ?? false,
38
+ };
28
39
  }
29
40
  catch (error) {
30
41
  throw this.handleError(error);
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OxyServicesUserMixin = OxyServicesUserMixin;
4
4
  const apiUtils_1 = require("../utils/apiUtils");
5
+ const keyManager_1 = require("../crypto/keyManager");
6
+ const signatureService_1 = require("../crypto/signatureService");
5
7
  function OxyServicesUserMixin(Base) {
6
8
  return class extends Base {
7
9
  constructor(...args) {
@@ -44,37 +46,22 @@ function OxyServicesUserMixin(Base) {
44
46
  cache: true,
45
47
  cacheTTL: 2 * 60 * 1000, // 2 minutes cache
46
48
  });
47
- // New API shape: { data: User[], pagination: {...} }
48
- const isSearchProfilesResponse = (payload) => typeof payload === 'object' &&
49
- payload !== null &&
50
- Array.isArray(payload.data);
51
- if (isSearchProfilesResponse(response)) {
52
- const typedResponse = response;
53
- const paginationInfo = typedResponse.pagination ?? {
54
- total: typedResponse.data.length,
55
- limit: pagination?.limit ?? typedResponse.data.length,
56
- offset: pagination?.offset ?? 0,
57
- hasMore: typedResponse.data.length === (pagination?.limit ?? typedResponse.data.length) &&
58
- (pagination?.limit ?? typedResponse.data.length) > 0,
59
- };
60
- return {
61
- data: typedResponse.data,
62
- pagination: paginationInfo,
63
- };
49
+ if (typeof response !== 'object' ||
50
+ response === null ||
51
+ !Array.isArray(response.data)) {
52
+ throw new Error('Unexpected search response format');
64
53
  }
65
- // Legacy API shape: returns raw User[]
66
- if (Array.isArray(response)) {
67
- const fallbackLimit = pagination?.limit ?? response.length;
68
- const fallbackPagination = {
69
- total: response.length,
70
- limit: fallbackLimit,
71
- offset: pagination?.offset ?? 0,
72
- hasMore: fallbackLimit > 0 && response.length === fallbackLimit,
73
- };
74
- return { data: response, pagination: fallbackPagination };
75
- }
76
- // If response is unexpected, throw an error
77
- throw new Error('Unexpected search response format');
54
+ const paginationInfo = response.pagination ?? {
55
+ total: response.data.length,
56
+ limit: pagination?.limit ?? response.data.length,
57
+ offset: pagination?.offset ?? 0,
58
+ hasMore: response.data.length === (pagination?.limit ?? response.data.length) &&
59
+ (pagination?.limit ?? response.data.length) > 0,
60
+ };
61
+ return {
62
+ data: response.data,
63
+ pagination: paginationInfo,
64
+ };
78
65
  }
79
66
  catch (error) {
80
67
  throw this.handleError(error);
@@ -156,12 +143,31 @@ function OxyServicesUserMixin(Base) {
156
143
  }, 'getCurrentUser');
157
144
  }
158
145
  /**
159
- * Update user profile
160
- * TanStack Query handles offline queuing automatically
146
+ * Update user profile.
147
+ *
148
+ * Invalidates the SDK-side response cache for every endpoint that
149
+ * returns the current user (`GET /users/me`, `GET /session/user/*`,
150
+ * `GET /users/<id>`, `GET /profiles/username/*`) so the next read
151
+ * doesn't return a stale snapshot. Without this, a follow-up
152
+ * `getUserBySession` call inside the 2-minute cache window can return
153
+ * the pre-update user — most visibly during onboarding, where it
154
+ * causes the username step to flicker back as if nothing was saved.
155
+ *
156
+ * TanStack Query handles offline queuing automatically.
161
157
  */
162
158
  async updateProfile(updates) {
163
159
  try {
164
- return await this.makeRequest('PUT', '/users/me', updates, { cache: false });
160
+ const result = await this.makeRequest('PUT', '/users/me', updates, { cache: false });
161
+ // Bust every cached representation of the current user. We use a
162
+ // prefix sweep rather than an enumeration because the SDK never
163
+ // tracks the set of active session IDs centrally.
164
+ this.clearCacheByPrefix('GET:/session/user/');
165
+ this.clearCacheByPrefix('GET:/users/me');
166
+ this.clearCacheByPrefix('GET:/profiles/username/');
167
+ if (result?.id) {
168
+ this.clearCacheEntry(`GET:/users/${result.id}`);
169
+ }
170
+ return result;
165
171
  }
166
172
  catch (error) {
167
173
  const errorAny = error;
@@ -245,14 +251,29 @@ function OxyServicesUserMixin(Base) {
245
251
  }
246
252
  }
247
253
  /**
248
- * Delete account permanently
249
- * @param password - User password for confirmation
250
- * @param confirmText - Confirmation text (usually username)
254
+ * Delete account permanently.
255
+ *
256
+ * Signs `delete:{publicKey}:{timestamp}` with the locally-stored identity
257
+ * private key and submits the signature alongside the confirmation text
258
+ * (must equal the user's username). The signature is the cryptographic
259
+ * proof of ownership — only the device holding the private key can issue
260
+ * a valid signature, so no password is required.
261
+ *
262
+ * @param confirmText - Must equal the user's username (verified server-side)
263
+ * @throws If no identity is stored on this device, or signing fails
251
264
  */
252
- async deleteAccount(password, confirmText) {
265
+ async deleteAccount(confirmText) {
253
266
  try {
267
+ const publicKey = await keyManager_1.KeyManager.getPublicKey();
268
+ if (!publicKey) {
269
+ throw new Error('No identity found on this device. Account deletion requires the device that holds your identity key.');
270
+ }
271
+ const timestamp = Date.now();
272
+ const message = `delete:${publicKey}:${timestamp}`;
273
+ const signature = await signatureService_1.SignatureService.sign(message);
254
274
  return await this.makeRequest('DELETE', '/users/me', {
255
- password,
275
+ signature,
276
+ timestamp,
256
277
  confirmText,
257
278
  }, { cache: false });
258
279
  }