@oxyhq/core 1.11.11 → 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 +227 -51
- 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 +70 -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/asyncUtils.js +34 -5
- 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 +228 -52
- 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 +70 -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/asyncUtils.js +34 -5
- 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 +40 -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/asyncUtils.d.ts +6 -2
- 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 +264 -51
- 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 -29
- 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 +90 -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/__tests__/asyncUtils.test.ts +187 -0
- package/src/utils/accountUtils.ts +82 -2
- package/src/utils/asyncUtils.ts +39 -9
- package/src/utils/deviceManager.ts +7 -4
- package/src/utils/platformCrypto.native.ts +101 -0
- package/src/utils/platformCrypto.ts +145 -0
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
|
|
22
|
+
import type { OxyServicesBase } from '../OxyServices.base';
|
|
23
|
+
|
|
24
|
+
/** A single match returned by `POST /contacts/discover`. */
|
|
25
|
+
export interface ContactDiscoveryMatch {
|
|
26
|
+
/** Oxy user ID (MongoDB ObjectId hex string). */
|
|
27
|
+
userId: string;
|
|
28
|
+
/** The hashed identifier from the request that matched this user. */
|
|
29
|
+
hashedIdentifier: string;
|
|
30
|
+
/** Whether the match came from the email index or phone index. */
|
|
31
|
+
matchType: 'email' | 'phone';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Response shape of `POST /contacts/discover`. */
|
|
35
|
+
export interface ContactDiscoveryResponse {
|
|
36
|
+
matches: ContactDiscoveryMatch[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function OxyServicesContactsMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
40
|
+
return class extends Base {
|
|
41
|
+
constructor(...args: any[]) {
|
|
42
|
+
super(...(args as [any]));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Discover which of the caller's contacts are on Oxy.
|
|
47
|
+
*
|
|
48
|
+
* @param hashedEmails - SHA-256 hex digests of normalized emails.
|
|
49
|
+
* @param hashedPhones - SHA-256 hex digests of normalized phone numbers.
|
|
50
|
+
* @returns Matches mapping each hashed identifier to the Oxy user ID it
|
|
51
|
+
* resolved to. Empty arrays are valid for either parameter, but at
|
|
52
|
+
* least one must be non-empty.
|
|
53
|
+
*
|
|
54
|
+
* The server enforces a 200-hash cap per channel per request — callers
|
|
55
|
+
* should batch larger address books client-side.
|
|
56
|
+
*/
|
|
57
|
+
async discoverContacts(
|
|
58
|
+
hashedEmails: string[],
|
|
59
|
+
hashedPhones: string[],
|
|
60
|
+
): Promise<ContactDiscoveryResponse> {
|
|
61
|
+
try {
|
|
62
|
+
return await this.makeRequest<ContactDiscoveryResponse>(
|
|
63
|
+
'POST',
|
|
64
|
+
'/contacts/discover',
|
|
65
|
+
{ hashedEmails, hashedPhones },
|
|
66
|
+
{ cache: false },
|
|
67
|
+
);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw this.handleError(error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -412,17 +412,7 @@ export function OxyServicesFeaturesMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
// ==================
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Delete user account (requires password confirmation)
|
|
421
|
-
*/
|
|
422
|
-
async deleteAccount(password: string): Promise<void> {
|
|
423
|
-
return this.withAuthRetry(async () => {
|
|
424
|
-
await this.makeRequest('DELETE', '/account', { password }, { cache: false });
|
|
425
|
-
}, 'deleteAccount');
|
|
426
|
-
}
|
|
415
|
+
// Account deletion lives in OxyServices.user mixin — it requires
|
|
416
|
+
// an identity-key signature (not just a password) and hits DELETE /users/me.
|
|
427
417
|
};
|
|
428
418
|
}
|
|
@@ -372,10 +372,11 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
372
372
|
{
|
|
373
373
|
configURL: options.configURL,
|
|
374
374
|
clientId: options.clientId,
|
|
375
|
-
//
|
|
376
|
-
|
|
375
|
+
// Older browsers read `nonce` at the top level; Chrome 145+
|
|
376
|
+
// expects it inside `params`. Send both for full coverage.
|
|
377
|
+
nonce: options.nonce,
|
|
377
378
|
params: {
|
|
378
|
-
nonce: options.nonce,
|
|
379
|
+
nonce: options.nonce,
|
|
379
380
|
},
|
|
380
381
|
...(options.loginHint && { loginHint: options.loginHint }),
|
|
381
382
|
},
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
|
|
5
5
|
import type { LanguageMetadata } from '../utils/languageUtils';
|
|
6
6
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
7
|
+
import { loadAsyncStorage } from '../utils/platformCrypto';
|
|
7
8
|
import { isDev } from '../shared/utils/debugUtils';
|
|
8
9
|
|
|
9
10
|
export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
@@ -23,10 +24,11 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
23
24
|
|
|
24
25
|
if (isReactNative) {
|
|
25
26
|
try {
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
27
|
+
// `loadAsyncStorage` is per-platform: the RN variant statically imports
|
|
28
|
+
// @react-native-async-storage/async-storage, the default variant throws
|
|
29
|
+
// (never called outside RN because of the `isReactNative` gate above).
|
|
30
|
+
const asyncStorageModule = await loadAsyncStorage();
|
|
31
|
+
const storage = asyncStorageModule.default;
|
|
30
32
|
return {
|
|
31
33
|
getItem: storage.getItem.bind(storage),
|
|
32
34
|
setItem: storage.setItem.bind(storage),
|
|
@@ -177,12 +177,16 @@ export function OxyServicesRedirectAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
177
177
|
this.storeTokens(accessToken, sessionId);
|
|
178
178
|
this.httpService.setTokens(accessToken);
|
|
179
179
|
|
|
180
|
-
// Build session response (minimal
|
|
180
|
+
// Build session response (minimal — full user data is fetched separately
|
|
181
|
+
// by the caller via getCurrentUser() once tokens are stored).
|
|
181
182
|
const session: SessionLoginResponse = {
|
|
182
183
|
sessionId,
|
|
183
184
|
deviceId: '', // Not available in redirect flow
|
|
184
185
|
expiresAt: expiresAt || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
185
|
-
user
|
|
186
|
+
// Placeholder user — caller MUST fetch real user data via getCurrentUser()
|
|
187
|
+
// before exposing this session to the application. The empty id signals
|
|
188
|
+
// that the user payload has not yet been populated.
|
|
189
|
+
user: { id: '', username: '' },
|
|
186
190
|
};
|
|
187
191
|
|
|
188
192
|
// Clean up URL (remove auth parameters)
|
|
@@ -24,19 +24,29 @@ export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
24
24
|
eventType?: SecurityEventType
|
|
25
25
|
): Promise<SecurityActivityResponse> {
|
|
26
26
|
try {
|
|
27
|
-
const params:
|
|
27
|
+
const params: Record<string, unknown> = {};
|
|
28
28
|
if (limit !== undefined) params.limit = limit;
|
|
29
29
|
if (offset !== undefined) params.offset = offset;
|
|
30
30
|
if (eventType) params.eventType = eventType;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
// The API responds with the standard paginated envelope:
|
|
33
|
+
// { data: SecurityActivity[], pagination: { total, limit, offset, hasMore } }
|
|
34
|
+
// SecurityActivityResponse is the flattened shape consumers expect.
|
|
35
|
+
const raw = await this.makeRequest<{
|
|
36
|
+
data: SecurityActivity[];
|
|
37
|
+
pagination: { total: number; limit: number; offset: number; hasMore: boolean };
|
|
38
|
+
}>('GET', '/security/activity', params, { cache: false });
|
|
39
|
+
|
|
40
|
+
const requestedLimit = typeof params.limit === 'number' ? params.limit : 0;
|
|
41
|
+
const requestedOffset = typeof params.offset === 'number' ? params.offset : 0;
|
|
38
42
|
|
|
39
|
-
return
|
|
43
|
+
return {
|
|
44
|
+
data: raw.data ?? [],
|
|
45
|
+
total: raw.pagination?.total ?? raw.data?.length ?? 0,
|
|
46
|
+
limit: raw.pagination?.limit ?? requestedLimit,
|
|
47
|
+
offset: raw.pagination?.offset ?? requestedOffset,
|
|
48
|
+
hasMore: raw.pagination?.hasMore ?? false,
|
|
49
|
+
};
|
|
40
50
|
} catch (error) {
|
|
41
51
|
throw this.handleError(error);
|
|
42
52
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User Management Methods Mixin
|
|
3
3
|
*/
|
|
4
|
-
import type { User, Notification, SearchProfilesResponse, PaginationInfo } from '../models/interfaces';
|
|
4
|
+
import type { User, Notification, SearchProfilesResponse, PaginationInfo, PrivacySettings } from '../models/interfaces';
|
|
5
5
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
6
6
|
import { buildSearchParams, buildPaginationParams, type PaginationParams } from '../utils/apiUtils';
|
|
7
|
+
import { KeyManager } from '../crypto/keyManager';
|
|
8
|
+
import { SignatureService } from '../crypto/signatureService';
|
|
7
9
|
|
|
8
10
|
export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T) {
|
|
9
11
|
return class extends Base {
|
|
@@ -24,6 +26,24 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Lightweight username lookup for login flows.
|
|
31
|
+
* Returns minimal public info: exists, color, avatar, displayName.
|
|
32
|
+
* Faster than getProfileByUsername — no stats, no formatting.
|
|
33
|
+
*/
|
|
34
|
+
async lookupUsername(username: string): Promise<{
|
|
35
|
+
exists: boolean;
|
|
36
|
+
username: string;
|
|
37
|
+
color: string | null;
|
|
38
|
+
avatar: string | null;
|
|
39
|
+
displayName: string;
|
|
40
|
+
}> {
|
|
41
|
+
return await this.makeRequest('GET', `/auth/lookup/${encodeURIComponent(username)}`, undefined, {
|
|
42
|
+
cache: true,
|
|
43
|
+
cacheTTL: 60 * 1000, // 1 minute cache
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
/**
|
|
28
48
|
* Search user profiles
|
|
29
49
|
*/
|
|
@@ -33,7 +53,7 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
33
53
|
const searchParams = buildSearchParams(params);
|
|
34
54
|
const paramsObj = Object.fromEntries(searchParams.entries());
|
|
35
55
|
|
|
36
|
-
const response = await this.makeRequest<SearchProfilesResponse
|
|
56
|
+
const response = await this.makeRequest<SearchProfilesResponse>(
|
|
37
57
|
'GET',
|
|
38
58
|
'/profiles/search',
|
|
39
59
|
paramsObj,
|
|
@@ -43,43 +63,26 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
43
63
|
}
|
|
44
64
|
);
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (isSearchProfilesResponse(response)) {
|
|
53
|
-
const typedResponse = response;
|
|
54
|
-
const paginationInfo: PaginationInfo = typedResponse.pagination ?? {
|
|
55
|
-
total: typedResponse.data.length,
|
|
56
|
-
limit: pagination?.limit ?? typedResponse.data.length,
|
|
57
|
-
offset: pagination?.offset ?? 0,
|
|
58
|
-
hasMore: typedResponse.data.length === (pagination?.limit ?? typedResponse.data.length) &&
|
|
59
|
-
(pagination?.limit ?? typedResponse.data.length) > 0,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
data: typedResponse.data,
|
|
64
|
-
pagination: paginationInfo,
|
|
65
|
-
};
|
|
66
|
+
if (
|
|
67
|
+
typeof response !== 'object' ||
|
|
68
|
+
response === null ||
|
|
69
|
+
!Array.isArray(response.data)
|
|
70
|
+
) {
|
|
71
|
+
throw new Error('Unexpected search response format');
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
limit
|
|
74
|
-
|
|
75
|
-
hasMore: fallbackLimit > 0 && response.length === fallbackLimit,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
return { data: response, pagination: fallbackPagination };
|
|
79
|
-
}
|
|
74
|
+
const paginationInfo: PaginationInfo = response.pagination ?? {
|
|
75
|
+
total: response.data.length,
|
|
76
|
+
limit: pagination?.limit ?? response.data.length,
|
|
77
|
+
offset: pagination?.offset ?? 0,
|
|
78
|
+
hasMore: response.data.length === (pagination?.limit ?? response.data.length) &&
|
|
79
|
+
(pagination?.limit ?? response.data.length) > 0,
|
|
80
|
+
};
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
return {
|
|
83
|
+
data: response.data,
|
|
84
|
+
pagination: paginationInfo,
|
|
85
|
+
};
|
|
83
86
|
} catch (error) {
|
|
84
87
|
throw this.handleError(error);
|
|
85
88
|
}
|
|
@@ -188,12 +191,33 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
/**
|
|
191
|
-
* Update user profile
|
|
192
|
-
*
|
|
194
|
+
* Update user profile.
|
|
195
|
+
*
|
|
196
|
+
* Invalidates the SDK-side response cache for every endpoint that
|
|
197
|
+
* returns the current user (`GET /users/me`, `GET /session/user/*`,
|
|
198
|
+
* `GET /users/<id>`, `GET /profiles/username/*`) so the next read
|
|
199
|
+
* doesn't return a stale snapshot. Without this, a follow-up
|
|
200
|
+
* `getUserBySession` call inside the 2-minute cache window can return
|
|
201
|
+
* the pre-update user — most visibly during onboarding, where it
|
|
202
|
+
* causes the username step to flicker back as if nothing was saved.
|
|
203
|
+
*
|
|
204
|
+
* TanStack Query handles offline queuing automatically.
|
|
193
205
|
*/
|
|
194
|
-
async updateProfile(updates:
|
|
206
|
+
async updateProfile(updates: Partial<User>): Promise<User> {
|
|
195
207
|
try {
|
|
196
|
-
|
|
208
|
+
const result = await this.makeRequest<User>('PUT', '/users/me', updates, { cache: false });
|
|
209
|
+
|
|
210
|
+
// Bust every cached representation of the current user. We use a
|
|
211
|
+
// prefix sweep rather than an enumeration because the SDK never
|
|
212
|
+
// tracks the set of active session IDs centrally.
|
|
213
|
+
this.clearCacheByPrefix('GET:/session/user/');
|
|
214
|
+
this.clearCacheByPrefix('GET:/users/me');
|
|
215
|
+
this.clearCacheByPrefix('GET:/profiles/username/');
|
|
216
|
+
if (result?.id) {
|
|
217
|
+
this.clearCacheEntry(`GET:/users/${result.id}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result;
|
|
197
221
|
} catch (error) {
|
|
198
222
|
const errorAny = error as any;
|
|
199
223
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -219,10 +243,10 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
219
243
|
* Get privacy settings for a user
|
|
220
244
|
* @param userId - The user ID (defaults to current user)
|
|
221
245
|
*/
|
|
222
|
-
async getPrivacySettings(userId?: string): Promise<
|
|
246
|
+
async getPrivacySettings(userId?: string): Promise<PrivacySettings> {
|
|
223
247
|
try {
|
|
224
248
|
const id = userId || (await this.getCurrentUser()).id;
|
|
225
|
-
return await this.makeRequest<
|
|
249
|
+
return await this.makeRequest<PrivacySettings>('GET', `/privacy/${id}/privacy`, undefined, {
|
|
226
250
|
cache: true,
|
|
227
251
|
cacheTTL: 2 * 60 * 1000, // 2 minutes cache
|
|
228
252
|
});
|
|
@@ -236,10 +260,10 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
236
260
|
* @param settings - Partial privacy settings object
|
|
237
261
|
* @param userId - The user ID (defaults to current user)
|
|
238
262
|
*/
|
|
239
|
-
async updatePrivacySettings(settings:
|
|
263
|
+
async updatePrivacySettings(settings: Partial<PrivacySettings>, userId?: string): Promise<PrivacySettings> {
|
|
240
264
|
try {
|
|
241
265
|
const id = userId || (await this.getCurrentUser()).id;
|
|
242
|
-
return await this.makeRequest<
|
|
266
|
+
return await this.makeRequest<PrivacySettings>('PATCH', `/privacy/${id}/privacy`, settings, {
|
|
243
267
|
cache: false,
|
|
244
268
|
});
|
|
245
269
|
} catch (error) {
|
|
@@ -281,14 +305,31 @@ export function OxyServicesUserMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
281
305
|
}
|
|
282
306
|
|
|
283
307
|
/**
|
|
284
|
-
* Delete account permanently
|
|
285
|
-
*
|
|
286
|
-
*
|
|
308
|
+
* Delete account permanently.
|
|
309
|
+
*
|
|
310
|
+
* Signs `delete:{publicKey}:{timestamp}` with the locally-stored identity
|
|
311
|
+
* private key and submits the signature alongside the confirmation text
|
|
312
|
+
* (must equal the user's username). The signature is the cryptographic
|
|
313
|
+
* proof of ownership — only the device holding the private key can issue
|
|
314
|
+
* a valid signature, so no password is required.
|
|
315
|
+
*
|
|
316
|
+
* @param confirmText - Must equal the user's username (verified server-side)
|
|
317
|
+
* @throws If no identity is stored on this device, or signing fails
|
|
287
318
|
*/
|
|
288
|
-
async deleteAccount(
|
|
319
|
+
async deleteAccount(confirmText: string): Promise<{ message: string }> {
|
|
289
320
|
try {
|
|
321
|
+
const publicKey = await KeyManager.getPublicKey();
|
|
322
|
+
if (!publicKey) {
|
|
323
|
+
throw new Error('No identity found on this device. Account deletion requires the device that holds your identity key.');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const timestamp = Date.now();
|
|
327
|
+
const message = `delete:${publicKey}:${timestamp}`;
|
|
328
|
+
const signature = await SignatureService.sign(message);
|
|
329
|
+
|
|
290
330
|
return await this.makeRequest<{ message: string }>('DELETE', '/users/me', {
|
|
291
|
-
|
|
331
|
+
signature,
|
|
332
|
+
timestamp,
|
|
292
333
|
confirmText,
|
|
293
334
|
}, { cache: false });
|
|
294
335
|
} catch (error) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { jwtDecode } from 'jwt-decode';
|
|
8
8
|
import type { ApiError, User } from '../models/interfaces';
|
|
9
9
|
import type { OxyServicesBase } from '../OxyServices.base';
|
|
10
|
+
import { loadNodeCrypto } from '../utils/platformCrypto';
|
|
10
11
|
import { CACHE_TIMES } from './mixinHelpers';
|
|
11
12
|
|
|
12
13
|
interface JwtPayload {
|
|
@@ -297,9 +298,15 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
297
298
|
return res.status(403).json(error);
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
// Verify JWT signature (not just decode)
|
|
301
|
+
// Verify JWT signature (not just decode).
|
|
302
|
+
// This middleware only runs on a Node Express server, but the file
|
|
303
|
+
// is bundled by Metro/Vite for RN/web consumers. `loadNodeCrypto`
|
|
304
|
+
// is per-platform: the RN variant throws (and is never called
|
|
305
|
+
// because service-token middleware is only mounted by Node hosts),
|
|
306
|
+
// so Metro never bundles a reference to Node's built-in.
|
|
301
307
|
try {
|
|
302
|
-
const
|
|
308
|
+
const nodeCrypto = await loadNodeCrypto();
|
|
309
|
+
const { createHmac, timingSafeEqual } = nodeCrypto;
|
|
303
310
|
const [headerB64, payloadB64, signatureB64] = token.split('.');
|
|
304
311
|
if (!headerB64 || !payloadB64 || !signatureB64) {
|
|
305
312
|
throw new Error('Invalid token structure');
|
|
@@ -314,7 +321,6 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
314
321
|
// Timing-safe comparison
|
|
315
322
|
const sigBuf = Buffer.from(signatureB64);
|
|
316
323
|
const expectedBuf = Buffer.from(expectedSig);
|
|
317
|
-
const { timingSafeEqual } = await import('crypto');
|
|
318
324
|
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
|
|
319
325
|
throw new Error('Invalid signature');
|
|
320
326
|
}
|
|
@@ -339,8 +345,8 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
339
345
|
return res.status(401).json(error);
|
|
340
346
|
}
|
|
341
347
|
|
|
342
|
-
// Check expiration
|
|
343
|
-
if (decoded.exp && decoded.exp
|
|
348
|
+
// Check expiration — reject tokens at exact expiry second (use <=)
|
|
349
|
+
if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
|
|
344
350
|
if (optional) {
|
|
345
351
|
req.userId = null;
|
|
346
352
|
req.user = null;
|
|
@@ -400,7 +406,8 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
400
406
|
}
|
|
401
407
|
|
|
402
408
|
// Check token expiration locally first (fast path)
|
|
403
|
-
|
|
409
|
+
// Reject tokens at exact expiry second (use <=)
|
|
410
|
+
if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
|
|
404
411
|
if (optional) {
|
|
405
412
|
req.userId = null;
|
|
406
413
|
req.user = null;
|
|
@@ -578,8 +585,8 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
578
585
|
return next(new Error('Invalid token payload'));
|
|
579
586
|
}
|
|
580
587
|
|
|
581
|
-
// Check expiration
|
|
582
|
-
if (decoded.exp && decoded.exp
|
|
588
|
+
// Check expiration — reject tokens at exact expiry second (use <=)
|
|
589
|
+
if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
|
|
583
590
|
return next(new Error('Token expired'));
|
|
584
591
|
}
|
|
585
592
|
|
|
@@ -597,13 +604,15 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
|
|
|
597
604
|
}
|
|
598
605
|
}
|
|
599
606
|
|
|
600
|
-
// Attach user data to socket
|
|
607
|
+
// Attach user data to socket. We expose BOTH `socket.data.userId`
|
|
608
|
+
// (the official Socket.IO data slot) and `socket.user` because
|
|
609
|
+
// every consumer in this ecosystem (Mention, Allo, api/server.ts)
|
|
610
|
+
// reads from `socket.user.id`.
|
|
601
611
|
socket.data = socket.data || {};
|
|
602
612
|
socket.data.userId = userId;
|
|
603
613
|
socket.data.sessionId = decoded.sessionId || null;
|
|
604
614
|
socket.data.token = token;
|
|
605
615
|
|
|
606
|
-
// Also set on socket.user for backward compatibility
|
|
607
616
|
socket.user = { id: userId, userId, sessionId: decoded.sessionId };
|
|
608
617
|
|
|
609
618
|
if (debug) {
|
package/src/mixins/index.ts
CHANGED
|
@@ -25,9 +25,53 @@ import { OxyServicesUtilityMixin } from './OxyServices.utility';
|
|
|
25
25
|
import { OxyServicesFeaturesMixin } from './OxyServices.features';
|
|
26
26
|
import { OxyServicesTopicsMixin } from './OxyServices.topics';
|
|
27
27
|
import { OxyServicesManagedAccountsMixin } from './OxyServices.managedAccounts';
|
|
28
|
+
import { OxyServicesContactsMixin } from './OxyServices.contacts';
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Instance shape of every mixin in the pipeline, intersected. The runtime
|
|
32
|
+
* `composeOxyServices()` produces a class whose instances expose all of
|
|
33
|
+
* these methods; we surface that to TypeScript via this intersection so the
|
|
34
|
+
* `extends` site in `OxyServices.ts` can avoid an `as any` cast.
|
|
35
|
+
*
|
|
36
|
+
* If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
|
|
37
|
+
* are visible without a cast.
|
|
38
|
+
*/
|
|
39
|
+
type AllMixinInstances =
|
|
40
|
+
& InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>>
|
|
41
|
+
& InstanceType<ReturnType<typeof OxyServicesFedCMMixin<typeof OxyServicesBase>>>
|
|
42
|
+
& InstanceType<ReturnType<typeof OxyServicesPopupAuthMixin<typeof OxyServicesBase>>>
|
|
43
|
+
& InstanceType<ReturnType<typeof OxyServicesRedirectAuthMixin<typeof OxyServicesBase>>>
|
|
44
|
+
& InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>>
|
|
45
|
+
& InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>>
|
|
46
|
+
& InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>>
|
|
47
|
+
& InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>>
|
|
48
|
+
& InstanceType<ReturnType<typeof OxyServicesKarmaMixin<typeof OxyServicesBase>>>
|
|
49
|
+
& InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>>
|
|
50
|
+
& InstanceType<ReturnType<typeof OxyServicesDeveloperMixin<typeof OxyServicesBase>>>
|
|
51
|
+
& InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>>
|
|
52
|
+
& InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>>
|
|
53
|
+
& InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>>
|
|
54
|
+
& InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>>
|
|
55
|
+
& InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>>
|
|
56
|
+
& InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>>
|
|
57
|
+
& InstanceType<ReturnType<typeof OxyServicesManagedAccountsMixin<typeof OxyServicesBase>>>
|
|
58
|
+
& InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>>
|
|
59
|
+
& InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Constructor type for the fully composed mixin pipeline. Each mixin returns
|
|
63
|
+
* a new constructor that augments its input; reducing across the pipeline
|
|
64
|
+
* yields an instance with every mixin's methods.
|
|
65
|
+
*/
|
|
66
|
+
export type ComposedOxyServicesConstructor = new (config: import('../OxyServices.base').OxyConfig) => AllMixinInstances;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A mixin function: takes a constructor and returns an augmented constructor.
|
|
70
|
+
* Each individual mixin uses a `<T extends typeof OxyServicesBase>` generic
|
|
71
|
+
* to preserve its specific augmentations, but those refinements are
|
|
72
|
+
* intentionally collapsed across the `reduce` call below.
|
|
73
|
+
*/
|
|
74
|
+
type MixinFunction = (Base: new (...args: unknown[]) => OxyServicesBase) => new (...args: unknown[]) => OxyServicesBase;
|
|
31
75
|
|
|
32
76
|
/**
|
|
33
77
|
* Mixin pipeline - applied in order from first to last.
|
|
@@ -70,6 +114,7 @@ const MIXIN_PIPELINE: MixinFunction[] = [
|
|
|
70
114
|
OxyServicesFeaturesMixin,
|
|
71
115
|
OxyServicesTopicsMixin,
|
|
72
116
|
OxyServicesManagedAccountsMixin,
|
|
117
|
+
OxyServicesContactsMixin,
|
|
73
118
|
|
|
74
119
|
// Utility (last, can use all above)
|
|
75
120
|
OxyServicesUtilityMixin,
|
|
@@ -79,15 +124,21 @@ const MIXIN_PIPELINE: MixinFunction[] = [
|
|
|
79
124
|
* Composes all OxyServices mixins using a pipeline pattern.
|
|
80
125
|
*
|
|
81
126
|
* This is equivalent to the nested calls but more readable and maintainable.
|
|
82
|
-
* Adding a new mixin:
|
|
127
|
+
* Adding a new mixin: add it to MIXIN_PIPELINE at the appropriate position
|
|
128
|
+
* AND extend `AllMixinInstances` so its methods are visible to consumers.
|
|
129
|
+
*
|
|
130
|
+
* The cast through `unknown` carries the runtime augmentation chain into the
|
|
131
|
+
* static type system. `Array.reduce` cannot track each mixin's generic
|
|
132
|
+
* refinement, so we assert the final shape exposed by all mixins together.
|
|
83
133
|
*
|
|
84
|
-
* @returns The fully composed OxyServices
|
|
134
|
+
* @returns The fully composed OxyServices constructor with all mixins applied
|
|
85
135
|
*/
|
|
86
|
-
export function composeOxyServices() {
|
|
87
|
-
|
|
136
|
+
export function composeOxyServices(): ComposedOxyServicesConstructor {
|
|
137
|
+
const composed = MIXIN_PIPELINE.reduce(
|
|
88
138
|
(Base, mixin) => mixin(Base),
|
|
89
|
-
OxyServicesBase as unknown as
|
|
139
|
+
OxyServicesBase as unknown as new (...args: unknown[]) => OxyServicesBase
|
|
90
140
|
);
|
|
141
|
+
return composed as unknown as ComposedOxyServicesConstructor;
|
|
91
142
|
}
|
|
92
143
|
|
|
93
144
|
// Export the pipeline for testing/debugging
|
package/src/models/interfaces.ts
CHANGED
|
@@ -23,6 +23,41 @@ export interface OxyConfig {
|
|
|
23
23
|
onRequestError?: (url: string, method: string, error: Error) => void;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Privacy settings for a user account.
|
|
28
|
+
*
|
|
29
|
+
* All fields are optional because:
|
|
30
|
+
* - Updates are dot-path partial PATCHes — clients send only changed keys.
|
|
31
|
+
* - The server may return a partial subdocument depending on the API
|
|
32
|
+
* build (older builds returned only the field that changed).
|
|
33
|
+
* - User accounts created before a new toggle was introduced won't
|
|
34
|
+
* have that key persisted yet.
|
|
35
|
+
*
|
|
36
|
+
* Mirrors `IPrivacySettings` from `packages/api/src/types/privacy.types.ts`,
|
|
37
|
+
* but with every field marked optional.
|
|
38
|
+
*/
|
|
39
|
+
export interface PrivacySettings {
|
|
40
|
+
isPrivateAccount?: boolean;
|
|
41
|
+
hideOnlineStatus?: boolean;
|
|
42
|
+
hideLastSeen?: boolean;
|
|
43
|
+
profileVisibility?: boolean;
|
|
44
|
+
loginAlerts?: boolean;
|
|
45
|
+
blockScreenshots?: boolean;
|
|
46
|
+
login?: boolean;
|
|
47
|
+
biometricLogin?: boolean;
|
|
48
|
+
showActivity?: boolean;
|
|
49
|
+
allowTagging?: boolean;
|
|
50
|
+
allowMentions?: boolean;
|
|
51
|
+
hideReadReceipts?: boolean;
|
|
52
|
+
allowDirectMessages?: boolean;
|
|
53
|
+
dataSharing?: boolean;
|
|
54
|
+
locationSharing?: boolean;
|
|
55
|
+
analyticsSharing?: boolean;
|
|
56
|
+
sensitiveContent?: boolean;
|
|
57
|
+
autoFilter?: boolean;
|
|
58
|
+
muteKeywords?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
export interface User {
|
|
27
62
|
id: string;
|
|
28
63
|
publicKey: string;
|
|
@@ -33,9 +68,7 @@ export interface User {
|
|
|
33
68
|
// Named color preset (e.g. 'teal', 'blue', 'purple')
|
|
34
69
|
color?: string;
|
|
35
70
|
// Privacy and security settings
|
|
36
|
-
privacySettings?:
|
|
37
|
-
[key: string]: unknown;
|
|
38
|
-
};
|
|
71
|
+
privacySettings?: PrivacySettings;
|
|
39
72
|
name?: {
|
|
40
73
|
first?: string;
|
|
41
74
|
last?: string;
|
|
@@ -289,6 +322,35 @@ export interface FileDeleteResponse {
|
|
|
289
322
|
fileId: string;
|
|
290
323
|
}
|
|
291
324
|
|
|
325
|
+
/**
|
|
326
|
+
* React Native file descriptor accepted by FormData.
|
|
327
|
+
*
|
|
328
|
+
* On React Native, the multipart upload reads the file from disk via the URI
|
|
329
|
+
* during the network request — no in-JS Blob construction is required (and
|
|
330
|
+
* doing so would fail on Hermes since RN's BlobManager cannot wrap an
|
|
331
|
+
* ArrayBuffer/ArrayBufferView).
|
|
332
|
+
*
|
|
333
|
+
* This shape matches what `expo-document-picker` and `expo-image-picker`
|
|
334
|
+
* return for selected assets, and is what `OxyServices.assetUpload` accepts
|
|
335
|
+
* on native platforms.
|
|
336
|
+
*/
|
|
337
|
+
export interface RNFileDescriptor {
|
|
338
|
+
uri: string;
|
|
339
|
+
type?: string;
|
|
340
|
+
name?: string;
|
|
341
|
+
size?: number;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Asset upload input — accepted by `OxyServices.assetUpload` and `uploadRawFile`.
|
|
346
|
+
*
|
|
347
|
+
* - `File` / `Blob`: standard web browser path. `assetUpload` appends the
|
|
348
|
+
* Blob to FormData directly.
|
|
349
|
+
* - {@link RNFileDescriptor}: React Native path. FormData reads the file from
|
|
350
|
+
* disk via the URI during the multipart request.
|
|
351
|
+
*/
|
|
352
|
+
export type AssetUploadInput = File | Blob | RNFileDescriptor;
|
|
353
|
+
|
|
292
354
|
/**
|
|
293
355
|
* Central Asset Service interfaces
|
|
294
356
|
*/
|