@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/CrossDomainAuth.js +3 -1
- package/dist/cjs/HttpService.js +214 -33
- package/dist/cjs/OxyServices.base.js +9 -0
- package/dist/cjs/OxyServices.js +8 -3
- package/dist/cjs/crypto/index.js +3 -1
- package/dist/cjs/crypto/keyManager.js +476 -172
- package/dist/cjs/crypto/polyfill.js +14 -65
- package/dist/cjs/crypto/recoveryPhrase.js +30 -11
- package/dist/cjs/crypto/signatureService.js +25 -60
- package/dist/cjs/i18n/locales/en-US.json +46 -1
- package/dist/cjs/i18n/locales/es-ES.json +46 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +46 -1
- package/dist/cjs/i18n/locales/locales/es-ES.json +46 -1
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/mixins/OxyServices.assets.js +9 -4
- package/dist/cjs/mixins/OxyServices.auth.js +147 -14
- package/dist/cjs/mixins/OxyServices.contacts.js +50 -0
- package/dist/cjs/mixins/OxyServices.features.js +0 -11
- package/dist/cjs/mixins/OxyServices.fedcm.js +4 -3
- package/dist/cjs/mixins/OxyServices.language.js +5 -36
- package/dist/cjs/mixins/OxyServices.redirect.js +6 -2
- package/dist/cjs/mixins/OxyServices.security.js +13 -2
- package/dist/cjs/mixins/OxyServices.user.js +59 -38
- package/dist/cjs/mixins/OxyServices.utility.js +416 -110
- package/dist/cjs/mixins/index.js +11 -3
- package/dist/cjs/utils/accountUtils.js +71 -2
- package/dist/cjs/utils/deviceManager.js +5 -36
- package/dist/cjs/utils/languageUtils.js +22 -0
- package/dist/cjs/utils/platformCrypto.js +165 -0
- package/dist/cjs/utils/platformCrypto.native.js +123 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/CrossDomainAuth.js +3 -1
- package/dist/esm/HttpService.js +215 -34
- package/dist/esm/OxyServices.base.js +9 -0
- package/dist/esm/OxyServices.js +8 -3
- package/dist/esm/crypto/index.js +1 -1
- package/dist/esm/crypto/keyManager.js +473 -138
- package/dist/esm/crypto/polyfill.js +14 -32
- package/dist/esm/crypto/recoveryPhrase.js +30 -11
- package/dist/esm/crypto/signatureService.js +25 -27
- package/dist/esm/i18n/locales/en-US.json +46 -1
- package/dist/esm/i18n/locales/es-ES.json +46 -1
- package/dist/esm/i18n/locales/locales/en-US.json +46 -1
- package/dist/esm/i18n/locales/locales/es-ES.json +46 -1
- package/dist/esm/index.js +4 -3
- package/dist/esm/mixins/OxyServices.assets.js +9 -4
- package/dist/esm/mixins/OxyServices.auth.js +145 -14
- package/dist/esm/mixins/OxyServices.contacts.js +47 -0
- package/dist/esm/mixins/OxyServices.features.js +0 -11
- package/dist/esm/mixins/OxyServices.fedcm.js +4 -3
- package/dist/esm/mixins/OxyServices.language.js +5 -3
- package/dist/esm/mixins/OxyServices.redirect.js +6 -2
- package/dist/esm/mixins/OxyServices.security.js +13 -2
- package/dist/esm/mixins/OxyServices.user.js +59 -38
- package/dist/esm/mixins/OxyServices.utility.js +416 -77
- package/dist/esm/mixins/index.js +11 -3
- package/dist/esm/utils/accountUtils.js +67 -1
- package/dist/esm/utils/deviceManager.js +5 -3
- package/dist/esm/utils/languageUtils.js +21 -0
- package/dist/esm/utils/platformCrypto.js +125 -0
- package/dist/esm/utils/platformCrypto.native.js +80 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +47 -3
- package/dist/types/OxyServices.base.d.ts +7 -0
- package/dist/types/OxyServices.d.ts +50 -7
- package/dist/types/crypto/index.d.ts +1 -1
- package/dist/types/crypto/keyManager.d.ts +110 -9
- package/dist/types/crypto/polyfill.d.ts +3 -1
- package/dist/types/crypto/recoveryPhrase.d.ts +31 -7
- package/dist/types/crypto/signatureService.d.ts +4 -0
- package/dist/types/index.d.ts +7 -5
- package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +6 -10
- package/dist/types/mixins/OxyServices.auth.d.ts +82 -5
- package/dist/types/mixins/OxyServices.contacts.d.ts +99 -0
- package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
- package/dist/types/mixins/OxyServices.features.d.ts +2 -7
- package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
- package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
- package/dist/types/mixins/OxyServices.language.d.ts +1 -0
- package/dist/types/mixins/OxyServices.location.d.ts +1 -0
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
- package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
- package/dist/types/mixins/OxyServices.popup.d.ts +1 -0
- package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
- package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
- package/dist/types/mixins/OxyServices.security.d.ts +1 -0
- package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.user.d.ts +28 -11
- package/dist/types/mixins/OxyServices.utility.d.ts +145 -10
- package/dist/types/mixins/index.d.ts +52 -4
- package/dist/types/models/interfaces.d.ts +62 -3
- package/dist/types/utils/accountUtils.d.ts +41 -1
- package/dist/types/utils/languageUtils.d.ts +1 -0
- package/dist/types/utils/platformCrypto.d.ts +87 -0
- package/dist/types/utils/platformCrypto.native.d.ts +54 -0
- package/package.json +45 -2
- package/src/CrossDomainAuth.ts +12 -10
- package/src/HttpService.ts +251 -40
- package/src/OxyServices.base.ts +10 -0
- package/src/OxyServices.ts +26 -7
- package/src/crypto/__tests__/keyManager.test.ts +336 -0
- package/src/crypto/index.ts +6 -1
- package/src/crypto/keyManager.ts +529 -151
- package/src/crypto/polyfill.ts +14 -34
- package/src/crypto/recoveryPhrase.ts +56 -17
- package/src/crypto/signatureService.ts +25 -30
- package/src/i18n/locales/en-US.json +46 -1
- package/src/i18n/locales/es-ES.json +46 -1
- package/src/index.ts +19 -4
- package/src/mixins/OxyServices.assets.ts +15 -11
- package/src/mixins/OxyServices.auth.ts +175 -15
- package/src/mixins/OxyServices.contacts.ts +73 -0
- package/src/mixins/OxyServices.features.ts +2 -12
- package/src/mixins/OxyServices.fedcm.ts +4 -3
- package/src/mixins/OxyServices.language.ts +6 -4
- package/src/mixins/OxyServices.redirect.ts +6 -2
- package/src/mixins/OxyServices.security.ts +18 -8
- package/src/mixins/OxyServices.user.ts +72 -49
- package/src/mixins/OxyServices.utility.ts +562 -89
- package/src/mixins/__tests__/serviceAuth.test.ts +623 -0
- package/src/mixins/index.ts +58 -7
- package/src/models/interfaces.ts +65 -3
- package/src/utils/accountUtils.ts +82 -2
- package/src/utils/deviceManager.ts +7 -4
- package/src/utils/languageUtils.ts +23 -2
- package/src/utils/platformCrypto.native.ts +101 -0
- package/src/utils/platformCrypto.ts +145 -0
|
@@ -1,31 +1,87 @@
|
|
|
1
1
|
import { OxyAuthenticationError } from '../OxyServices.errors.js';
|
|
2
|
+
import { loadNodeCrypto } from '../utils/platformCrypto.js';
|
|
3
|
+
import { logger } from '../utils/loggerUtils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Sentinel error raised when getServiceToken() is called with a known apiKey
|
|
6
|
+
* but a non-matching secret. Indicates either credential drift in the caller
|
|
7
|
+
* or a cross-tenant cache lookup attempt. Surface as a 401-equivalent.
|
|
8
|
+
*/
|
|
9
|
+
export class ServiceCredentialMismatchError extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super('Service credential mismatch: provided secret does not match the secret stored for this apiKey');
|
|
12
|
+
this.name = 'ServiceCredentialMismatchError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
2
15
|
export function OxyServicesAuthMixin(Base) {
|
|
3
16
|
return class extends Base {
|
|
4
17
|
constructor(...args) {
|
|
5
18
|
super(...args);
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Per-credential token cache.
|
|
21
|
+
*
|
|
22
|
+
* Keyed by SHA-256(apiKey). Each entry carries:
|
|
23
|
+
* - the issued service JWT
|
|
24
|
+
* - its expiry timestamp
|
|
25
|
+
* - the secret that produced it (Buffer for constant-time compare)
|
|
26
|
+
* - an optional in-flight promise to deduplicate concurrent refreshes
|
|
27
|
+
*
|
|
28
|
+
* The previous implementation kept ONE token/exp pair per OxyServices
|
|
29
|
+
* instance. That meant calling `getServiceToken(keyA, secretA)` populated
|
|
30
|
+
* the cache, and a subsequent `getServiceToken(keyB, secretB)` (different
|
|
31
|
+
* tenant) would receive tenant A's token. This is fixed by routing every
|
|
32
|
+
* lookup through the Map.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
this._serviceTokenCache = new Map();
|
|
37
|
+
/** @internal Raw apiKey stored by configureServiceAuth() for use by getServiceToken() */
|
|
38
|
+
this._serviceApiKey = null;
|
|
39
|
+
/** @internal Raw apiSecret stored by configureServiceAuth() for use by getServiceToken() */
|
|
40
|
+
this._serviceApiSecret = null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Hash an apiKey into a stable Map cache key. Uses Node's SHA-256 — service
|
|
44
|
+
* tokens are only ever issued by a Node host (the SDK on web/RN never has
|
|
45
|
+
* the apiSecret in the first place), so we can rely on Node crypto here.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
async _hashApiKey(apiKey) {
|
|
50
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
51
|
+
return nodeCrypto.createHash('sha256').update(apiKey).digest('hex');
|
|
10
52
|
}
|
|
11
53
|
/**
|
|
12
54
|
* Configure service credentials for internal service-to-service communication.
|
|
13
55
|
* Call this once at startup so that getServiceToken() and makeServiceRequest()
|
|
14
56
|
* can automatically obtain and refresh tokens.
|
|
15
57
|
*
|
|
58
|
+
* Calling this with credentials that differ from a previously-configured pair
|
|
59
|
+
* is allowed — each `(apiKey, apiSecret)` pair is cached independently, so
|
|
60
|
+
* legitimate multi-tenant hosts that need to switch credentials cannot leak
|
|
61
|
+
* one tenant's token to another tenant on the same instance.
|
|
62
|
+
*
|
|
16
63
|
* @param apiKey - DeveloperApp API key (oxy_dk_*)
|
|
17
64
|
* @param apiSecret - DeveloperApp API secret
|
|
18
65
|
*/
|
|
19
66
|
configureServiceAuth(apiKey, apiSecret) {
|
|
20
67
|
this._serviceApiKey = apiKey;
|
|
21
68
|
this._serviceApiSecret = apiSecret;
|
|
22
|
-
// Invalidate any cached token
|
|
23
|
-
this._serviceToken = null;
|
|
24
|
-
this._serviceTokenExp = 0;
|
|
25
69
|
}
|
|
26
70
|
/**
|
|
27
71
|
* Get a service token for internal service-to-service communication.
|
|
28
|
-
* Tokens are short-lived (1h) and automatically cached/refreshed
|
|
72
|
+
* Tokens are short-lived (1h) and automatically cached/refreshed per
|
|
73
|
+
* `(apiKey, apiSecret)` pair.
|
|
74
|
+
*
|
|
75
|
+
* Concurrent callers for the same credential pair share a single in-flight
|
|
76
|
+
* request to avoid hammering `/auth/service-token` when the cache is empty
|
|
77
|
+
* or expired.
|
|
78
|
+
*
|
|
79
|
+
* **Security guarantee:** if the cache already holds a token for this
|
|
80
|
+
* apiKey but the supplied apiSecret does not constant-time match the
|
|
81
|
+
* secret that originally produced that token, this method throws
|
|
82
|
+
* `ServiceCredentialMismatchError` instead of returning the cached token.
|
|
83
|
+
* This prevents an attacker who learned a peer's apiKey from extracting
|
|
84
|
+
* their service token by polling with a wrong secret.
|
|
29
85
|
*
|
|
30
86
|
* @param apiKey - DeveloperApp API key (optional if configureServiceAuth was called)
|
|
31
87
|
* @param apiSecret - DeveloperApp API secret (optional if configureServiceAuth was called)
|
|
@@ -36,14 +92,89 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
36
92
|
if (!key || !secret) {
|
|
37
93
|
throw new Error('Service credentials not provided. Call configureServiceAuth() or pass apiKey and apiSecret.');
|
|
38
94
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
95
|
+
const cacheKey = await this._hashApiKey(key);
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const providedSecretBuf = Buffer.from(secret, 'utf8');
|
|
98
|
+
let entry = this._serviceTokenCache.get(cacheKey);
|
|
99
|
+
// Verify the secret on every cache hit, regardless of token freshness.
|
|
100
|
+
// Constant-time compare prevents timing oracles on the stored secret.
|
|
101
|
+
if (entry) {
|
|
102
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
103
|
+
const storedSecretBuf = entry.secretBuf;
|
|
104
|
+
const lengthMatch = storedSecretBuf.length === providedSecretBuf.length;
|
|
105
|
+
// Always run timingSafeEqual on equal-length inputs to keep timing flat.
|
|
106
|
+
// When lengths differ, run against a zero-padded copy of the same length
|
|
107
|
+
// to avoid an early-return timing signal.
|
|
108
|
+
const compareBuf = lengthMatch
|
|
109
|
+
? providedSecretBuf
|
|
110
|
+
: Buffer.alloc(storedSecretBuf.length);
|
|
111
|
+
const compareResult = nodeCrypto.timingSafeEqual(storedSecretBuf, compareBuf);
|
|
112
|
+
if (!lengthMatch || !compareResult) {
|
|
113
|
+
logger.warn('[oxy.auth] Service token cache hit with mismatched secret', {
|
|
114
|
+
component: 'auth',
|
|
115
|
+
method: 'getServiceToken',
|
|
116
|
+
});
|
|
117
|
+
throw new ServiceCredentialMismatchError();
|
|
118
|
+
}
|
|
119
|
+
// Return cached token if still valid (with 60s buffer for clock drift)
|
|
120
|
+
if (entry.token && entry.expiresAt > now + 60000) {
|
|
121
|
+
return entry.token;
|
|
122
|
+
}
|
|
123
|
+
// If a fetch is already in-flight for this credential, share its result
|
|
124
|
+
if (entry.pending) {
|
|
125
|
+
return entry.pending;
|
|
126
|
+
}
|
|
42
127
|
}
|
|
128
|
+
else {
|
|
129
|
+
// First time seeing this apiKey on this instance — seed an empty entry
|
|
130
|
+
// so concurrent callers serialize on the same promise.
|
|
131
|
+
entry = {
|
|
132
|
+
token: '',
|
|
133
|
+
expiresAt: 0,
|
|
134
|
+
secretBuf: providedSecretBuf,
|
|
135
|
+
pending: null,
|
|
136
|
+
};
|
|
137
|
+
this._serviceTokenCache.set(cacheKey, entry);
|
|
138
|
+
}
|
|
139
|
+
const pending = this._doFetchServiceToken(key, secret, cacheKey, providedSecretBuf);
|
|
140
|
+
entry.pending = pending;
|
|
141
|
+
try {
|
|
142
|
+
return await pending;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
// Clear the in-flight slot; the entry itself (with fresh token / expiry)
|
|
146
|
+
// is updated inside _doFetchServiceToken before we land here.
|
|
147
|
+
const settled = this._serviceTokenCache.get(cacheKey);
|
|
148
|
+
if (settled) {
|
|
149
|
+
settled.pending = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Perform the actual /auth/service-token request and cache the result.
|
|
155
|
+
* Separated so getServiceToken() can deduplicate concurrent calls.
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
158
|
+
async _doFetchServiceToken(key, secret, cacheKey, secretBuf) {
|
|
43
159
|
const response = await this.makeRequest('POST', '/auth/service-token', { apiKey: key, apiSecret: secret }, { cache: false, retry: false });
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
160
|
+
const expiresAt = Date.now() + response.expiresIn * 1000;
|
|
161
|
+
// Update the entry in-place so any caller that already grabbed a reference
|
|
162
|
+
// (via `_serviceTokenCache.get(...)`) sees the fresh state.
|
|
163
|
+
const entry = this._serviceTokenCache.get(cacheKey);
|
|
164
|
+
if (entry) {
|
|
165
|
+
entry.token = response.token;
|
|
166
|
+
entry.expiresAt = expiresAt;
|
|
167
|
+
entry.secretBuf = secretBuf;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
this._serviceTokenCache.set(cacheKey, {
|
|
171
|
+
token: response.token,
|
|
172
|
+
expiresAt,
|
|
173
|
+
secretBuf,
|
|
174
|
+
pending: null,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return response.token;
|
|
47
178
|
}
|
|
48
179
|
/**
|
|
49
180
|
* Make an authenticated request on behalf of a user using a service token.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Discovery Mixin
|
|
3
|
+
*
|
|
4
|
+
* Privacy-preserving discovery of which address-book contacts are on Oxy.
|
|
5
|
+
*
|
|
6
|
+
* The client hashes emails and phones locally before calling the API.
|
|
7
|
+
* The server responds with only Oxy user IDs and the hashes that matched,
|
|
8
|
+
* so the consumer can map each match back to the local contact that
|
|
9
|
+
* produced it.
|
|
10
|
+
*
|
|
11
|
+
* Hashing rules (must match the server `utils/contactHash.ts` exactly):
|
|
12
|
+
* - SHA-256, hex-encoded, lowercase
|
|
13
|
+
* - Email: `value.trim().toLowerCase()` then digest
|
|
14
|
+
* - Phone: trim → keep a single leading "+" → strip non-digits → prepend "+"
|
|
15
|
+
* if missing → digest
|
|
16
|
+
*
|
|
17
|
+
* Mobile clients can compute these digests with `expo-crypto`'s
|
|
18
|
+
* `digestStringAsync(SHA256, value, { encoding: HEX })`. Web clients should
|
|
19
|
+
* use `SubtleCrypto.digest('SHA-256', ...)`.
|
|
20
|
+
*/
|
|
21
|
+
export function OxyServicesContactsMixin(Base) {
|
|
22
|
+
return class extends Base {
|
|
23
|
+
constructor(...args) {
|
|
24
|
+
super(...args);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Discover which of the caller's contacts are on Oxy.
|
|
28
|
+
*
|
|
29
|
+
* @param hashedEmails - SHA-256 hex digests of normalized emails.
|
|
30
|
+
* @param hashedPhones - SHA-256 hex digests of normalized phone numbers.
|
|
31
|
+
* @returns Matches mapping each hashed identifier to the Oxy user ID it
|
|
32
|
+
* resolved to. Empty arrays are valid for either parameter, but at
|
|
33
|
+
* least one must be non-empty.
|
|
34
|
+
*
|
|
35
|
+
* The server enforces a 200-hash cap per channel per request — callers
|
|
36
|
+
* should batch larger address books client-side.
|
|
37
|
+
*/
|
|
38
|
+
async discoverContacts(hashedEmails, hashedPhones) {
|
|
39
|
+
try {
|
|
40
|
+
return await this.makeRequest('POST', '/contacts/discover', { hashedEmails, hashedPhones }, { cache: false });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw this.handleError(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -291,16 +291,5 @@ export function OxyServicesFeaturesMixin(Base) {
|
|
|
291
291
|
throw this.handleError(error);
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
|
-
// ==================
|
|
295
|
-
// ACCOUNT
|
|
296
|
-
// ==================
|
|
297
|
-
/**
|
|
298
|
-
* Delete user account (requires password confirmation)
|
|
299
|
-
*/
|
|
300
|
-
async deleteAccount(password) {
|
|
301
|
-
return this.withAuthRetry(async () => {
|
|
302
|
-
await this.makeRequest('DELETE', '/account', { password }, { cache: false });
|
|
303
|
-
}, 'deleteAccount');
|
|
304
|
-
}
|
|
305
294
|
};
|
|
306
295
|
}
|
|
@@ -311,10 +311,11 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
311
311
|
{
|
|
312
312
|
configURL: options.configURL,
|
|
313
313
|
clientId: options.clientId,
|
|
314
|
-
//
|
|
315
|
-
|
|
314
|
+
// Older browsers read `nonce` at the top level; Chrome 145+
|
|
315
|
+
// expects it inside `params`. Send both for full coverage.
|
|
316
|
+
nonce: options.nonce,
|
|
316
317
|
params: {
|
|
317
|
-
nonce: options.nonce,
|
|
318
|
+
nonce: options.nonce,
|
|
318
319
|
},
|
|
319
320
|
...(options.loginHint && { loginHint: options.loginHint }),
|
|
320
321
|
},
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Language Methods Mixin
|
|
3
3
|
*/
|
|
4
4
|
import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils.js';
|
|
5
|
+
import { loadAsyncStorage } from '../utils/platformCrypto.js';
|
|
5
6
|
import { isDev } from '../shared/utils/debugUtils.js';
|
|
6
7
|
export function OxyServicesLanguageMixin(Base) {
|
|
7
8
|
return class extends Base {
|
|
@@ -15,9 +16,10 @@ export function OxyServicesLanguageMixin(Base) {
|
|
|
15
16
|
const isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
16
17
|
if (isReactNative) {
|
|
17
18
|
try {
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// `loadAsyncStorage` is per-platform: the RN variant statically imports
|
|
20
|
+
// @react-native-async-storage/async-storage, the default variant throws
|
|
21
|
+
// (never called outside RN because of the `isReactNative` gate above).
|
|
22
|
+
const asyncStorageModule = await loadAsyncStorage();
|
|
21
23
|
const storage = asyncStorageModule.default;
|
|
22
24
|
return {
|
|
23
25
|
getItem: storage.getItem.bind(storage),
|
|
@@ -149,12 +149,16 @@ export function OxyServicesRedirectAuthMixin(Base) {
|
|
|
149
149
|
// Store tokens
|
|
150
150
|
this.storeTokens(accessToken, sessionId);
|
|
151
151
|
this.httpService.setTokens(accessToken);
|
|
152
|
-
// Build session response (minimal
|
|
152
|
+
// Build session response (minimal — full user data is fetched separately
|
|
153
|
+
// by the caller via getCurrentUser() once tokens are stored).
|
|
153
154
|
const session = {
|
|
154
155
|
sessionId,
|
|
155
156
|
deviceId: '', // Not available in redirect flow
|
|
156
157
|
expiresAt: expiresAt || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
157
|
-
user
|
|
158
|
+
// Placeholder user — caller MUST fetch real user data via getCurrentUser()
|
|
159
|
+
// before exposing this session to the application. The empty id signals
|
|
160
|
+
// that the user payload has not yet been populated.
|
|
161
|
+
user: { id: '', username: '' },
|
|
158
162
|
};
|
|
159
163
|
// Clean up URL (remove auth parameters)
|
|
160
164
|
this.cleanAuthCallbackUrl(url);
|
|
@@ -20,8 +20,19 @@ export function OxyServicesSecurityMixin(Base) {
|
|
|
20
20
|
params.offset = offset;
|
|
21
21
|
if (eventType)
|
|
22
22
|
params.eventType = eventType;
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// The API responds with the standard paginated envelope:
|
|
24
|
+
// { data: SecurityActivity[], pagination: { total, limit, offset, hasMore } }
|
|
25
|
+
// SecurityActivityResponse is the flattened shape consumers expect.
|
|
26
|
+
const raw = await this.makeRequest('GET', '/security/activity', params, { cache: false });
|
|
27
|
+
const requestedLimit = typeof params.limit === 'number' ? params.limit : 0;
|
|
28
|
+
const requestedOffset = typeof params.offset === 'number' ? params.offset : 0;
|
|
29
|
+
return {
|
|
30
|
+
data: raw.data ?? [],
|
|
31
|
+
total: raw.pagination?.total ?? raw.data?.length ?? 0,
|
|
32
|
+
limit: raw.pagination?.limit ?? requestedLimit,
|
|
33
|
+
offset: raw.pagination?.offset ?? requestedOffset,
|
|
34
|
+
hasMore: raw.pagination?.hasMore ?? false,
|
|
35
|
+
};
|
|
25
36
|
}
|
|
26
37
|
catch (error) {
|
|
27
38
|
throw this.handleError(error);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { buildSearchParams, buildPaginationParams } from '../utils/apiUtils.js';
|
|
2
|
+
import { KeyManager } from '../crypto/keyManager.js';
|
|
3
|
+
import { SignatureService } from '../crypto/signatureService.js';
|
|
2
4
|
export function OxyServicesUserMixin(Base) {
|
|
3
5
|
return class extends Base {
|
|
4
6
|
constructor(...args) {
|
|
@@ -41,37 +43,22 @@ export function OxyServicesUserMixin(Base) {
|
|
|
41
43
|
cache: true,
|
|
42
44
|
cacheTTL: 2 * 60 * 1000, // 2 minutes cache
|
|
43
45
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (isSearchProfilesResponse(response)) {
|
|
49
|
-
const typedResponse = response;
|
|
50
|
-
const paginationInfo = typedResponse.pagination ?? {
|
|
51
|
-
total: typedResponse.data.length,
|
|
52
|
-
limit: pagination?.limit ?? typedResponse.data.length,
|
|
53
|
-
offset: pagination?.offset ?? 0,
|
|
54
|
-
hasMore: typedResponse.data.length === (pagination?.limit ?? typedResponse.data.length) &&
|
|
55
|
-
(pagination?.limit ?? typedResponse.data.length) > 0,
|
|
56
|
-
};
|
|
57
|
-
return {
|
|
58
|
-
data: typedResponse.data,
|
|
59
|
-
pagination: paginationInfo,
|
|
60
|
-
};
|
|
46
|
+
if (typeof response !== 'object' ||
|
|
47
|
+
response === null ||
|
|
48
|
+
!Array.isArray(response.data)) {
|
|
49
|
+
throw new Error('Unexpected search response format');
|
|
61
50
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
limit
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
// If response is unexpected, throw an error
|
|
74
|
-
throw new Error('Unexpected search response format');
|
|
51
|
+
const paginationInfo = response.pagination ?? {
|
|
52
|
+
total: response.data.length,
|
|
53
|
+
limit: pagination?.limit ?? response.data.length,
|
|
54
|
+
offset: pagination?.offset ?? 0,
|
|
55
|
+
hasMore: response.data.length === (pagination?.limit ?? response.data.length) &&
|
|
56
|
+
(pagination?.limit ?? response.data.length) > 0,
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
data: response.data,
|
|
60
|
+
pagination: paginationInfo,
|
|
61
|
+
};
|
|
75
62
|
}
|
|
76
63
|
catch (error) {
|
|
77
64
|
throw this.handleError(error);
|
|
@@ -153,12 +140,31 @@ export function OxyServicesUserMixin(Base) {
|
|
|
153
140
|
}, 'getCurrentUser');
|
|
154
141
|
}
|
|
155
142
|
/**
|
|
156
|
-
* Update user profile
|
|
157
|
-
*
|
|
143
|
+
* Update user profile.
|
|
144
|
+
*
|
|
145
|
+
* Invalidates the SDK-side response cache for every endpoint that
|
|
146
|
+
* returns the current user (`GET /users/me`, `GET /session/user/*`,
|
|
147
|
+
* `GET /users/<id>`, `GET /profiles/username/*`) so the next read
|
|
148
|
+
* doesn't return a stale snapshot. Without this, a follow-up
|
|
149
|
+
* `getUserBySession` call inside the 2-minute cache window can return
|
|
150
|
+
* the pre-update user — most visibly during onboarding, where it
|
|
151
|
+
* causes the username step to flicker back as if nothing was saved.
|
|
152
|
+
*
|
|
153
|
+
* TanStack Query handles offline queuing automatically.
|
|
158
154
|
*/
|
|
159
155
|
async updateProfile(updates) {
|
|
160
156
|
try {
|
|
161
|
-
|
|
157
|
+
const result = await this.makeRequest('PUT', '/users/me', updates, { cache: false });
|
|
158
|
+
// Bust every cached representation of the current user. We use a
|
|
159
|
+
// prefix sweep rather than an enumeration because the SDK never
|
|
160
|
+
// tracks the set of active session IDs centrally.
|
|
161
|
+
this.clearCacheByPrefix('GET:/session/user/');
|
|
162
|
+
this.clearCacheByPrefix('GET:/users/me');
|
|
163
|
+
this.clearCacheByPrefix('GET:/profiles/username/');
|
|
164
|
+
if (result?.id) {
|
|
165
|
+
this.clearCacheEntry(`GET:/users/${result.id}`);
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
162
168
|
}
|
|
163
169
|
catch (error) {
|
|
164
170
|
const errorAny = error;
|
|
@@ -242,14 +248,29 @@ export function OxyServicesUserMixin(Base) {
|
|
|
242
248
|
}
|
|
243
249
|
}
|
|
244
250
|
/**
|
|
245
|
-
* Delete account permanently
|
|
246
|
-
*
|
|
247
|
-
*
|
|
251
|
+
* Delete account permanently.
|
|
252
|
+
*
|
|
253
|
+
* Signs `delete:{publicKey}:{timestamp}` with the locally-stored identity
|
|
254
|
+
* private key and submits the signature alongside the confirmation text
|
|
255
|
+
* (must equal the user's username). The signature is the cryptographic
|
|
256
|
+
* proof of ownership — only the device holding the private key can issue
|
|
257
|
+
* a valid signature, so no password is required.
|
|
258
|
+
*
|
|
259
|
+
* @param confirmText - Must equal the user's username (verified server-side)
|
|
260
|
+
* @throws If no identity is stored on this device, or signing fails
|
|
248
261
|
*/
|
|
249
|
-
async deleteAccount(
|
|
262
|
+
async deleteAccount(confirmText) {
|
|
250
263
|
try {
|
|
264
|
+
const publicKey = await KeyManager.getPublicKey();
|
|
265
|
+
if (!publicKey) {
|
|
266
|
+
throw new Error('No identity found on this device. Account deletion requires the device that holds your identity key.');
|
|
267
|
+
}
|
|
268
|
+
const timestamp = Date.now();
|
|
269
|
+
const message = `delete:${publicKey}:${timestamp}`;
|
|
270
|
+
const signature = await SignatureService.sign(message);
|
|
251
271
|
return await this.makeRequest('DELETE', '/users/me', {
|
|
252
|
-
|
|
272
|
+
signature,
|
|
273
|
+
timestamp,
|
|
253
274
|
confirmText,
|
|
254
275
|
}, { cache: false });
|
|
255
276
|
}
|