@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,4 +1,13 @@
1
- import { isReactNative } from '../utils/platform.js';
1
+ import { isReactNative } from '@oxyhq/protocol';
2
+ import { logger } from '../utils/loggerUtils.js';
3
+ import { extractErrorStatus } from '../utils/errorUtils.js';
4
+ /**
5
+ * Maximum number of ids sent per `POST /assets/service/by-ids` request. Matches
6
+ * the server-side batch cap (the route rejects empty or > 100 id arrays with a
7
+ * 400); larger inputs are split into multiple chunked calls and merged. Mirrors
8
+ * `getUsersByIds`'s `USERS_BY_IDS_CHUNK_SIZE`.
9
+ */
10
+ const SERVICE_ASSET_METADATA_CHUNK_SIZE = 100;
2
11
  export function OxyServicesAssetsMixin(Base) {
3
12
  return class extends Base {
4
13
  constructor(...args) {
@@ -144,6 +153,63 @@ export function OxyServicesAssetsMixin(Base) {
144
153
  }
145
154
  return urls;
146
155
  }
156
+ /**
157
+ * Resolve many Oxy asset ids to their content-addressed metadata in one
158
+ * round-trip per chunk via `POST /assets/service/by-ids` (body `{ ids }`).
159
+ *
160
+ * Returns each asset's `sha256`, `mime`, byte `size`, and `status` — built
161
+ * for server-to-server callers (e.g. Mention's MTN Protocol blob-ref
162
+ * resolution) that need the content hash for an asset id. Ids are
163
+ * deduplicated and validated (empty/blank ids dropped) before being split
164
+ * into chunks of {@link SERVICE_ASSET_METADATA_CHUNK_SIZE} (the server-side
165
+ * cap). The server omits unknown/deleted ids from each chunk's `data`, so
166
+ * the merged result may be shorter than the requested id list and the caller
167
+ * is expected to map by `id`.
168
+ *
169
+ * **Service-token auth (required).** `/assets/service/by-ids` is guarded by
170
+ * `serviceAuthMiddleware` + the `files:read` scope and is called via
171
+ * `makeServiceRequest`, which attaches `Authorization: Bearer <serviceToken>`
172
+ * (the same client that calls `POST /assets/service/cache`). The calling
173
+ * client MUST be service-configured (`configureServiceAuth(apiKey,
174
+ * apiSecret)`) before invoking this method; otherwise `getServiceToken()`
175
+ * throws because no credentials are available. A plain user-session request
176
+ * is rejected by the route's service-auth guard.
177
+ *
178
+ * Resilience: chunks are independent. A failed chunk is logged and skipped —
179
+ * the method returns every entry that resolved successfully rather than
180
+ * discarding the whole call on one chunk's failure. An empty/whitespace-only
181
+ * input resolves immediately with `[]` and performs no network call.
182
+ *
183
+ * Not cached at the SDK layer: it's a POST keyed on a multi-id body (low hit
184
+ * rate), mirroring the sibling service/POST methods which never cache.
185
+ */
186
+ async getServiceAssetMetadataByIds(ids) {
187
+ const uniqueIds = Array.from(new Set(ids.filter((id) => typeof id === 'string' && id.trim().length > 0)));
188
+ if (uniqueIds.length === 0) {
189
+ return [];
190
+ }
191
+ const chunks = [];
192
+ for (let i = 0; i < uniqueIds.length; i += SERVICE_ASSET_METADATA_CHUNK_SIZE) {
193
+ chunks.push(uniqueIds.slice(i, i + SERVICE_ASSET_METADATA_CHUNK_SIZE));
194
+ }
195
+ // Run chunks concurrently; a single chunk failure must not sink the rest.
196
+ const settled = await Promise.all(chunks.map(async (chunk) => {
197
+ try {
198
+ const entries = await this.makeServiceRequest('POST', '/assets/service/by-ids', { ids: chunk });
199
+ return Array.isArray(entries) ? entries : [];
200
+ }
201
+ catch (error) {
202
+ logger.warn('getServiceAssetMetadataByIds: chunk failed, continuing with remaining chunks', {
203
+ method: 'getServiceAssetMetadataByIds',
204
+ chunkSize: chunk.length,
205
+ status: extractErrorStatus(error),
206
+ error: error instanceof Error ? error.message : String(error),
207
+ });
208
+ return [];
209
+ }
210
+ }));
211
+ return settled.flat();
212
+ }
147
213
  /**
148
214
  * Upload raw file data
149
215
  */
@@ -1,7 +1,7 @@
1
1
  import { OxyAuthenticationError } from '../OxyServices.errors.js';
2
2
  import { KeyManager } from '../crypto/keyManager.js';
3
3
  import { SignatureService } from '../crypto/signatureService.js';
4
- import { loadNodeCrypto } from '../utils/platformCrypto.js';
4
+ import { loadNodeCrypto } from '@oxyhq/protocol';
5
5
  import { logger } from '../utils/loggerUtils.js';
6
6
  import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity.js';
7
7
  /**
@@ -1,4 +1,4 @@
1
- import { canonicalize } from '../crypto/canonicalJson.js';
1
+ import { canonicalize, verifySignature } from '@oxyhq/protocol';
2
2
  import { SignatureService } from '../crypto/signatureService.js';
3
3
  import { buildUserDid } from './OxyServices.identity.js';
4
4
  import { CACHE_TIMES } from './mixinHelpers.js';
@@ -150,7 +150,7 @@ export function parseAttestPayload(raw) {
150
150
  * key, matching the server which omits absent keys entirely) and checks the
151
151
  * `ES256K-DER-SHA256` signature against `attestation.publicKey`.
152
152
  *
153
- * NEVER throws: `SignatureService.verify` already swallows malformed-input
153
+ * NEVER throws: `verifySignature` already swallows malformed-input
154
154
  * errors and returns `false`, and an absent attestation short-circuits to
155
155
  * `false`. A pure, reusable helper (Commons can call it on a cached card).
156
156
  *
@@ -165,7 +165,7 @@ export async function verifyPublicCardAttestation(card, attestation) {
165
165
  if (!signature || !publicKey) {
166
166
  return false;
167
167
  }
168
- return SignatureService.verify(canonicalize(card), signature, publicKey);
168
+ return verifySignature(canonicalize(card), signature, publicKey);
169
169
  }
170
170
  export function OxyServicesCivicMixin(Base) {
171
171
  return class extends Base {
@@ -2,7 +2,7 @@
2
2
  * Language Methods Mixin
3
3
  */
4
4
  import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils.js';
5
- import { loadAsyncStorage } from '../utils/platformCrypto.js';
5
+ import { loadAsyncStorage } from '@oxyhq/protocol';
6
6
  import { isDev } from '../shared/utils/debugUtils.js';
7
7
  export function OxyServicesLanguageMixin(Base) {
8
8
  return class extends Base {
@@ -5,7 +5,7 @@
5
5
  * and Express.js authentication middleware
6
6
  */
7
7
  import { jwtDecode } from 'jwt-decode';
8
- import { loadNodeCrypto } from '../utils/platformCrypto.js';
8
+ import { loadNodeCrypto } from '@oxyhq/protocol';
9
9
  import { logger } from '../utils/loggerUtils.js';
10
10
  import { CACHE_TIMES } from './mixinHelpers.js';
11
11
  /**
@@ -47,8 +47,6 @@ export function OxyServicesUtilityMixin(Base) {
47
47
  // monorepo-wide refactor of the mixin pipeline.
48
48
  constructor(...args) {
49
49
  super(...args);
50
- /** @internal In-memory cache for acting-as verification results (TTL: 5 min) */
51
- this._actingAsCache = new Map();
52
50
  /**
53
51
  * In-memory cache for service-acting-as verification.
54
52
  * Negative results are cached for 1min to avoid hammering the verify
@@ -58,46 +56,6 @@ export function OxyServicesUtilityMixin(Base) {
58
56
  */
59
57
  this._serviceActingAsCache = new Map();
60
58
  }
61
- /**
62
- * Verify that a user is authorized to act as an account (direct membership
63
- * or inherited via an ancestor). Backed by `GET /accounts/verify-acting-as`.
64
- * Results are cached in-memory for 5 minutes to avoid repeated API calls.
65
- *
66
- * @internal Used by the auth() middleware — not part of the public API
67
- */
68
- async verifyActingAs(userId, accountId) {
69
- const cacheKey = `${userId}:${accountId}`;
70
- const now = Date.now();
71
- // Check cache
72
- const cached = this._actingAsCache.get(cacheKey);
73
- if (cached && cached.expiresAt > now) {
74
- return cached.result;
75
- }
76
- // Query the API
77
- try {
78
- const result = await this.makeRequest('GET', '/accounts/verify-acting-as', { accountId, userId }, { cache: false, retry: false, timeout: 5000 });
79
- // Cache successful result for 5 minutes
80
- this._actingAsCache.set(cacheKey, {
81
- result: result && result.authorized ? result : null,
82
- expiresAt: now + 5 * 60 * 1000,
83
- });
84
- return result && result.authorized ? result : null;
85
- }
86
- catch (error) {
87
- logger.warn('[oxy.auth] verifyActingAs lookup failed — caching negative result', {
88
- component: 'auth',
89
- method: 'verifyActingAs',
90
- userId,
91
- accountId,
92
- }, error);
93
- // Cache negative result for 1 minute to avoid hammering on transient errors
94
- this._actingAsCache.set(cacheKey, {
95
- result: null,
96
- expiresAt: now + 1 * 60 * 1000,
97
- });
98
- return null;
99
- }
100
- }
101
59
  /**
102
60
  * Verify that a service app holds an active delegation grant authorising
103
61
  * it to act on behalf of `userId`. Returns the grant (with allowed scopes)
@@ -218,47 +176,6 @@ export function OxyServicesUtilityMixin(Base) {
218
176
  const oxyInstance = this;
219
177
  // Return an async middleware function
220
178
  return async (req, res, next) => {
221
- // Process X-Acting-As header for managed account identity delegation.
222
- // Called after successful authentication, before next(). If the header
223
- // is present, verifies authorization and swaps the request identity to
224
- // the managed account, preserving the original user for audit trails.
225
- const processActingAs = async () => {
226
- const actingAsUserId = req.headers['x-acting-as'];
227
- if (!actingAsUserId || typeof actingAsUserId !== 'string')
228
- return true; // No header, proceed normally
229
- const currentUserId = req.userId;
230
- if (!currentUserId)
231
- return true; // No authenticated user yet — nothing to swap
232
- const verification = await oxyInstance.verifyActingAs(currentUserId, actingAsUserId);
233
- if (!verification) {
234
- const error = {
235
- error: 'ACTING_AS_UNAUTHORIZED',
236
- message: 'Not authorized to act as this account',
237
- code: 'ACTING_AS_UNAUTHORIZED',
238
- status: 403,
239
- };
240
- if (onError) {
241
- onError(error);
242
- }
243
- else {
244
- res.status(403).json(error);
245
- }
246
- return false;
247
- }
248
- // Preserve original user for audit trails
249
- req.originalUser = { id: currentUserId, ...(req.user ?? {}) };
250
- req.actingAs = { userId: actingAsUserId, role: verification.role };
251
- // Swap user identity to the managed account
252
- req.userId = actingAsUserId;
253
- req.user = { id: actingAsUserId, _id: actingAsUserId };
254
- if (debug) {
255
- logger.debug(`[oxy.auth] Acting as ${actingAsUserId} (role=${verification.role}) original=${currentUserId}`, {
256
- component: 'auth',
257
- method: 'auth.processActingAs',
258
- });
259
- }
260
- return true;
261
- };
262
179
  try {
263
180
  // Extract token from Authorization header.
264
181
  // Node/Express normalizes `Authorization` to a string; we guard
@@ -340,9 +257,9 @@ export function OxyServicesUtilityMixin(Base) {
340
257
  // Signature verification uses a manual HMAC-SHA256 compare because
341
258
  // this file ships into RN/web bundles where `jsonwebtoken` is
342
259
  // unavailable. The middleware only ever runs on Node hosts (see
343
- // platformCrypto's doc-comment), and `loadNodeCrypto` is per-
344
- // platform: the RN variant throws so Metro never bundles a Node
345
- // built-in reference.
260
+ // `@oxyhq/protocol`'s `platform/crypto` doc-comment), and
261
+ // `loadNodeCrypto` is per-platform: the RN variant throws so Metro
262
+ // never bundles a Node built-in reference.
346
263
  try {
347
264
  await verifyServiceTokenSignature(token, jwtSecret);
348
265
  verifyServiceTokenClaims(decoded, {
@@ -551,10 +468,7 @@ export function OxyServicesUtilityMixin(Base) {
551
468
  method: 'auth',
552
469
  });
553
470
  }
554
- // Process X-Acting-As header before proceeding
555
- if (await processActingAs())
556
- return next();
557
- return;
471
+ return next();
558
472
  }
559
473
  catch (validationError) {
560
474
  if (debug) {
@@ -618,9 +532,7 @@ export function OxyServicesUtilityMixin(Base) {
618
532
  method: 'auth',
619
533
  });
620
534
  }
621
- // Process X-Acting-As header before proceeding
622
- if (await processActingAs())
623
- next();
535
+ next();
624
536
  }
625
537
  catch (error) {
626
538
  const handled = oxyInstance.handleError(error);
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Extracted from {@link HttpService} so the identity-tag derivation is a pure,
5
5
  * independently testable function with no dependency on instance/token state.
6
- * The HTTP service injects the live access token and acting-as id; everything
7
- * here is referentially transparent given those inputs.
6
+ * The HTTP service injects the live access token; everything here is
7
+ * referentially transparent given that input.
8
8
  */
9
9
  import { jwtDecode } from 'jwt-decode';
10
10
  /**
@@ -58,25 +58,23 @@ export function fnv1a32(str) {
58
58
  *
59
59
  * We use the decoded user id rather than the raw JWT so the token never lands
60
60
  * in a cache key (no token leakage through any cache-key logging, no key bloat).
61
- * The acting-as id is folded in because managed-account responses differ per
62
- * acting identity and `X-Acting-As` already changes the server response for
63
- * the same bearer token.
61
+ * Switching into a managed account mints a REAL new session whose access token
62
+ * carries the target account's id, so the identity tag changes naturally on a
63
+ * switch there is no separate acting-as discriminator to fold in.
64
64
  *
65
65
  * @param accessToken The current bearer access token, or `null` when anonymous.
66
- * @param actingAsUserId The active managed-account id, or `null`.
67
66
  */
68
- export function computeIdentityTag(accessToken, actingAsUserId) {
69
- let principal = ANON_IDENTITY;
70
- if (accessToken) {
71
- try {
72
- const decoded = jwtDecode(accessToken);
73
- principal = decoded.userId || decoded.id || `t${fnv1a32(accessToken)}`;
74
- }
75
- catch {
76
- // Undecodable token — still partition it away from anon and from other
77
- // tokens via a hash. Never silently fall back to ANON_IDENTITY.
78
- principal = `t${fnv1a32(accessToken)}`;
79
- }
67
+ export function computeIdentityTag(accessToken) {
68
+ if (!accessToken) {
69
+ return ANON_IDENTITY;
70
+ }
71
+ try {
72
+ const decoded = jwtDecode(accessToken);
73
+ return decoded.userId || decoded.id || `t${fnv1a32(accessToken)}`;
74
+ }
75
+ catch {
76
+ // Undecodable token still partition it away from anon and from other
77
+ // tokens via a hash. Never silently fall back to ANON_IDENTITY.
78
+ return `t${fnv1a32(accessToken)}`;
80
79
  }
81
- return actingAsUserId ? `${principal}~as${actingAsUserId}` : principal;
82
80
  }
@@ -1,4 +1,4 @@
1
- import { loadAsyncStorage } from './platformCrypto.js';
1
+ import { loadAsyncStorage } from '@oxyhq/protocol';
2
2
  /**
3
3
  * Client-side device management utility
4
4
  * Handles persistent device identification across app sessions
@@ -74,18 +74,6 @@ export function isIOS() {
74
74
  export function isAndroid() {
75
75
  return getPlatformOS() === 'android';
76
76
  }
77
- /**
78
- * Check if running in React Native
79
- */
80
- export function isReactNative() {
81
- return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
82
- }
83
- /**
84
- * Check if running in Node.js
85
- */
86
- export function isNodeJS() {
87
- return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
88
- }
89
77
  /**
90
78
  * Set the platform OS explicitly
91
79
  * Called by React Native entry point to register the platform