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