@oxyhq/core 1.11.10 → 1.11.12
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/HttpService.js +26 -18
- package/dist/cjs/OxyServices.base.js +21 -0
- package/dist/cjs/crypto/signatureService.js +11 -11
- package/dist/cjs/mixins/OxyServices.managedAccounts.js +117 -0
- package/dist/cjs/mixins/OxyServices.user.js +11 -0
- package/dist/cjs/mixins/OxyServices.utility.js +81 -2
- package/dist/cjs/mixins/index.js +2 -0
- package/dist/cjs/utils/asyncUtils.js +34 -5
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +26 -18
- package/dist/esm/OxyServices.base.js +21 -0
- package/dist/esm/crypto/keyManager.js +3 -3
- package/dist/esm/crypto/polyfill.js +1 -1
- package/dist/esm/crypto/signatureService.js +12 -12
- package/dist/esm/mixins/OxyServices.language.js +1 -1
- package/dist/esm/mixins/OxyServices.managedAccounts.js +114 -0
- package/dist/esm/mixins/OxyServices.user.js +11 -0
- package/dist/esm/mixins/OxyServices.utility.js +81 -2
- package/dist/esm/mixins/index.js +2 -0
- package/dist/esm/utils/asyncUtils.js +34 -5
- package/dist/esm/utils/deviceManager.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +3 -0
- package/dist/types/OxyServices.base.d.ts +17 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mixins/OxyServices.analytics.d.ts +2 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +2 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +2 -0
- package/dist/types/mixins/OxyServices.developer.d.ts +2 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +2 -0
- package/dist/types/mixins/OxyServices.features.d.ts +5 -1
- package/dist/types/mixins/OxyServices.fedcm.d.ts +2 -0
- package/dist/types/mixins/OxyServices.karma.d.ts +2 -0
- package/dist/types/mixins/OxyServices.language.d.ts +2 -0
- package/dist/types/mixins/OxyServices.location.d.ts +2 -0
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +125 -0
- package/dist/types/mixins/OxyServices.payment.d.ts +2 -0
- package/dist/types/mixins/OxyServices.popup.d.ts +2 -0
- package/dist/types/mixins/OxyServices.privacy.d.ts +2 -0
- package/dist/types/mixins/OxyServices.redirect.d.ts +2 -0
- package/dist/types/mixins/OxyServices.security.d.ts +2 -0
- package/dist/types/mixins/OxyServices.topics.d.ts +2 -0
- package/dist/types/mixins/OxyServices.user.d.ts +14 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +22 -0
- package/dist/types/models/interfaces.d.ts +2 -0
- package/dist/types/utils/asyncUtils.d.ts +6 -2
- package/package.json +1 -1
- package/src/HttpService.ts +30 -11
- package/src/OxyServices.base.ts +23 -0
- package/src/crypto/keyManager.ts +3 -3
- package/src/crypto/polyfill.ts +1 -1
- package/src/crypto/signatureService.ts +13 -12
- package/src/index.ts +1 -0
- package/src/mixins/OxyServices.language.ts +1 -1
- package/src/mixins/OxyServices.managedAccounts.ts +147 -0
- package/src/mixins/OxyServices.user.ts +18 -0
- package/src/mixins/OxyServices.utility.ts +103 -2
- package/src/mixins/index.ts +2 -0
- package/src/models/interfaces.ts +3 -0
- package/src/utils/__tests__/asyncUtils.test.ts +187 -0
- package/src/utils/asyncUtils.ts +39 -9
- package/src/utils/deviceManager.ts +1 -1
package/dist/esm/HttpService.js
CHANGED
|
@@ -16,7 +16,6 @@ import { TTLCache, registerCacheForCleanup } from './utils/cache.js';
|
|
|
16
16
|
import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils.js';
|
|
17
17
|
import { retryAsync } from './utils/asyncUtils.js';
|
|
18
18
|
import { handleHttpError } from './utils/errorUtils.js';
|
|
19
|
-
import { isDev } from './shared/utils/debugUtils.js';
|
|
20
19
|
import { jwtDecode } from 'jwt-decode';
|
|
21
20
|
import { isNative, getPlatformOS } from './utils/platform.js';
|
|
22
21
|
/**
|
|
@@ -81,6 +80,8 @@ export class HttpService {
|
|
|
81
80
|
this.tokenRefreshPromise = null;
|
|
82
81
|
this.tokenRefreshCooldownUntil = 0;
|
|
83
82
|
this._onTokenRefreshed = null;
|
|
83
|
+
// Acting-as identity for managed accounts
|
|
84
|
+
this._actingAsUserId = null;
|
|
84
85
|
// Performance monitoring
|
|
85
86
|
this.requestMetrics = {
|
|
86
87
|
totalRequests: 0,
|
|
@@ -185,9 +186,12 @@ export class HttpService {
|
|
|
185
186
|
if (isNativeApp && isStateChangingMethod) {
|
|
186
187
|
headers['X-Native-App'] = 'true';
|
|
187
188
|
}
|
|
188
|
-
// Debug logging for CSRF issues
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
// Debug logging for CSRF issues — routed through the SimpleLogger so
|
|
190
|
+
// it only fires when consumers opt in via `enableLogging`. Previously
|
|
191
|
+
// this was a bare console.log that leaked noise into every host app's
|
|
192
|
+
// stdout in development.
|
|
193
|
+
if (isStateChangingMethod) {
|
|
194
|
+
this.logger.debug('CSRF Debug:', {
|
|
191
195
|
url,
|
|
192
196
|
method,
|
|
193
197
|
isNativeApp,
|
|
@@ -197,6 +201,10 @@ export class HttpService {
|
|
|
197
201
|
hasNativeAppHeader: headers['X-Native-App'] === 'true',
|
|
198
202
|
});
|
|
199
203
|
}
|
|
204
|
+
// Add X-Acting-As header for managed account identity delegation
|
|
205
|
+
if (this._actingAsUserId) {
|
|
206
|
+
headers['X-Acting-As'] = this._actingAsUserId;
|
|
207
|
+
}
|
|
200
208
|
// Merge custom headers if provided
|
|
201
209
|
if (config.headers) {
|
|
202
210
|
Object.entries(config.headers).forEach(([key, value]) => {
|
|
@@ -403,23 +411,20 @@ export class HttpService {
|
|
|
403
411
|
// Return cached token if available
|
|
404
412
|
const cachedToken = this.tokenStore.getCsrfToken();
|
|
405
413
|
if (cachedToken) {
|
|
406
|
-
|
|
407
|
-
console.log('[HttpService] Using cached CSRF token');
|
|
414
|
+
this.logger.debug('Using cached CSRF token');
|
|
408
415
|
return cachedToken;
|
|
409
416
|
}
|
|
410
417
|
// Deduplicate concurrent CSRF token fetches
|
|
411
418
|
const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
|
|
412
419
|
if (existingPromise) {
|
|
413
|
-
|
|
414
|
-
console.log('[HttpService] Waiting for existing CSRF fetch');
|
|
420
|
+
this.logger.debug('Waiting for existing CSRF fetch');
|
|
415
421
|
return existingPromise;
|
|
416
422
|
}
|
|
417
423
|
const fetchPromise = (async () => {
|
|
418
424
|
const maxAttempts = 2;
|
|
419
425
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
420
426
|
try {
|
|
421
|
-
|
|
422
|
-
console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/csrf-token`, `(attempt ${attempt})`);
|
|
427
|
+
this.logger.debug('Fetching CSRF token from:', `${this.baseURL}/csrf-token`, `(attempt ${attempt})`);
|
|
423
428
|
// Use AbortController for timeout (more compatible than AbortSignal.timeout)
|
|
424
429
|
const controller = new AbortController();
|
|
425
430
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
@@ -430,12 +435,10 @@ export class HttpService {
|
|
|
430
435
|
signal: controller.signal,
|
|
431
436
|
});
|
|
432
437
|
clearTimeout(timeoutId);
|
|
433
|
-
|
|
434
|
-
console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
|
|
438
|
+
this.logger.debug('CSRF fetch response:', response.status, response.ok);
|
|
435
439
|
if (response.ok) {
|
|
436
440
|
const data = await response.json();
|
|
437
|
-
|
|
438
|
-
console.log('[HttpService] CSRF response data:', data);
|
|
441
|
+
this.logger.debug('CSRF response data:', data);
|
|
439
442
|
const token = data.csrfToken || null;
|
|
440
443
|
this.tokenStore.setCsrfToken(token);
|
|
441
444
|
this.logger.debug('CSRF token fetched');
|
|
@@ -448,13 +451,11 @@ export class HttpService {
|
|
|
448
451
|
this.logger.debug('CSRF token from header');
|
|
449
452
|
return headerToken;
|
|
450
453
|
}
|
|
451
|
-
|
|
452
|
-
console.log('[HttpService] CSRF fetch failed with status:', response.status);
|
|
454
|
+
this.logger.debug('CSRF fetch failed with status:', response.status);
|
|
453
455
|
this.logger.warn('Failed to fetch CSRF token:', response.status);
|
|
454
456
|
}
|
|
455
457
|
catch (error) {
|
|
456
|
-
|
|
457
|
-
console.log('[HttpService] CSRF fetch error:', error);
|
|
458
|
+
this.logger.debug('CSRF fetch error:', error);
|
|
458
459
|
this.logger.warn('CSRF token fetch error:', error);
|
|
459
460
|
}
|
|
460
461
|
// Wait before retry (500ms)
|
|
@@ -579,6 +580,13 @@ export class HttpService {
|
|
|
579
580
|
async delete(url, config) {
|
|
580
581
|
return this.request({ method: 'DELETE', url, ...config });
|
|
581
582
|
}
|
|
583
|
+
// Acting-as identity management (managed accounts)
|
|
584
|
+
setActingAs(userId) {
|
|
585
|
+
this._actingAsUserId = userId;
|
|
586
|
+
}
|
|
587
|
+
getActingAs() {
|
|
588
|
+
return this._actingAsUserId;
|
|
589
|
+
}
|
|
582
590
|
// Token management
|
|
583
591
|
setTokens(accessToken, refreshToken = '') {
|
|
584
592
|
this.tokenStore.setTokens(accessToken, refreshToken);
|
|
@@ -138,6 +138,27 @@ export class OxyServicesBase {
|
|
|
138
138
|
getAccessToken() {
|
|
139
139
|
return this.httpService.getAccessToken();
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Set the acting-as identity for managed accounts.
|
|
143
|
+
*
|
|
144
|
+
* When set, all subsequent API requests will include the `X-Acting-As` header,
|
|
145
|
+
* causing the server to attribute actions to the managed account. The
|
|
146
|
+
* authenticated user must be an authorized manager of the target account.
|
|
147
|
+
*
|
|
148
|
+
* Pass `null` to clear and revert to the authenticated user's own identity.
|
|
149
|
+
*
|
|
150
|
+
* @param userId - The managed account user ID, or null to clear
|
|
151
|
+
*/
|
|
152
|
+
setActingAs(userId) {
|
|
153
|
+
this.httpService.setActingAs(userId);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get the current acting-as identity (managed account user ID), or null
|
|
157
|
+
* if operating as the authenticated user's own identity.
|
|
158
|
+
*/
|
|
159
|
+
getActingAs() {
|
|
160
|
+
return this.httpService.getActingAs();
|
|
161
|
+
}
|
|
141
162
|
/**
|
|
142
163
|
* Wait for authentication to be ready
|
|
143
164
|
*
|
|
@@ -45,7 +45,7 @@ async function initSecureStore() {
|
|
|
45
45
|
try {
|
|
46
46
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
47
47
|
const moduleName = 'expo-secure-store';
|
|
48
|
-
SecureStore = await import(moduleName);
|
|
48
|
+
SecureStore = await import(/* @vite-ignore */ moduleName);
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
|
51
51
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -68,7 +68,7 @@ async function initExpoCrypto() {
|
|
|
68
68
|
if (!ExpoCrypto) {
|
|
69
69
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
70
70
|
const moduleName = 'expo-crypto';
|
|
71
|
-
ExpoCrypto = await import(moduleName);
|
|
71
|
+
ExpoCrypto = await import(/* @vite-ignore */ moduleName);
|
|
72
72
|
}
|
|
73
73
|
return ExpoCrypto;
|
|
74
74
|
}
|
|
@@ -94,7 +94,7 @@ async function getSecureRandomBytes(length) {
|
|
|
94
94
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
95
95
|
try {
|
|
96
96
|
const cryptoModuleName = 'crypto';
|
|
97
|
-
const nodeCrypto = await import(cryptoModuleName);
|
|
97
|
+
const nodeCrypto = await import(/* @vite-ignore */ cryptoModuleName);
|
|
98
98
|
return new Uint8Array(nodeCrypto.randomBytes(length));
|
|
99
99
|
}
|
|
100
100
|
catch (error) {
|
|
@@ -40,7 +40,7 @@ function startExpoCryptoLoad() {
|
|
|
40
40
|
expoCryptoLoadPromise = (async () => {
|
|
41
41
|
try {
|
|
42
42
|
const moduleName = 'expo-crypto';
|
|
43
|
-
expoCryptoModule = await import(moduleName);
|
|
43
|
+
expoCryptoModule = await import(/* @vite-ignore */ moduleName);
|
|
44
44
|
}
|
|
45
45
|
catch {
|
|
46
46
|
// expo-crypto not available — expected in non-RN environments
|
|
@@ -8,20 +8,24 @@ import _cjs_elliptic from 'elliptic';
|
|
|
8
8
|
const { ec: EC } = _cjs_elliptic;
|
|
9
9
|
import { KeyManager } from './keyManager.js';
|
|
10
10
|
import { isReactNative, isNodeJS } from '../utils/platform.js';
|
|
11
|
-
// Lazy
|
|
11
|
+
// Lazy imports for platform-specific crypto
|
|
12
12
|
let ExpoCrypto = null;
|
|
13
|
+
let NodeCrypto = null;
|
|
13
14
|
const ec = new EC('secp256k1');
|
|
14
|
-
/**
|
|
15
|
-
* Initialize expo-crypto module
|
|
16
|
-
*/
|
|
17
15
|
async function initExpoCrypto() {
|
|
18
16
|
if (!ExpoCrypto) {
|
|
19
|
-
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
20
17
|
const moduleName = 'expo-crypto';
|
|
21
|
-
ExpoCrypto = await import(moduleName);
|
|
18
|
+
ExpoCrypto = await import(/* @vite-ignore */ moduleName);
|
|
22
19
|
}
|
|
23
20
|
return ExpoCrypto;
|
|
24
21
|
}
|
|
22
|
+
async function initNodeCrypto() {
|
|
23
|
+
if (!NodeCrypto) {
|
|
24
|
+
const moduleName = 'crypto';
|
|
25
|
+
NodeCrypto = await import(/* @vite-ignore */ moduleName);
|
|
26
|
+
}
|
|
27
|
+
return NodeCrypto;
|
|
28
|
+
}
|
|
25
29
|
/**
|
|
26
30
|
* Compute SHA-256 hash of a string
|
|
27
31
|
*/
|
|
@@ -31,10 +35,9 @@ async function sha256(message) {
|
|
|
31
35
|
const Crypto = await initExpoCrypto();
|
|
32
36
|
return Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, message);
|
|
33
37
|
}
|
|
34
|
-
// In Node.js, use Node's crypto module
|
|
35
38
|
if (isNodeJS()) {
|
|
36
39
|
try {
|
|
37
|
-
const nodeCrypto = await
|
|
40
|
+
const nodeCrypto = await initNodeCrypto();
|
|
38
41
|
return nodeCrypto.createHash('sha256').update(message).digest('hex');
|
|
39
42
|
}
|
|
40
43
|
catch {
|
|
@@ -62,12 +65,9 @@ export class SignatureService {
|
|
|
62
65
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
63
66
|
.join('');
|
|
64
67
|
}
|
|
65
|
-
// In Node.js, use Node's crypto module
|
|
66
|
-
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
67
68
|
if (isNodeJS()) {
|
|
68
69
|
try {
|
|
69
|
-
const
|
|
70
|
-
const nodeCrypto = await import(cryptoModuleName);
|
|
70
|
+
const nodeCrypto = await initNodeCrypto();
|
|
71
71
|
return nodeCrypto.randomBytes(32).toString('hex');
|
|
72
72
|
}
|
|
73
73
|
catch {
|
|
@@ -17,7 +17,7 @@ export function OxyServicesLanguageMixin(Base) {
|
|
|
17
17
|
try {
|
|
18
18
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
19
19
|
const moduleName = '@react-native-async-storage/async-storage';
|
|
20
|
-
const asyncStorageModule = await import(moduleName);
|
|
20
|
+
const asyncStorageModule = await import(/* @vite-ignore */ moduleName);
|
|
21
21
|
const storage = asyncStorageModule.default;
|
|
22
22
|
return {
|
|
23
23
|
getItem: storage.getItem.bind(storage),
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export function OxyServicesManagedAccountsMixin(Base) {
|
|
2
|
+
return class extends Base {
|
|
3
|
+
constructor(...args) {
|
|
4
|
+
super(...args);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Create a new managed account (sub-account).
|
|
8
|
+
*
|
|
9
|
+
* The server creates a User document with `isManagedAccount: true` and links
|
|
10
|
+
* it to the authenticated user as owner.
|
|
11
|
+
*/
|
|
12
|
+
async createManagedAccount(data) {
|
|
13
|
+
try {
|
|
14
|
+
return await this.makeRequest('POST', '/managed-accounts', data, {
|
|
15
|
+
cache: false,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw this.handleError(error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* List all accounts the authenticated user manages.
|
|
24
|
+
*/
|
|
25
|
+
async getManagedAccounts() {
|
|
26
|
+
try {
|
|
27
|
+
return await this.makeRequest('GET', '/managed-accounts', undefined, {
|
|
28
|
+
cache: true,
|
|
29
|
+
cacheTTL: 2 * 60 * 1000, // 2 minutes cache
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw this.handleError(error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get details for a specific managed account.
|
|
38
|
+
*/
|
|
39
|
+
async getManagedAccountDetails(accountId) {
|
|
40
|
+
try {
|
|
41
|
+
return await this.makeRequest('GET', `/managed-accounts/${accountId}`, undefined, {
|
|
42
|
+
cache: true,
|
|
43
|
+
cacheTTL: 2 * 60 * 1000,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw this.handleError(error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Update a managed account's profile data.
|
|
52
|
+
* Requires owner or admin role.
|
|
53
|
+
*/
|
|
54
|
+
async updateManagedAccount(accountId, data) {
|
|
55
|
+
try {
|
|
56
|
+
return await this.makeRequest('PUT', `/managed-accounts/${accountId}`, data, {
|
|
57
|
+
cache: false,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw this.handleError(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Delete a managed account permanently.
|
|
66
|
+
* Requires owner role.
|
|
67
|
+
*/
|
|
68
|
+
async deleteManagedAccount(accountId) {
|
|
69
|
+
try {
|
|
70
|
+
await this.makeRequest('DELETE', `/managed-accounts/${accountId}`, undefined, {
|
|
71
|
+
cache: false,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw this.handleError(error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Add a manager to a managed account.
|
|
80
|
+
* Requires owner or admin role on the account.
|
|
81
|
+
*
|
|
82
|
+
* @param accountId - The managed account to add the manager to
|
|
83
|
+
* @param userId - The user to grant management access
|
|
84
|
+
* @param role - The role to assign: 'admin' or 'editor'
|
|
85
|
+
*/
|
|
86
|
+
async addManager(accountId, userId, role) {
|
|
87
|
+
try {
|
|
88
|
+
await this.makeRequest('POST', `/managed-accounts/${accountId}/managers`, { userId, role }, {
|
|
89
|
+
cache: false,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw this.handleError(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Remove a manager from a managed account.
|
|
98
|
+
* Requires owner role.
|
|
99
|
+
*
|
|
100
|
+
* @param accountId - The managed account
|
|
101
|
+
* @param userId - The manager to remove
|
|
102
|
+
*/
|
|
103
|
+
async removeManager(accountId, userId) {
|
|
104
|
+
try {
|
|
105
|
+
await this.makeRequest('DELETE', `/managed-accounts/${accountId}/managers/${userId}`, undefined, {
|
|
106
|
+
cache: false,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw this.handleError(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -18,6 +18,17 @@ export function OxyServicesUserMixin(Base) {
|
|
|
18
18
|
throw this.handleError(error);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Lightweight username lookup for login flows.
|
|
23
|
+
* Returns minimal public info: exists, color, avatar, displayName.
|
|
24
|
+
* Faster than getProfileByUsername — no stats, no formatting.
|
|
25
|
+
*/
|
|
26
|
+
async lookupUsername(username) {
|
|
27
|
+
return await this.makeRequest('GET', `/auth/lookup/${encodeURIComponent(username)}`, undefined, {
|
|
28
|
+
cache: true,
|
|
29
|
+
cacheTTL: 60 * 1000, // 1 minute cache
|
|
30
|
+
});
|
|
31
|
+
}
|
|
21
32
|
/**
|
|
22
33
|
* Search user profiles
|
|
23
34
|
*/
|
|
@@ -10,6 +10,41 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
10
10
|
return class extends Base {
|
|
11
11
|
constructor(...args) {
|
|
12
12
|
super(...args);
|
|
13
|
+
/** @internal In-memory cache for acting-as verification results (TTL: 5 min) */
|
|
14
|
+
this._actingAsCache = new Map();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Verify that a user is authorized to act as a managed account.
|
|
18
|
+
* Results are cached in-memory for 5 minutes to avoid repeated API calls.
|
|
19
|
+
*
|
|
20
|
+
* @internal Used by the auth() middleware — not part of the public API
|
|
21
|
+
*/
|
|
22
|
+
async verifyActingAs(userId, accountId) {
|
|
23
|
+
const cacheKey = `${userId}:${accountId}`;
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
// Check cache
|
|
26
|
+
const cached = this._actingAsCache.get(cacheKey);
|
|
27
|
+
if (cached && cached.expiresAt > now) {
|
|
28
|
+
return cached.result;
|
|
29
|
+
}
|
|
30
|
+
// Query the API
|
|
31
|
+
try {
|
|
32
|
+
const result = await this.makeRequest('GET', '/managed-accounts/verify', { accountId, userId }, { cache: false, retry: false, timeout: 5000 });
|
|
33
|
+
// Cache successful result for 5 minutes
|
|
34
|
+
this._actingAsCache.set(cacheKey, {
|
|
35
|
+
result: result && result.authorized ? result : null,
|
|
36
|
+
expiresAt: now + 5 * 60 * 1000,
|
|
37
|
+
});
|
|
38
|
+
return result && result.authorized ? result : null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Cache negative result for 1 minute to avoid hammering on transient errors
|
|
42
|
+
this._actingAsCache.set(cacheKey, {
|
|
43
|
+
result: null,
|
|
44
|
+
expiresAt: now + 1 * 60 * 1000,
|
|
45
|
+
});
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
13
48
|
}
|
|
14
49
|
/**
|
|
15
50
|
* Fetch link metadata
|
|
@@ -72,6 +107,45 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
72
107
|
const oxyInstance = this;
|
|
73
108
|
// Return an async middleware function
|
|
74
109
|
return async (req, res, next) => {
|
|
110
|
+
// Process X-Acting-As header for managed account identity delegation.
|
|
111
|
+
// Called after successful authentication, before next(). If the header
|
|
112
|
+
// is present, verifies authorization and swaps the request identity to
|
|
113
|
+
// the managed account, preserving the original user for audit trails.
|
|
114
|
+
const processActingAs = async () => {
|
|
115
|
+
const actingAsUserId = req.headers['x-acting-as'];
|
|
116
|
+
if (!actingAsUserId)
|
|
117
|
+
return true; // No header, proceed normally
|
|
118
|
+
const verification = await oxyInstance.verifyActingAs(req.userId, actingAsUserId);
|
|
119
|
+
if (!verification) {
|
|
120
|
+
const error = {
|
|
121
|
+
error: 'ACTING_AS_UNAUTHORIZED',
|
|
122
|
+
message: 'Not authorized to act as this account',
|
|
123
|
+
code: 'ACTING_AS_UNAUTHORIZED',
|
|
124
|
+
status: 403,
|
|
125
|
+
};
|
|
126
|
+
if (onError) {
|
|
127
|
+
onError(error);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
res.status(403).json(error);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
// Preserve original user for audit trails
|
|
135
|
+
req.originalUser = { id: req.userId, ...req.user };
|
|
136
|
+
req.actingAs = { userId: actingAsUserId, role: verification.role };
|
|
137
|
+
// Swap user identity to the managed account
|
|
138
|
+
req.userId = actingAsUserId;
|
|
139
|
+
req.user = { id: actingAsUserId };
|
|
140
|
+
// Also set _id for routes that use Pattern B (req.user._id)
|
|
141
|
+
if (req.user) {
|
|
142
|
+
req.user._id = actingAsUserId;
|
|
143
|
+
}
|
|
144
|
+
if (debug) {
|
|
145
|
+
console.log(`[oxy.auth] Acting as ${actingAsUserId} (role=${verification.role}) original=${req.originalUser.id}`);
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
};
|
|
75
149
|
try {
|
|
76
150
|
// Extract token from Authorization header or query params
|
|
77
151
|
const authHeader = req.headers['authorization'];
|
|
@@ -294,7 +368,10 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
294
368
|
if (debug) {
|
|
295
369
|
console.log(`[oxy.auth] OK user=${userId} session=${decoded.sessionId}`);
|
|
296
370
|
}
|
|
297
|
-
|
|
371
|
+
// Process X-Acting-As header before proceeding
|
|
372
|
+
if (await processActingAs())
|
|
373
|
+
return next();
|
|
374
|
+
return;
|
|
298
375
|
}
|
|
299
376
|
catch (validationError) {
|
|
300
377
|
if (debug) {
|
|
@@ -345,7 +422,9 @@ export function OxyServicesUtilityMixin(Base) {
|
|
|
345
422
|
if (debug) {
|
|
346
423
|
console.log(`[oxy.auth] OK user=${userId} (no session)`);
|
|
347
424
|
}
|
|
348
|
-
|
|
425
|
+
// Process X-Acting-As header before proceeding
|
|
426
|
+
if (await processActingAs())
|
|
427
|
+
next();
|
|
349
428
|
}
|
|
350
429
|
catch (error) {
|
|
351
430
|
const apiError = oxyInstance.handleError(error);
|
package/dist/esm/mixins/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { OxyServicesSecurityMixin } from './OxyServices.security.js';
|
|
|
23
23
|
import { OxyServicesUtilityMixin } from './OxyServices.utility.js';
|
|
24
24
|
import { OxyServicesFeaturesMixin } from './OxyServices.features.js';
|
|
25
25
|
import { OxyServicesTopicsMixin } from './OxyServices.topics.js';
|
|
26
|
+
import { OxyServicesManagedAccountsMixin } from './OxyServices.managedAccounts.js';
|
|
26
27
|
/**
|
|
27
28
|
* Mixin pipeline - applied in order from first to last.
|
|
28
29
|
*
|
|
@@ -60,6 +61,7 @@ const MIXIN_PIPELINE = [
|
|
|
60
61
|
OxyServicesSecurityMixin,
|
|
61
62
|
OxyServicesFeaturesMixin,
|
|
62
63
|
OxyServicesTopicsMixin,
|
|
64
|
+
OxyServicesManagedAccountsMixin,
|
|
63
65
|
// Utility (last, can use all above)
|
|
64
66
|
OxyServicesUtilityMixin,
|
|
65
67
|
];
|
|
@@ -30,18 +30,47 @@ export async function parallelWithErrorHandling(operations, errorHandler) {
|
|
|
30
30
|
const results = await Promise.allSettled(operations.map((op, index) => withErrorHandling(op, error => errorHandler?.(error, index))));
|
|
31
31
|
return results.map(result => result.status === 'fulfilled' ? result.value : null);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Extract an HTTP status code from an error value, tolerating both the
|
|
35
|
+
* axios-style nested shape (`error.response.status`) and the flat shape
|
|
36
|
+
* produced by {@link handleHttpError} / fetch-based clients (`error.status`).
|
|
37
|
+
*
|
|
38
|
+
* Centralising this lookup prevents retry predicates from silently falling
|
|
39
|
+
* through when one of the two shapes is missing, which previously caused
|
|
40
|
+
* @oxyhq/core to retry 4xx responses and turn sub-10ms failures into
|
|
41
|
+
* multi-second stalls for every missing-resource lookup.
|
|
42
|
+
*/
|
|
43
|
+
function extractHttpStatus(error) {
|
|
44
|
+
if (!error || typeof error !== 'object')
|
|
45
|
+
return undefined;
|
|
46
|
+
const candidate = error;
|
|
47
|
+
const flat = candidate.status;
|
|
48
|
+
if (typeof flat === 'number' && Number.isFinite(flat))
|
|
49
|
+
return flat;
|
|
50
|
+
const nested = candidate.response?.status;
|
|
51
|
+
if (typeof nested === 'number' && Number.isFinite(nested))
|
|
52
|
+
return nested;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
33
55
|
/**
|
|
34
56
|
* Retry an async operation with exponential backoff
|
|
35
57
|
*
|
|
36
|
-
* By default, does not retry on 4xx errors (client errors).
|
|
37
|
-
*
|
|
58
|
+
* By default, does not retry on 4xx errors (client errors). The default
|
|
59
|
+
* predicate accepts both the axios-style `error.response.status` and the
|
|
60
|
+
* flat `error.status` shape produced by {@link handleHttpError}, so callers
|
|
61
|
+
* never accidentally retry a deterministic client failure.
|
|
62
|
+
*
|
|
63
|
+
* Use the `shouldRetry` callback to customize retry behavior.
|
|
38
64
|
*/
|
|
39
65
|
export async function retryAsync(operation, maxRetries = 3, baseDelay = 1000, shouldRetry) {
|
|
40
66
|
let lastError;
|
|
41
|
-
// Default shouldRetry: don't retry on 4xx errors
|
|
67
|
+
// Default shouldRetry: don't retry on 4xx errors (client errors).
|
|
68
|
+
// Checks BOTH `error.status` (flat shape from handleHttpError / fetch
|
|
69
|
+
// clients) AND `error.response.status` (axios-style shape) so neither
|
|
70
|
+
// representation can leak a client error into the retry loop.
|
|
42
71
|
const defaultShouldRetry = (error) => {
|
|
43
|
-
|
|
44
|
-
if (
|
|
72
|
+
const status = extractHttpStatus(error);
|
|
73
|
+
if (status !== undefined && status >= 400 && status < 500) {
|
|
45
74
|
return false;
|
|
46
75
|
}
|
|
47
76
|
return true;
|
|
@@ -17,7 +17,7 @@ export class DeviceManager {
|
|
|
17
17
|
try {
|
|
18
18
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
19
19
|
const moduleName = '@react-native-async-storage/async-storage';
|
|
20
|
-
const asyncStorageModule = await import(moduleName);
|
|
20
|
+
const asyncStorageModule = await import(/* @vite-ignore */ moduleName);
|
|
21
21
|
const storage = asyncStorageModule.default;
|
|
22
22
|
return {
|
|
23
23
|
getItem: storage.getItem.bind(storage),
|