@oxyhq/core 1.11.12 → 1.11.13
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 +7 -2
- package/dist/cjs/mixins/OxyServices.assets.js +9 -4
- package/dist/cjs/mixins/OxyServices.auth.js +27 -0
- 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 +19 -43
- 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/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 +2 -2
- package/dist/esm/mixins/OxyServices.assets.js +9 -4
- package/dist/esm/mixins/OxyServices.auth.js +27 -0
- 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 +19 -10
- 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/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 +36 -3
- 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 +4 -3
- 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 +16 -0
- 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 +1 -0
- 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/platformCrypto.d.ts +87 -0
- package/dist/types/utils/platformCrypto.native.d.ts +54 -0
- package/package.json +28 -1
- package/src/CrossDomainAuth.ts +12 -10
- package/src/HttpService.ts +251 -40
- package/src/OxyServices.base.ts +10 -0
- package/src/OxyServices.ts +9 -4
- 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 +16 -3
- package/src/mixins/OxyServices.assets.ts +15 -11
- package/src/mixins/OxyServices.auth.ts +28 -0
- 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 +19 -10
- 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/platformCrypto.native.ts +101 -0
- package/src/utils/platformCrypto.ts +145 -0
|
@@ -7,6 +7,12 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
7
7
|
/** @internal */ this._serviceTokenExp = 0;
|
|
8
8
|
/** @internal */ this._serviceApiKey = null;
|
|
9
9
|
/** @internal */ this._serviceApiSecret = null;
|
|
10
|
+
/**
|
|
11
|
+
* In-flight promise for service token fetch. Used to deduplicate concurrent
|
|
12
|
+
* calls to getServiceToken() — pattern mirrors AuthManager.refreshToken().
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
this._serviceTokenPromise = null;
|
|
10
16
|
}
|
|
11
17
|
/**
|
|
12
18
|
* Configure service credentials for internal service-to-service communication.
|
|
@@ -27,6 +33,9 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
27
33
|
* Get a service token for internal service-to-service communication.
|
|
28
34
|
* Tokens are short-lived (1h) and automatically cached/refreshed.
|
|
29
35
|
*
|
|
36
|
+
* Concurrent callers share a single in-flight request to avoid hammering
|
|
37
|
+
* `/auth/service-token` when the cache is empty or expired.
|
|
38
|
+
*
|
|
30
39
|
* @param apiKey - DeveloperApp API key (optional if configureServiceAuth was called)
|
|
31
40
|
* @param apiSecret - DeveloperApp API secret (optional if configureServiceAuth was called)
|
|
32
41
|
*/
|
|
@@ -40,6 +49,24 @@ export function OxyServicesAuthMixin(Base) {
|
|
|
40
49
|
if (this._serviceToken && this._serviceTokenExp > Date.now() + 60000) {
|
|
41
50
|
return this._serviceToken;
|
|
42
51
|
}
|
|
52
|
+
// If a fetch is already in-flight, share the same promise
|
|
53
|
+
if (this._serviceTokenPromise) {
|
|
54
|
+
return this._serviceTokenPromise;
|
|
55
|
+
}
|
|
56
|
+
this._serviceTokenPromise = this._doFetchServiceToken(key, secret);
|
|
57
|
+
try {
|
|
58
|
+
return await this._serviceTokenPromise;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
this._serviceTokenPromise = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Perform the actual /auth/service-token request and cache the result.
|
|
66
|
+
* Separated so getServiceToken() can deduplicate concurrent calls.
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
async _doFetchServiceToken(key, secret) {
|
|
43
70
|
const response = await this.makeRequest('POST', '/auth/service-token', { apiKey: key, apiSecret: secret }, { cache: false, retry: false });
|
|
44
71
|
this._serviceToken = response.token;
|
|
45
72
|
this._serviceTokenExp = Date.now() + response.expiresIn * 1000;
|
|
@@ -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
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* and Express.js authentication middleware
|
|
6
6
|
*/
|
|
7
7
|
import { jwtDecode } from 'jwt-decode';
|
|
8
|
+
import { loadNodeCrypto } from '../utils/platformCrypto.js';
|
|
8
9
|
import { CACHE_TIMES } from './mixinHelpers.js';
|
|
9
10
|
export function OxyServicesUtilityMixin(Base) {
|
|
10
11
|
return class extends Base {
|
|
@@ -218,9 +219,15 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
218
219
|
return onError(error);
|
|
219
220
|
return res.status(403).json(error);
|
|
220
221
|
}
|
|
221
|
-
// Verify JWT signature (not just decode)
|
|
222
|
+
// Verify JWT signature (not just decode).
|
|
223
|
+
// This middleware only runs on a Node Express server, but the file
|
|
224
|
+
// is bundled by Metro/Vite for RN/web consumers. `loadNodeCrypto`
|
|
225
|
+
// is per-platform: the RN variant throws (and is never called
|
|
226
|
+
// because service-token middleware is only mounted by Node hosts),
|
|
227
|
+
// so Metro never bundles a reference to Node's built-in.
|
|
222
228
|
try {
|
|
223
|
-
const
|
|
229
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
230
|
+
const { createHmac, timingSafeEqual } = nodeCrypto;
|
|
224
231
|
const [headerB64, payloadB64, signatureB64] = token.split('.');
|
|
225
232
|
if (!headerB64 || !payloadB64 || !signatureB64) {
|
|
226
233
|
throw new Error('Invalid token structure');
|
|
@@ -234,7 +241,6 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
234
241
|
// Timing-safe comparison
|
|
235
242
|
const sigBuf = Buffer.from(signatureB64);
|
|
236
243
|
const expectedBuf = Buffer.from(expectedSig);
|
|
237
|
-
const { timingSafeEqual } = await import('crypto');
|
|
238
244
|
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
|
|
239
245
|
throw new Error('Invalid signature');
|
|
240
246
|
}
|
|
@@ -259,8 +265,8 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
259
265
|
return onError(error);
|
|
260
266
|
return res.status(401).json(error);
|
|
261
267
|
}
|
|
262
|
-
// Check expiration
|
|
263
|
-
if (decoded.exp && decoded.exp
|
|
268
|
+
// Check expiration — reject tokens at exact expiry second (use <=)
|
|
269
|
+
if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
|
|
264
270
|
if (optional) {
|
|
265
271
|
req.userId = null;
|
|
266
272
|
req.user = null;
|
|
@@ -315,7 +321,8 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
315
321
|
return res.status(401).json(error);
|
|
316
322
|
}
|
|
317
323
|
// Check token expiration locally first (fast path)
|
|
318
|
-
|
|
324
|
+
// Reject tokens at exact expiry second (use <=)
|
|
325
|
+
if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
|
|
319
326
|
if (optional) {
|
|
320
327
|
req.userId = null;
|
|
321
328
|
req.user = null;
|
|
@@ -482,8 +489,8 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
482
489
|
if (!userId) {
|
|
483
490
|
return next(new Error('Invalid token payload'));
|
|
484
491
|
}
|
|
485
|
-
// Check expiration
|
|
486
|
-
if (decoded.exp && decoded.exp
|
|
492
|
+
// Check expiration — reject tokens at exact expiry second (use <=)
|
|
493
|
+
if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
|
|
487
494
|
return next(new Error('Token expired'));
|
|
488
495
|
}
|
|
489
496
|
// Validate session if available
|
|
@@ -500,12 +507,14 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
500
507
|
return next(new Error('Session validation failed'));
|
|
501
508
|
}
|
|
502
509
|
}
|
|
503
|
-
// Attach user data to socket
|
|
510
|
+
// Attach user data to socket. We expose BOTH `socket.data.userId`
|
|
511
|
+
// (the official Socket.IO data slot) and `socket.user` because
|
|
512
|
+
// every consumer in this ecosystem (Mention, Allo, api/server.ts)
|
|
513
|
+
// reads from `socket.user.id`.
|
|
504
514
|
socket.data = socket.data || {};
|
|
505
515
|
socket.data.userId = userId;
|
|
506
516
|
socket.data.sessionId = decoded.sessionId || null;
|
|
507
517
|
socket.data.token = token;
|
|
508
|
-
// Also set on socket.user for backward compatibility
|
|
509
518
|
socket.user = { id: userId, userId, sessionId: decoded.sessionId };
|
|
510
519
|
if (debug) {
|
|
511
520
|
console.log(`[oxy.authSocket] OK user=${userId}`);
|
package/dist/esm/mixins/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { OxyServicesUtilityMixin } from './OxyServices.utility.js';
|
|
|
24
24
|
import { OxyServicesFeaturesMixin } from './OxyServices.features.js';
|
|
25
25
|
import { OxyServicesTopicsMixin } from './OxyServices.topics.js';
|
|
26
26
|
import { OxyServicesManagedAccountsMixin } from './OxyServices.managedAccounts.js';
|
|
27
|
+
import { OxyServicesContactsMixin } from './OxyServices.contacts.js';
|
|
27
28
|
/**
|
|
28
29
|
* Mixin pipeline - applied in order from first to last.
|
|
29
30
|
*
|
|
@@ -62,6 +63,7 @@ const MIXIN_PIPELINE = [
|
|
|
62
63
|
OxyServicesFeaturesMixin,
|
|
63
64
|
OxyServicesTopicsMixin,
|
|
64
65
|
OxyServicesManagedAccountsMixin,
|
|
66
|
+
OxyServicesContactsMixin,
|
|
65
67
|
// Utility (last, can use all above)
|
|
66
68
|
OxyServicesUtilityMixin,
|
|
67
69
|
];
|
|
@@ -69,12 +71,18 @@ const MIXIN_PIPELINE = [
|
|
|
69
71
|
* Composes all OxyServices mixins using a pipeline pattern.
|
|
70
72
|
*
|
|
71
73
|
* This is equivalent to the nested calls but more readable and maintainable.
|
|
72
|
-
* Adding a new mixin:
|
|
74
|
+
* Adding a new mixin: add it to MIXIN_PIPELINE at the appropriate position
|
|
75
|
+
* AND extend `AllMixinInstances` so its methods are visible to consumers.
|
|
73
76
|
*
|
|
74
|
-
*
|
|
77
|
+
* The cast through `unknown` carries the runtime augmentation chain into the
|
|
78
|
+
* static type system. `Array.reduce` cannot track each mixin's generic
|
|
79
|
+
* refinement, so we assert the final shape exposed by all mixins together.
|
|
80
|
+
*
|
|
81
|
+
* @returns The fully composed OxyServices constructor with all mixins applied
|
|
75
82
|
*/
|
|
76
83
|
export function composeOxyServices() {
|
|
77
|
-
|
|
84
|
+
const composed = MIXIN_PIPELINE.reduce((Base, mixin) => mixin(Base), OxyServicesBase);
|
|
85
|
+
return composed;
|
|
78
86
|
}
|
|
79
87
|
// Export the pipeline for testing/debugging
|
|
80
88
|
export { MIXIN_PIPELINE };
|
|
@@ -2,6 +2,72 @@
|
|
|
2
2
|
* Shared account types and pure helper functions.
|
|
3
3
|
* Used by both @oxyhq/services (React Native) and @oxyhq/auth (Web) account stores.
|
|
4
4
|
*/
|
|
5
|
+
import { translate } from '../i18n/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Truncate a long public key for display, e.g. `0x12345678…`.
|
|
8
|
+
* Falls back to the raw key if it's too short to truncate.
|
|
9
|
+
*/
|
|
10
|
+
export const formatPublicKeyHandle = (publicKey) => {
|
|
11
|
+
const cleaned = publicKey.startsWith('0x') ? publicKey.slice(2) : publicKey;
|
|
12
|
+
if (cleaned.length <= 8)
|
|
13
|
+
return `0x${cleaned}`;
|
|
14
|
+
return `0x${cleaned.slice(0, 8)}…`;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a friendly display name for a user.
|
|
18
|
+
*
|
|
19
|
+
* Order of preference:
|
|
20
|
+
* 1. `name.full`, or composed `name.first name.last`
|
|
21
|
+
* 2. `name` (when stored as a plain string)
|
|
22
|
+
* 3. `username`
|
|
23
|
+
* 4. `Account 0x12345678…` (derived from publicKey, when present)
|
|
24
|
+
* 5. Translated fallback (e.g. "Unnamed")
|
|
25
|
+
*
|
|
26
|
+
* The translation key `common.unnamed` is used for the final fallback. If the
|
|
27
|
+
* caller does not pass a locale, the default English translation is used.
|
|
28
|
+
*/
|
|
29
|
+
export const getAccountDisplayName = (user, locale) => {
|
|
30
|
+
if (!user)
|
|
31
|
+
return translate(locale, 'common.unnamed');
|
|
32
|
+
const { name, username, publicKey } = user;
|
|
33
|
+
if (name && typeof name === 'object') {
|
|
34
|
+
if (typeof name.full === 'string' && name.full.trim())
|
|
35
|
+
return name.full.trim();
|
|
36
|
+
const first = typeof name.first === 'string' ? name.first.trim() : '';
|
|
37
|
+
const last = typeof name.last === 'string' ? name.last.trim() : '';
|
|
38
|
+
const composed = [first, last].filter(Boolean).join(' ').trim();
|
|
39
|
+
if (composed)
|
|
40
|
+
return composed;
|
|
41
|
+
}
|
|
42
|
+
else if (typeof name === 'string' && name.trim()) {
|
|
43
|
+
return name.trim();
|
|
44
|
+
}
|
|
45
|
+
if (typeof username === 'string' && username.trim())
|
|
46
|
+
return username.trim();
|
|
47
|
+
if (typeof publicKey === 'string' && publicKey.length > 0) {
|
|
48
|
+
return translate(locale, 'common.accountFallback', {
|
|
49
|
+
handle: formatPublicKeyHandle(publicKey),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return translate(locale, 'common.unnamed');
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a `@handle` style identifier for a user.
|
|
56
|
+
*
|
|
57
|
+
* Returns the bare username when present (without the `@`), otherwise a
|
|
58
|
+
* truncated public-key handle (`0x12345678…`), or `undefined` when neither is
|
|
59
|
+
* available — callers can decide whether to hide the line entirely.
|
|
60
|
+
*/
|
|
61
|
+
export const getAccountFallbackHandle = (user) => {
|
|
62
|
+
if (!user)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (typeof user.username === 'string' && user.username.trim())
|
|
65
|
+
return user.username.trim();
|
|
66
|
+
if (typeof user.publicKey === 'string' && user.publicKey.length > 0) {
|
|
67
|
+
return formatPublicKeyHandle(user.publicKey);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
5
71
|
/**
|
|
6
72
|
* Build an ordered array of QuickAccounts from a map and order list.
|
|
7
73
|
*/
|
|
@@ -23,7 +89,7 @@ export const buildAccountsArray = (accounts, order) => {
|
|
|
23
89
|
* @param getFileDownloadUrl - Function to generate avatar download URL from file ID
|
|
24
90
|
*/
|
|
25
91
|
export const createQuickAccount = (sessionId, userData, existingAccount, getFileDownloadUrl) => {
|
|
26
|
-
const displayName = userData
|
|
92
|
+
const displayName = getAccountDisplayName(userData);
|
|
27
93
|
const userId = userData.id || (typeof userData._id === 'string' ? userData._id : userData._id?.toString());
|
|
28
94
|
// Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
|
|
29
95
|
let avatarUrl;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadAsyncStorage } from './platformCrypto.js';
|
|
1
2
|
/**
|
|
2
3
|
* Client-side device management utility
|
|
3
4
|
* Handles persistent device identification across app sessions
|
|
@@ -15,9 +16,10 @@ export class DeviceManager {
|
|
|
15
16
|
static async getStorage() {
|
|
16
17
|
if (this.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),
|