@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.
Files changed (112) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +6 -18
  3. package/dist/cjs/OxyServices.base.js +0 -21
  4. package/dist/cjs/crypto/keyManager.js +7 -7
  5. package/dist/cjs/crypto/polyfill.js +6 -5
  6. package/dist/cjs/crypto/signatureService.js +44 -220
  7. package/dist/cjs/index.js +4 -8
  8. package/dist/cjs/mixins/OxyServices.accounts.js +90 -0
  9. package/dist/cjs/mixins/OxyServices.assets.js +68 -2
  10. package/dist/cjs/mixins/OxyServices.auth.js +3 -3
  11. package/dist/cjs/mixins/OxyServices.civic.js +3 -3
  12. package/dist/cjs/mixins/OxyServices.language.js +2 -2
  13. package/dist/cjs/mixins/OxyServices.utility.js +7 -95
  14. package/dist/cjs/utils/cacheKey.js +17 -19
  15. package/dist/cjs/utils/deviceManager.js +2 -2
  16. package/dist/cjs/utils/platform.js +0 -14
  17. package/dist/esm/.tsbuildinfo +1 -1
  18. package/dist/esm/HttpService.js +6 -18
  19. package/dist/esm/OxyServices.base.js +0 -21
  20. package/dist/esm/crypto/keyManager.js +4 -4
  21. package/dist/esm/crypto/polyfill.js +5 -4
  22. package/dist/esm/crypto/signatureService.js +39 -214
  23. package/dist/esm/index.js +1 -2
  24. package/dist/esm/mixins/OxyServices.accounts.js +90 -0
  25. package/dist/esm/mixins/OxyServices.assets.js +67 -1
  26. package/dist/esm/mixins/OxyServices.auth.js +1 -1
  27. package/dist/esm/mixins/OxyServices.civic.js +3 -3
  28. package/dist/esm/mixins/OxyServices.language.js +1 -1
  29. package/dist/esm/mixins/OxyServices.utility.js +6 -94
  30. package/dist/esm/utils/cacheKey.js +17 -19
  31. package/dist/esm/utils/deviceManager.js +1 -1
  32. package/dist/esm/utils/platform.js +0 -12
  33. package/dist/types/.tsbuildinfo +1 -1
  34. package/dist/types/HttpService.d.ts +3 -6
  35. package/dist/types/OxyServices.base.d.ts +0 -17
  36. package/dist/types/crypto/polyfill.d.ts +2 -2
  37. package/dist/types/crypto/signatureService.d.ts +18 -84
  38. package/dist/types/index.d.ts +4 -5
  39. package/dist/types/mixins/OxyServices.accounts.d.ts +66 -5
  40. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  41. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  42. package/dist/types/mixins/OxyServices.assets.d.ts +42 -3
  43. package/dist/types/mixins/OxyServices.auth.d.ts +0 -2
  44. package/dist/types/mixins/OxyServices.civic.d.ts +3 -5
  45. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  46. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  47. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  48. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  49. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -2
  50. package/dist/types/mixins/OxyServices.identity.d.ts +8 -5
  51. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  52. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  53. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  54. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -44
  55. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  56. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  57. package/dist/types/mixins/OxyServices.redirect.d.ts +0 -2
  58. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  59. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  60. package/dist/types/mixins/OxyServices.silent.d.ts +0 -2
  61. package/dist/types/mixins/OxyServices.sso.d.ts +0 -2
  62. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  63. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  64. package/dist/types/mixins/OxyServices.utility.d.ts +0 -32
  65. package/dist/types/models/interfaces.d.ts +18 -0
  66. package/dist/types/server/auth.d.ts +0 -6
  67. package/dist/types/server/index.d.ts +1 -1
  68. package/dist/types/utils/cacheKey.d.ts +6 -7
  69. package/dist/types/utils/platform.d.ts +0 -8
  70. package/package.json +4 -7
  71. package/src/HttpService.ts +6 -22
  72. package/src/OxyServices.base.ts +0 -23
  73. package/src/__tests__/httpServiceCache.test.ts +0 -19
  74. package/src/crypto/__tests__/keyManager.atomicity.test.ts +2 -1
  75. package/src/crypto/__tests__/keyManager.test.ts +9 -7
  76. package/src/crypto/__tests__/signChallengeShared.test.ts +2 -1
  77. package/src/crypto/__tests__/signedRecord.test.ts +37 -150
  78. package/src/crypto/keyManager.ts +28 -17
  79. package/src/crypto/polyfill.ts +5 -4
  80. package/src/crypto/signatureService.ts +67 -255
  81. package/src/index.ts +4 -3
  82. package/src/mixins/OxyServices.accounts.ts +136 -3
  83. package/src/mixins/OxyServices.assets.ts +96 -2
  84. package/src/mixins/OxyServices.auth.ts +1 -1
  85. package/src/mixins/OxyServices.civic.ts +6 -17
  86. package/src/mixins/OxyServices.identity.ts +8 -2
  87. package/src/mixins/OxyServices.language.ts +1 -1
  88. package/src/mixins/OxyServices.nodes.ts +1 -12
  89. package/src/mixins/OxyServices.utility.ts +6 -119
  90. package/src/mixins/__tests__/OxyServices.civic.test.ts +2 -2
  91. package/src/mixins/__tests__/OxyServices.serviceAssetMetadata.test.ts +116 -0
  92. package/src/mixins/__tests__/accounts.test.ts +120 -0
  93. package/src/models/interfaces.ts +19 -0
  94. package/src/server/auth.ts +0 -7
  95. package/src/server/index.ts +0 -1
  96. package/src/utils/__tests__/cacheKey.test.ts +0 -0
  97. package/src/utils/cacheKey.ts +16 -21
  98. package/src/utils/deviceManager.ts +1 -1
  99. package/src/utils/platform.ts +0 -14
  100. package/dist/cjs/crypto/canonicalJson.js +0 -107
  101. package/dist/cjs/utils/platformCrypto.js +0 -165
  102. package/dist/cjs/utils/platformCrypto.native.js +0 -123
  103. package/dist/esm/crypto/canonicalJson.js +0 -104
  104. package/dist/esm/utils/platformCrypto.js +0 -125
  105. package/dist/esm/utils/platformCrypto.native.js +0 -80
  106. package/dist/types/crypto/canonicalJson.d.ts +0 -44
  107. package/dist/types/utils/platformCrypto.d.ts +0 -87
  108. package/dist/types/utils/platformCrypto.native.d.ts +0 -54
  109. package/src/crypto/__tests__/canonicalJson.test.ts +0 -116
  110. package/src/crypto/canonicalJson.ts +0 -120
  111. package/src/utils/platformCrypto.native.ts +0 -101
  112. 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 platform_1 = require("../utils/platform");
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, platform_1.isReactNative)()) {
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 platformCrypto_1 = require("../utils/platformCrypto");
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, platformCrypto_1.loadNodeCrypto)();
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, platformCrypto_1.loadNodeCrypto)();
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 canonicalJson_1 = require("../crypto/canonicalJson");
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: `SignatureService.verify` already swallows malformed-input
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 signatureService_1.SignatureService.verify((0, canonicalJson_1.canonicalize)(card), signature, publicKey);
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 platformCrypto_1 = require("../utils/platformCrypto");
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, platformCrypto_1.loadAsyncStorage)();
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 platformCrypto_1 = require("../utils/platformCrypto");
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
- // platformCrypto's doc-comment), and `loadNodeCrypto` is per-
347
- // platform: the RN variant throws so Metro never bundles a Node
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
- // Process X-Acting-As header before proceeding
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
- // Process X-Acting-As header before proceeding
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, platformCrypto_1.loadNodeCrypto)();
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 and acting-as id; everything
8
- * here is referentially transparent given those inputs.
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
- * The acting-as id is folded in because managed-account responses differ per
67
- * acting identity and `X-Acting-As` already changes the server response for
68
- * the same bearer token.
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, actingAsUserId) {
74
- let principal = exports.ANON_IDENTITY;
75
- if (accessToken) {
76
- try {
77
- const decoded = (0, jwt_decode_1.jwtDecode)(accessToken);
78
- principal = 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
- principal = `t${fnv1a32(accessToken)}`;
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 platformCrypto_1 = require("./platformCrypto");
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, platformCrypto_1.loadAsyncStorage)();
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