@oxyhq/core 4.0.1 → 5.1.0
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 +6 -18
- package/dist/cjs/OxyServices.base.js +0 -21
- package/dist/cjs/crypto/keyManager.js +7 -7
- package/dist/cjs/crypto/polyfill.js +6 -5
- package/dist/cjs/crypto/signatureService.js +44 -220
- package/dist/cjs/index.js +4 -8
- package/dist/cjs/mixins/OxyServices.accounts.js +90 -0
- package/dist/cjs/mixins/OxyServices.assets.js +68 -2
- package/dist/cjs/mixins/OxyServices.auth.js +3 -3
- package/dist/cjs/mixins/OxyServices.civic.js +3 -3
- package/dist/cjs/mixins/OxyServices.language.js +2 -2
- package/dist/cjs/mixins/OxyServices.utility.js +7 -95
- package/dist/cjs/utils/cacheKey.js +17 -19
- package/dist/cjs/utils/deviceManager.js +2 -2
- package/dist/cjs/utils/platform.js +0 -14
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +6 -18
- package/dist/esm/OxyServices.base.js +0 -21
- package/dist/esm/crypto/keyManager.js +4 -4
- package/dist/esm/crypto/polyfill.js +5 -4
- package/dist/esm/crypto/signatureService.js +39 -214
- package/dist/esm/index.js +1 -2
- package/dist/esm/mixins/OxyServices.accounts.js +90 -0
- package/dist/esm/mixins/OxyServices.assets.js +67 -1
- package/dist/esm/mixins/OxyServices.auth.js +1 -1
- package/dist/esm/mixins/OxyServices.civic.js +3 -3
- package/dist/esm/mixins/OxyServices.language.js +1 -1
- package/dist/esm/mixins/OxyServices.utility.js +6 -94
- package/dist/esm/utils/cacheKey.js +17 -19
- package/dist/esm/utils/deviceManager.js +1 -1
- package/dist/esm/utils/platform.js +0 -12
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +3 -6
- package/dist/types/OxyServices.base.d.ts +0 -17
- package/dist/types/crypto/polyfill.d.ts +2 -2
- package/dist/types/crypto/signatureService.d.ts +18 -84
- package/dist/types/index.d.ts +4 -5
- package/dist/types/mixins/OxyServices.accounts.d.ts +66 -5
- package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
- package/dist/types/mixins/OxyServices.assets.d.ts +42 -3
- package/dist/types/mixins/OxyServices.auth.d.ts +0 -2
- package/dist/types/mixins/OxyServices.civic.d.ts +3 -5
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
- package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
- package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
- package/dist/types/mixins/OxyServices.features.d.ts +0 -2
- package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -2
- package/dist/types/mixins/OxyServices.identity.d.ts +8 -5
- package/dist/types/mixins/OxyServices.language.d.ts +0 -2
- package/dist/types/mixins/OxyServices.links.d.ts +0 -2
- package/dist/types/mixins/OxyServices.location.d.ts +0 -2
- package/dist/types/mixins/OxyServices.nodes.d.ts +0 -44
- package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
- package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
- package/dist/types/mixins/OxyServices.redirect.d.ts +0 -2
- package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
- package/dist/types/mixins/OxyServices.security.d.ts +0 -2
- package/dist/types/mixins/OxyServices.silent.d.ts +0 -2
- package/dist/types/mixins/OxyServices.sso.d.ts +0 -2
- package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.user.d.ts +0 -2
- package/dist/types/mixins/OxyServices.utility.d.ts +0 -32
- package/dist/types/models/interfaces.d.ts +18 -0
- package/dist/types/server/auth.d.ts +0 -6
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/utils/cacheKey.d.ts +6 -7
- package/dist/types/utils/platform.d.ts +0 -8
- package/package.json +4 -7
- package/src/HttpService.ts +6 -22
- package/src/OxyServices.base.ts +0 -23
- package/src/__tests__/httpServiceCache.test.ts +0 -19
- package/src/crypto/__tests__/keyManager.atomicity.test.ts +2 -1
- package/src/crypto/__tests__/keyManager.test.ts +9 -7
- package/src/crypto/__tests__/signChallengeShared.test.ts +2 -1
- package/src/crypto/__tests__/signedRecord.test.ts +37 -150
- package/src/crypto/keyManager.ts +28 -17
- package/src/crypto/polyfill.ts +5 -4
- package/src/crypto/signatureService.ts +67 -255
- package/src/index.ts +4 -3
- package/src/mixins/OxyServices.accounts.ts +136 -3
- package/src/mixins/OxyServices.assets.ts +96 -2
- package/src/mixins/OxyServices.auth.ts +1 -1
- package/src/mixins/OxyServices.civic.ts +6 -17
- package/src/mixins/OxyServices.identity.ts +8 -2
- package/src/mixins/OxyServices.language.ts +1 -1
- package/src/mixins/OxyServices.nodes.ts +1 -12
- package/src/mixins/OxyServices.utility.ts +6 -119
- package/src/mixins/__tests__/OxyServices.civic.test.ts +2 -2
- package/src/mixins/__tests__/OxyServices.serviceAssetMetadata.test.ts +116 -0
- package/src/mixins/__tests__/accounts.test.ts +120 -0
- package/src/models/interfaces.ts +19 -0
- package/src/server/auth.ts +0 -7
- package/src/server/index.ts +0 -1
- package/src/utils/__tests__/cacheKey.test.ts +0 -0
- package/src/utils/cacheKey.ts +16 -21
- package/src/utils/deviceManager.ts +1 -1
- package/src/utils/platform.ts +0 -14
- package/dist/cjs/crypto/canonicalJson.js +0 -107
- package/dist/cjs/utils/platformCrypto.js +0 -165
- package/dist/cjs/utils/platformCrypto.native.js +0 -123
- package/dist/esm/crypto/canonicalJson.js +0 -104
- package/dist/esm/utils/platformCrypto.js +0 -125
- package/dist/esm/utils/platformCrypto.native.js +0 -80
- package/dist/types/crypto/canonicalJson.d.ts +0 -44
- package/dist/types/utils/platformCrypto.d.ts +0 -87
- package/dist/types/utils/platformCrypto.native.d.ts +0 -54
- package/src/crypto/__tests__/canonicalJson.test.ts +0 -116
- package/src/crypto/canonicalJson.ts +0 -120
- package/src/utils/platformCrypto.native.ts +0 -101
- package/src/utils/platformCrypto.ts +0 -145
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OxyServicesAccountsMixin = OxyServicesAccountsMixin;
|
|
4
|
+
const userIdentity_1 = require("../utils/userIdentity");
|
|
5
|
+
const platform_1 = require("../utils/platform");
|
|
6
|
+
const loggerUtils_1 = require("../utils/loggerUtils");
|
|
4
7
|
const mixinHelpers_1 = require("./mixinHelpers");
|
|
5
8
|
function OxyServicesAccountsMixin(Base) {
|
|
6
9
|
return class extends Base {
|
|
@@ -43,6 +46,93 @@ function OxyServicesAccountsMixin(Base) {
|
|
|
43
46
|
throw this.handleError(error);
|
|
44
47
|
}
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Switch the active session INTO a managed account.
|
|
51
|
+
*
|
|
52
|
+
* Calls `POST /accounts/:id/switch` with the signed-in operator's bearer.
|
|
53
|
+
* The server authorises the operator (must hold `account:act_as` over the
|
|
54
|
+
* target, directly or inherited — else 403; 404 if missing/archived; 403 if
|
|
55
|
+
* the target is a personal account), then mints a REAL session for the
|
|
56
|
+
* target account and returns it in the canonical login / `claimSessionByToken`
|
|
57
|
+
* shape (`{ sessionId, deviceId, expiresAt, accessToken, user, authuser }`).
|
|
58
|
+
*
|
|
59
|
+
* Unlike the removed `X-Acting-As` delegation header, the returned session
|
|
60
|
+
* IS the new identity: this plants `accessToken` as the active token —
|
|
61
|
+
* exactly like `claimSessionByToken` / `verifyChallenge` — so every
|
|
62
|
+
* subsequent request authenticates as the target account.
|
|
63
|
+
*
|
|
64
|
+
* Joining the device multi-account set (so the switch survives a reload and
|
|
65
|
+
* propagates cross-domain via `/auth/refresh-all`) requires a SECOND call, to
|
|
66
|
+
* `POST /auth/session`, made here after the token is planted. The switch route
|
|
67
|
+
* lives at `/accounts/*`, OUTSIDE the `oxy_rt_<authuser>` cookie's `Path=/auth`
|
|
68
|
+
* scope, so the server never sees the device's existing slots from it and
|
|
69
|
+
* would clobber slot 0 (destroying the operator's own session). `/auth/session`
|
|
70
|
+
* runs where those cookies ARE visible, so the server allocates a NEW slot that
|
|
71
|
+
* coexists with the operator's and returns its `authuser`. This step is
|
|
72
|
+
* web-only (native multi-account uses stored sessions, not cookies) and
|
|
73
|
+
* best-effort — a failure leaves the in-session switch intact; the switched
|
|
74
|
+
* account simply won't survive a reload until the cookie is next established.
|
|
75
|
+
*
|
|
76
|
+
* After planting, the SDK's identity-scoped GET cache is fully cleared so
|
|
77
|
+
* every cached read re-fetches as the new account. (The consuming
|
|
78
|
+
* `OxyContext` additionally invalidates its React Query cache and updates
|
|
79
|
+
* session state from the returned `user`; this clears the SDK's own HTTP
|
|
80
|
+
* cache at the source — `setTokens` deliberately preserves the warm cache for
|
|
81
|
+
* same-user silent refreshes, so the sweep here is explicit.)
|
|
82
|
+
*
|
|
83
|
+
* @param accountId - The target account's Mongo `_id`.
|
|
84
|
+
* @returns The minted session (planted) plus the device `authuser` slot.
|
|
85
|
+
*/
|
|
86
|
+
async switchToAccount(accountId) {
|
|
87
|
+
try {
|
|
88
|
+
const res = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/switch`, undefined, { cache: false });
|
|
89
|
+
// Plant the freshly minted session as the ACTIVE session, mirroring
|
|
90
|
+
// `claimSessionByToken` / `verifyChallenge`: the response body carries
|
|
91
|
+
// the first access token. The device refresh cookie is established below.
|
|
92
|
+
if (res?.accessToken) {
|
|
93
|
+
this.setTokens(res.accessToken);
|
|
94
|
+
}
|
|
95
|
+
// Register the switched session in the device's multi-account set by
|
|
96
|
+
// establishing its first-party refresh cookie. This MUST be a separate
|
|
97
|
+
// call to `POST /auth/session`: the switch route is at `/accounts/*`,
|
|
98
|
+
// outside the `oxy_rt_<authuser>` cookie's `Path=/auth` scope, so it can
|
|
99
|
+
// never read the device's existing slots and would overwrite slot 0
|
|
100
|
+
// (destroying the operator's own session). `/auth/session` runs where the
|
|
101
|
+
// cookies ARE visible, so the server allocates a NEW slot that coexists
|
|
102
|
+
// with the operator's and returns its `authuser`. Web-only; best-effort.
|
|
103
|
+
let authuser = res.authuser;
|
|
104
|
+
if ((0, platform_1.isWeb)()) {
|
|
105
|
+
try {
|
|
106
|
+
const established = await this.makeRequest('POST', '/auth/session', undefined, { cache: false });
|
|
107
|
+
if (typeof established?.authuser === 'number') {
|
|
108
|
+
authuser = established.authuser;
|
|
109
|
+
}
|
|
110
|
+
// /auth/session mints a fresh access token off the same session;
|
|
111
|
+
// re-plant it so the active token matches the rotated cookie.
|
|
112
|
+
if (established?.accessToken) {
|
|
113
|
+
this.setTokens(established.accessToken);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
loggerUtils_1.logger.warn('[OxyServices] Failed to establish device refresh cookie after account switch; the switch is active in-session but may not survive a reload', { component: 'OxyServices', method: 'switchToAccount' }, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Identity changed → drop the entire GET response cache so no entry
|
|
121
|
+
// personalised for the previous identity is reused. Cache keys are
|
|
122
|
+
// identity-scoped, so a different identity could not READ the old
|
|
123
|
+
// entries anyway, but clearing guarantees a clean refetch as the new
|
|
124
|
+
// account and frees the prior identity's resident data.
|
|
125
|
+
this.clearCache();
|
|
126
|
+
return {
|
|
127
|
+
...res,
|
|
128
|
+
...(typeof authuser === 'number' ? { authuser } : {}),
|
|
129
|
+
user: (0, userIdentity_1.normalizeUserIdentity)(res.user),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
throw this.handleError(error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
46
136
|
/**
|
|
47
137
|
* Create a new (non-personal) account. The caller becomes its `owner`.
|
|
48
138
|
* @param data - Account configuration: kind, optional parent, and profile.
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OxyServicesAssetsMixin = OxyServicesAssetsMixin;
|
|
4
|
-
const
|
|
4
|
+
const protocol_1 = require("@oxyhq/protocol");
|
|
5
|
+
const loggerUtils_1 = require("../utils/loggerUtils");
|
|
6
|
+
const errorUtils_1 = require("../utils/errorUtils");
|
|
7
|
+
/**
|
|
8
|
+
* Maximum number of ids sent per `POST /assets/service/by-ids` request. Matches
|
|
9
|
+
* the server-side batch cap (the route rejects empty or > 100 id arrays with a
|
|
10
|
+
* 400); larger inputs are split into multiple chunked calls and merged. Mirrors
|
|
11
|
+
* `getUsersByIds`'s `USERS_BY_IDS_CHUNK_SIZE`.
|
|
12
|
+
*/
|
|
13
|
+
const SERVICE_ASSET_METADATA_CHUNK_SIZE = 100;
|
|
5
14
|
function OxyServicesAssetsMixin(Base) {
|
|
6
15
|
return class extends Base {
|
|
7
16
|
constructor(...args) {
|
|
@@ -147,6 +156,63 @@ function OxyServicesAssetsMixin(Base) {
|
|
|
147
156
|
}
|
|
148
157
|
return urls;
|
|
149
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve many Oxy asset ids to their content-addressed metadata in one
|
|
161
|
+
* round-trip per chunk via `POST /assets/service/by-ids` (body `{ ids }`).
|
|
162
|
+
*
|
|
163
|
+
* Returns each asset's `sha256`, `mime`, byte `size`, and `status` — built
|
|
164
|
+
* for server-to-server callers (e.g. Mention's MTN Protocol blob-ref
|
|
165
|
+
* resolution) that need the content hash for an asset id. Ids are
|
|
166
|
+
* deduplicated and validated (empty/blank ids dropped) before being split
|
|
167
|
+
* into chunks of {@link SERVICE_ASSET_METADATA_CHUNK_SIZE} (the server-side
|
|
168
|
+
* cap). The server omits unknown/deleted ids from each chunk's `data`, so
|
|
169
|
+
* the merged result may be shorter than the requested id list and the caller
|
|
170
|
+
* is expected to map by `id`.
|
|
171
|
+
*
|
|
172
|
+
* **Service-token auth (required).** `/assets/service/by-ids` is guarded by
|
|
173
|
+
* `serviceAuthMiddleware` + the `files:read` scope and is called via
|
|
174
|
+
* `makeServiceRequest`, which attaches `Authorization: Bearer <serviceToken>`
|
|
175
|
+
* (the same client that calls `POST /assets/service/cache`). The calling
|
|
176
|
+
* client MUST be service-configured (`configureServiceAuth(apiKey,
|
|
177
|
+
* apiSecret)`) before invoking this method; otherwise `getServiceToken()`
|
|
178
|
+
* throws because no credentials are available. A plain user-session request
|
|
179
|
+
* is rejected by the route's service-auth guard.
|
|
180
|
+
*
|
|
181
|
+
* Resilience: chunks are independent. A failed chunk is logged and skipped —
|
|
182
|
+
* the method returns every entry that resolved successfully rather than
|
|
183
|
+
* discarding the whole call on one chunk's failure. An empty/whitespace-only
|
|
184
|
+
* input resolves immediately with `[]` and performs no network call.
|
|
185
|
+
*
|
|
186
|
+
* Not cached at the SDK layer: it's a POST keyed on a multi-id body (low hit
|
|
187
|
+
* rate), mirroring the sibling service/POST methods which never cache.
|
|
188
|
+
*/
|
|
189
|
+
async getServiceAssetMetadataByIds(ids) {
|
|
190
|
+
const uniqueIds = Array.from(new Set(ids.filter((id) => typeof id === 'string' && id.trim().length > 0)));
|
|
191
|
+
if (uniqueIds.length === 0) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const chunks = [];
|
|
195
|
+
for (let i = 0; i < uniqueIds.length; i += SERVICE_ASSET_METADATA_CHUNK_SIZE) {
|
|
196
|
+
chunks.push(uniqueIds.slice(i, i + SERVICE_ASSET_METADATA_CHUNK_SIZE));
|
|
197
|
+
}
|
|
198
|
+
// Run chunks concurrently; a single chunk failure must not sink the rest.
|
|
199
|
+
const settled = await Promise.all(chunks.map(async (chunk) => {
|
|
200
|
+
try {
|
|
201
|
+
const entries = await this.makeServiceRequest('POST', '/assets/service/by-ids', { ids: chunk });
|
|
202
|
+
return Array.isArray(entries) ? entries : [];
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
loggerUtils_1.logger.warn('getServiceAssetMetadataByIds: chunk failed, continuing with remaining chunks', {
|
|
206
|
+
method: 'getServiceAssetMetadataByIds',
|
|
207
|
+
chunkSize: chunk.length,
|
|
208
|
+
status: (0, errorUtils_1.extractErrorStatus)(error),
|
|
209
|
+
error: error instanceof Error ? error.message : String(error),
|
|
210
|
+
});
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}));
|
|
214
|
+
return settled.flat();
|
|
215
|
+
}
|
|
150
216
|
/**
|
|
151
217
|
* Upload raw file data
|
|
152
218
|
*/
|
|
@@ -173,7 +239,7 @@ function OxyServicesAssetsMixin(Base) {
|
|
|
173
239
|
}
|
|
174
240
|
else if ('uri' in file && typeof file.uri === 'string') {
|
|
175
241
|
const descriptor = file;
|
|
176
|
-
if ((0,
|
|
242
|
+
if ((0, protocol_1.isReactNative)()) {
|
|
177
243
|
// React Native file descriptor — RN's FormData handles {uri, type, name} natively.
|
|
178
244
|
// It reads the file from disk during the multipart request — no in-JS Blob
|
|
179
245
|
// conversion (which would fail on Hermes for ArrayBuffer-backed Blobs).
|
|
@@ -5,7 +5,7 @@ exports.OxyServicesAuthMixin = OxyServicesAuthMixin;
|
|
|
5
5
|
const OxyServices_errors_1 = require("../OxyServices.errors");
|
|
6
6
|
const keyManager_1 = require("../crypto/keyManager");
|
|
7
7
|
const signatureService_1 = require("../crypto/signatureService");
|
|
8
|
-
const
|
|
8
|
+
const protocol_1 = require("@oxyhq/protocol");
|
|
9
9
|
const loggerUtils_1 = require("../utils/loggerUtils");
|
|
10
10
|
const userIdentity_1 = require("../utils/userIdentity");
|
|
11
11
|
/**
|
|
@@ -61,7 +61,7 @@ function OxyServicesAuthMixin(Base) {
|
|
|
61
61
|
* @internal
|
|
62
62
|
*/
|
|
63
63
|
async _hashApiKey(apiKey) {
|
|
64
|
-
const nodeCrypto = await (0,
|
|
64
|
+
const nodeCrypto = await (0, protocol_1.loadNodeCrypto)();
|
|
65
65
|
return nodeCrypto.createHash('sha256').update(apiKey).digest('hex');
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
@@ -113,7 +113,7 @@ function OxyServicesAuthMixin(Base) {
|
|
|
113
113
|
// Verify the secret on every cache hit, regardless of token freshness.
|
|
114
114
|
// Constant-time compare prevents timing oracles on the stored secret.
|
|
115
115
|
if (entry) {
|
|
116
|
-
const nodeCrypto = await (0,
|
|
116
|
+
const nodeCrypto = await (0, protocol_1.loadNodeCrypto)();
|
|
117
117
|
const storedSecretBuf = entry.secretBuf;
|
|
118
118
|
const lengthMatch = storedSecretBuf.length === providedSecretBuf.length;
|
|
119
119
|
// Always run timingSafeEqual on equal-length inputs to keep timing flat.
|
|
@@ -4,7 +4,7 @@ exports.parseIdPayload = parseIdPayload;
|
|
|
4
4
|
exports.parseAttestPayload = parseAttestPayload;
|
|
5
5
|
exports.verifyPublicCardAttestation = verifyPublicCardAttestation;
|
|
6
6
|
exports.OxyServicesCivicMixin = OxyServicesCivicMixin;
|
|
7
|
-
const
|
|
7
|
+
const protocol_1 = require("@oxyhq/protocol");
|
|
8
8
|
const signatureService_1 = require("../crypto/signatureService");
|
|
9
9
|
const OxyServices_identity_1 = require("./OxyServices.identity");
|
|
10
10
|
const mixinHelpers_1 = require("./mixinHelpers");
|
|
@@ -156,7 +156,7 @@ function parseAttestPayload(raw) {
|
|
|
156
156
|
* key, matching the server which omits absent keys entirely) and checks the
|
|
157
157
|
* `ES256K-DER-SHA256` signature against `attestation.publicKey`.
|
|
158
158
|
*
|
|
159
|
-
* NEVER throws: `
|
|
159
|
+
* NEVER throws: `verifySignature` already swallows malformed-input
|
|
160
160
|
* errors and returns `false`, and an absent attestation short-circuits to
|
|
161
161
|
* `false`. A pure, reusable helper (Commons can call it on a cached card).
|
|
162
162
|
*
|
|
@@ -171,7 +171,7 @@ async function verifyPublicCardAttestation(card, attestation) {
|
|
|
171
171
|
if (!signature || !publicKey) {
|
|
172
172
|
return false;
|
|
173
173
|
}
|
|
174
|
-
return
|
|
174
|
+
return (0, protocol_1.verifySignature)((0, protocol_1.canonicalize)(card), signature, publicKey);
|
|
175
175
|
}
|
|
176
176
|
function OxyServicesCivicMixin(Base) {
|
|
177
177
|
return class extends Base {
|
|
@@ -5,7 +5,7 @@ exports.OxyServicesLanguageMixin = OxyServicesLanguageMixin;
|
|
|
5
5
|
* Language Methods Mixin
|
|
6
6
|
*/
|
|
7
7
|
const languageUtils_1 = require("../utils/languageUtils");
|
|
8
|
-
const
|
|
8
|
+
const protocol_1 = require("@oxyhq/protocol");
|
|
9
9
|
const debugUtils_1 = require("../shared/utils/debugUtils");
|
|
10
10
|
function OxyServicesLanguageMixin(Base) {
|
|
11
11
|
return class extends Base {
|
|
@@ -22,7 +22,7 @@ function OxyServicesLanguageMixin(Base) {
|
|
|
22
22
|
// `loadAsyncStorage` is per-platform: the RN variant statically imports
|
|
23
23
|
// @react-native-async-storage/async-storage, the default variant throws
|
|
24
24
|
// (never called outside RN because of the `isReactNative` gate above).
|
|
25
|
-
const asyncStorageModule = await (0,
|
|
25
|
+
const asyncStorageModule = await (0, protocol_1.loadAsyncStorage)();
|
|
26
26
|
const storage = asyncStorageModule.default;
|
|
27
27
|
return {
|
|
28
28
|
getItem: storage.getItem.bind(storage),
|
|
@@ -8,7 +8,7 @@ exports.OxyServicesUtilityMixin = OxyServicesUtilityMixin;
|
|
|
8
8
|
* and Express.js authentication middleware
|
|
9
9
|
*/
|
|
10
10
|
const jwt_decode_1 = require("jwt-decode");
|
|
11
|
-
const
|
|
11
|
+
const protocol_1 = require("@oxyhq/protocol");
|
|
12
12
|
const loggerUtils_1 = require("../utils/loggerUtils");
|
|
13
13
|
const mixinHelpers_1 = require("./mixinHelpers");
|
|
14
14
|
/**
|
|
@@ -50,8 +50,6 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
50
50
|
// monorepo-wide refactor of the mixin pipeline.
|
|
51
51
|
constructor(...args) {
|
|
52
52
|
super(...args);
|
|
53
|
-
/** @internal In-memory cache for acting-as verification results (TTL: 5 min) */
|
|
54
|
-
this._actingAsCache = new Map();
|
|
55
53
|
/**
|
|
56
54
|
* In-memory cache for service-acting-as verification.
|
|
57
55
|
* Negative results are cached for 1min to avoid hammering the verify
|
|
@@ -61,46 +59,6 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
61
59
|
*/
|
|
62
60
|
this._serviceActingAsCache = new Map();
|
|
63
61
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Verify that a user is authorized to act as an account (direct membership
|
|
66
|
-
* or inherited via an ancestor). Backed by `GET /accounts/verify-acting-as`.
|
|
67
|
-
* Results are cached in-memory for 5 minutes to avoid repeated API calls.
|
|
68
|
-
*
|
|
69
|
-
* @internal Used by the auth() middleware — not part of the public API
|
|
70
|
-
*/
|
|
71
|
-
async verifyActingAs(userId, accountId) {
|
|
72
|
-
const cacheKey = `${userId}:${accountId}`;
|
|
73
|
-
const now = Date.now();
|
|
74
|
-
// Check cache
|
|
75
|
-
const cached = this._actingAsCache.get(cacheKey);
|
|
76
|
-
if (cached && cached.expiresAt > now) {
|
|
77
|
-
return cached.result;
|
|
78
|
-
}
|
|
79
|
-
// Query the API
|
|
80
|
-
try {
|
|
81
|
-
const result = await this.makeRequest('GET', '/accounts/verify-acting-as', { accountId, userId }, { cache: false, retry: false, timeout: 5000 });
|
|
82
|
-
// Cache successful result for 5 minutes
|
|
83
|
-
this._actingAsCache.set(cacheKey, {
|
|
84
|
-
result: result && result.authorized ? result : null,
|
|
85
|
-
expiresAt: now + 5 * 60 * 1000,
|
|
86
|
-
});
|
|
87
|
-
return result && result.authorized ? result : null;
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
loggerUtils_1.logger.warn('[oxy.auth] verifyActingAs lookup failed — caching negative result', {
|
|
91
|
-
component: 'auth',
|
|
92
|
-
method: 'verifyActingAs',
|
|
93
|
-
userId,
|
|
94
|
-
accountId,
|
|
95
|
-
}, error);
|
|
96
|
-
// Cache negative result for 1 minute to avoid hammering on transient errors
|
|
97
|
-
this._actingAsCache.set(cacheKey, {
|
|
98
|
-
result: null,
|
|
99
|
-
expiresAt: now + 1 * 60 * 1000,
|
|
100
|
-
});
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
62
|
/**
|
|
105
63
|
* Verify that a service app holds an active delegation grant authorising
|
|
106
64
|
* it to act on behalf of `userId`. Returns the grant (with allowed scopes)
|
|
@@ -221,47 +179,6 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
221
179
|
const oxyInstance = this;
|
|
222
180
|
// Return an async middleware function
|
|
223
181
|
return async (req, res, next) => {
|
|
224
|
-
// Process X-Acting-As header for managed account identity delegation.
|
|
225
|
-
// Called after successful authentication, before next(). If the header
|
|
226
|
-
// is present, verifies authorization and swaps the request identity to
|
|
227
|
-
// the managed account, preserving the original user for audit trails.
|
|
228
|
-
const processActingAs = async () => {
|
|
229
|
-
const actingAsUserId = req.headers['x-acting-as'];
|
|
230
|
-
if (!actingAsUserId || typeof actingAsUserId !== 'string')
|
|
231
|
-
return true; // No header, proceed normally
|
|
232
|
-
const currentUserId = req.userId;
|
|
233
|
-
if (!currentUserId)
|
|
234
|
-
return true; // No authenticated user yet — nothing to swap
|
|
235
|
-
const verification = await oxyInstance.verifyActingAs(currentUserId, actingAsUserId);
|
|
236
|
-
if (!verification) {
|
|
237
|
-
const error = {
|
|
238
|
-
error: 'ACTING_AS_UNAUTHORIZED',
|
|
239
|
-
message: 'Not authorized to act as this account',
|
|
240
|
-
code: 'ACTING_AS_UNAUTHORIZED',
|
|
241
|
-
status: 403,
|
|
242
|
-
};
|
|
243
|
-
if (onError) {
|
|
244
|
-
onError(error);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
res.status(403).json(error);
|
|
248
|
-
}
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
// Preserve original user for audit trails
|
|
252
|
-
req.originalUser = { id: currentUserId, ...(req.user ?? {}) };
|
|
253
|
-
req.actingAs = { userId: actingAsUserId, role: verification.role };
|
|
254
|
-
// Swap user identity to the managed account
|
|
255
|
-
req.userId = actingAsUserId;
|
|
256
|
-
req.user = { id: actingAsUserId, _id: actingAsUserId };
|
|
257
|
-
if (debug) {
|
|
258
|
-
loggerUtils_1.logger.debug(`[oxy.auth] Acting as ${actingAsUserId} (role=${verification.role}) original=${currentUserId}`, {
|
|
259
|
-
component: 'auth',
|
|
260
|
-
method: 'auth.processActingAs',
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
return true;
|
|
264
|
-
};
|
|
265
182
|
try {
|
|
266
183
|
// Extract token from Authorization header.
|
|
267
184
|
// Node/Express normalizes `Authorization` to a string; we guard
|
|
@@ -343,9 +260,9 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
343
260
|
// Signature verification uses a manual HMAC-SHA256 compare because
|
|
344
261
|
// this file ships into RN/web bundles where `jsonwebtoken` is
|
|
345
262
|
// unavailable. The middleware only ever runs on Node hosts (see
|
|
346
|
-
//
|
|
347
|
-
// platform: the RN variant throws so Metro
|
|
348
|
-
// built-in reference.
|
|
263
|
+
// `@oxyhq/protocol`'s `platform/crypto` doc-comment), and
|
|
264
|
+
// `loadNodeCrypto` is per-platform: the RN variant throws so Metro
|
|
265
|
+
// never bundles a Node built-in reference.
|
|
349
266
|
try {
|
|
350
267
|
await verifyServiceTokenSignature(token, jwtSecret);
|
|
351
268
|
verifyServiceTokenClaims(decoded, {
|
|
@@ -554,10 +471,7 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
554
471
|
method: 'auth',
|
|
555
472
|
});
|
|
556
473
|
}
|
|
557
|
-
|
|
558
|
-
if (await processActingAs())
|
|
559
|
-
return next();
|
|
560
|
-
return;
|
|
474
|
+
return next();
|
|
561
475
|
}
|
|
562
476
|
catch (validationError) {
|
|
563
477
|
if (debug) {
|
|
@@ -621,9 +535,7 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
621
535
|
method: 'auth',
|
|
622
536
|
});
|
|
623
537
|
}
|
|
624
|
-
|
|
625
|
-
if (await processActingAs())
|
|
626
|
-
next();
|
|
538
|
+
next();
|
|
627
539
|
}
|
|
628
540
|
catch (error) {
|
|
629
541
|
const handled = oxyInstance.handleError(error);
|
|
@@ -861,7 +773,7 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
861
773
|
* `ServiceTokenSignatureError` on signature mismatch — both map to 401.
|
|
862
774
|
*/
|
|
863
775
|
async function verifyServiceTokenSignature(token, secret) {
|
|
864
|
-
const nodeCrypto = await (0,
|
|
776
|
+
const nodeCrypto = await (0, protocol_1.loadNodeCrypto)();
|
|
865
777
|
const { createHmac, timingSafeEqual } = nodeCrypto;
|
|
866
778
|
const parts = token.split('.');
|
|
867
779
|
if (parts.length !== 3) {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Extracted from {@link HttpService} so the identity-tag derivation is a pure,
|
|
6
6
|
* independently testable function with no dependency on instance/token state.
|
|
7
|
-
* The HTTP service injects the live access token
|
|
8
|
-
*
|
|
7
|
+
* The HTTP service injects the live access token; everything here is
|
|
8
|
+
* referentially transparent given that input.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.ANON_IDENTITY = void 0;
|
|
@@ -63,25 +63,23 @@ function fnv1a32(str) {
|
|
|
63
63
|
*
|
|
64
64
|
* We use the decoded user id rather than the raw JWT so the token never lands
|
|
65
65
|
* in a cache key (no token leakage through any cache-key logging, no key bloat).
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* Switching into a managed account mints a REAL new session whose access token
|
|
67
|
+
* carries the target account's id, so the identity tag changes naturally on a
|
|
68
|
+
* switch — there is no separate acting-as discriminator to fold in.
|
|
69
69
|
*
|
|
70
70
|
* @param accessToken The current bearer access token, or `null` when anonymous.
|
|
71
|
-
* @param actingAsUserId The active managed-account id, or `null`.
|
|
72
71
|
*/
|
|
73
|
-
function computeIdentityTag(accessToken
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
72
|
+
function computeIdentityTag(accessToken) {
|
|
73
|
+
if (!accessToken) {
|
|
74
|
+
return exports.ANON_IDENTITY;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const decoded = (0, jwt_decode_1.jwtDecode)(accessToken);
|
|
78
|
+
return decoded.userId || decoded.id || `t${fnv1a32(accessToken)}`;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Undecodable token — still partition it away from anon and from other
|
|
82
|
+
// tokens via a hash. Never silently fall back to ANON_IDENTITY.
|
|
83
|
+
return `t${fnv1a32(accessToken)}`;
|
|
85
84
|
}
|
|
86
|
-
return actingAsUserId ? `${principal}~as${actingAsUserId}` : principal;
|
|
87
85
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DeviceManager = void 0;
|
|
4
|
-
const
|
|
4
|
+
const protocol_1 = require("@oxyhq/protocol");
|
|
5
5
|
/**
|
|
6
6
|
* Client-side device management utility
|
|
7
7
|
* Handles persistent device identification across app sessions
|
|
@@ -22,7 +22,7 @@ class DeviceManager {
|
|
|
22
22
|
// `loadAsyncStorage` is per-platform: the RN variant statically imports
|
|
23
23
|
// @react-native-async-storage/async-storage, the default variant throws
|
|
24
24
|
// (never called outside RN because of the `isReactNative()` gate above).
|
|
25
|
-
const asyncStorageModule = await (0,
|
|
25
|
+
const asyncStorageModule = await (0, protocol_1.loadAsyncStorage)();
|
|
26
26
|
const storage = asyncStorageModule.default;
|
|
27
27
|
return {
|
|
28
28
|
getItem: storage.getItem.bind(storage),
|
|
@@ -12,8 +12,6 @@ exports.isWeb = isWeb;
|
|
|
12
12
|
exports.isNative = isNative;
|
|
13
13
|
exports.isIOS = isIOS;
|
|
14
14
|
exports.isAndroid = isAndroid;
|
|
15
|
-
exports.isReactNative = isReactNative;
|
|
16
|
-
exports.isNodeJS = isNodeJS;
|
|
17
15
|
exports.setPlatformOS = setPlatformOS;
|
|
18
16
|
/**
|
|
19
17
|
* Detect the current platform without importing react-native
|
|
@@ -84,18 +82,6 @@ function isIOS() {
|
|
|
84
82
|
function isAndroid() {
|
|
85
83
|
return getPlatformOS() === 'android';
|
|
86
84
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Check if running in React Native
|
|
89
|
-
*/
|
|
90
|
-
function isReactNative() {
|
|
91
|
-
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Check if running in Node.js
|
|
95
|
-
*/
|
|
96
|
-
function isNodeJS() {
|
|
97
|
-
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
98
|
-
}
|
|
99
85
|
/**
|
|
100
86
|
* Set the platform OS explicitly
|
|
101
87
|
* Called by React Native entry point to register the platform
|