@oxyhq/core 4.0.1 → 5.0.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 (109) 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 +54 -0
  9. package/dist/cjs/mixins/OxyServices.assets.js +2 -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 +54 -0
  25. package/dist/esm/mixins/OxyServices.assets.js +1 -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 +3 -4
  39. package/dist/types/mixins/OxyServices.accounts.d.ts +57 -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 +0 -2
  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/server/auth.d.ts +0 -6
  66. package/dist/types/server/index.d.ts +1 -1
  67. package/dist/types/utils/cacheKey.d.ts +6 -7
  68. package/dist/types/utils/platform.d.ts +0 -8
  69. package/package.json +4 -7
  70. package/src/HttpService.ts +6 -22
  71. package/src/OxyServices.base.ts +0 -23
  72. package/src/__tests__/httpServiceCache.test.ts +0 -19
  73. package/src/crypto/__tests__/keyManager.atomicity.test.ts +2 -1
  74. package/src/crypto/__tests__/keyManager.test.ts +9 -7
  75. package/src/crypto/__tests__/signChallengeShared.test.ts +2 -1
  76. package/src/crypto/__tests__/signedRecord.test.ts +37 -150
  77. package/src/crypto/keyManager.ts +28 -17
  78. package/src/crypto/polyfill.ts +5 -4
  79. package/src/crypto/signatureService.ts +67 -255
  80. package/src/index.ts +3 -3
  81. package/src/mixins/OxyServices.accounts.ts +91 -3
  82. package/src/mixins/OxyServices.assets.ts +1 -1
  83. package/src/mixins/OxyServices.auth.ts +1 -1
  84. package/src/mixins/OxyServices.civic.ts +6 -17
  85. package/src/mixins/OxyServices.identity.ts +8 -2
  86. package/src/mixins/OxyServices.language.ts +1 -1
  87. package/src/mixins/OxyServices.nodes.ts +1 -12
  88. package/src/mixins/OxyServices.utility.ts +6 -119
  89. package/src/mixins/__tests__/OxyServices.civic.test.ts +2 -2
  90. package/src/mixins/__tests__/accounts.test.ts +70 -0
  91. package/src/server/auth.ts +0 -7
  92. package/src/server/index.ts +0 -1
  93. package/src/utils/__tests__/cacheKey.test.ts +0 -0
  94. package/src/utils/cacheKey.ts +16 -21
  95. package/src/utils/deviceManager.ts +1 -1
  96. package/src/utils/platform.ts +0 -14
  97. package/dist/cjs/crypto/canonicalJson.js +0 -107
  98. package/dist/cjs/utils/platformCrypto.js +0 -165
  99. package/dist/cjs/utils/platformCrypto.native.js +0 -123
  100. package/dist/esm/crypto/canonicalJson.js +0 -104
  101. package/dist/esm/utils/platformCrypto.js +0 -125
  102. package/dist/esm/utils/platformCrypto.native.js +0 -80
  103. package/dist/types/crypto/canonicalJson.d.ts +0 -44
  104. package/dist/types/utils/platformCrypto.d.ts +0 -87
  105. package/dist/types/utils/platformCrypto.native.d.ts +0 -54
  106. package/src/crypto/__tests__/canonicalJson.test.ts +0 -116
  107. package/src/crypto/canonicalJson.ts +0 -120
  108. package/src/utils/platformCrypto.native.ts +0 -101
  109. package/src/utils/platformCrypto.ts +0 -145
@@ -4,7 +4,7 @@
4
4
  import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
5
5
  import type { LanguageMetadata } from '../utils/languageUtils';
6
6
  import type { OxyServicesBase } from '../OxyServices.base';
7
- import { loadAsyncStorage } from '../utils/platformCrypto';
7
+ import { loadAsyncStorage } from '@oxyhq/protocol';
8
8
  import { isDev } from '../shared/utils/debugUtils';
9
9
 
10
10
  export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base: T) {
@@ -40,6 +40,7 @@
40
40
  * server's `serializeNode` projection exactly. Dates cross the wire as ISO
41
41
  * strings.
42
42
  */
43
+ import type { ChainHeadResponse } from '@oxyhq/contracts';
43
44
  import type { OxyServicesBase } from '../OxyServices.base';
44
45
  import { SignatureService } from '../crypto/signatureService';
45
46
  import { buildUserDid } from './OxyServices.identity';
@@ -157,18 +158,6 @@ export interface RemoveNodeResult {
157
158
  revoked: boolean;
158
159
  }
159
160
 
160
- /**
161
- * The current chain head as returned by `GET /identity/records/:userId/chain/head`.
162
- * `headRecordId` is `null` and `seq` is `-1` when the subject has no chain yet,
163
- * so the next record's coordinates are always `seq: head.seq + 1` (genesis = 0)
164
- * and `prev: head.headRecordId` (genesis = null).
165
- */
166
- interface ChainHeadResponse {
167
- headRecordId: string | null;
168
- seq: number;
169
- recordCount: number;
170
- }
171
-
172
161
  export function OxyServicesNodesMixin<T extends typeof OxyServicesBase>(Base: T) {
173
162
  return class extends Base {
174
163
  constructor(...args: any[]) {
@@ -7,7 +7,7 @@
7
7
  import { jwtDecode } from 'jwt-decode';
8
8
  import type { ApiError, User } from '../models/interfaces';
9
9
  import type { OxyServicesBase } from '../OxyServices.base';
10
- import { loadNodeCrypto } from '../utils/platformCrypto';
10
+ import { loadNodeCrypto } from '@oxyhq/protocol';
11
11
  import { logger } from '../utils/loggerUtils';
12
12
  import { CACHE_TIMES } from './mixinHelpers';
13
13
 
@@ -26,17 +26,6 @@ interface JwtPayload {
26
26
  [key: string]: unknown;
27
27
  }
28
28
 
29
- /**
30
- * Result from the account acting-as verification endpoint
31
- * (`GET /accounts/verify-acting-as`). Indicates whether a user is authorized to
32
- * act as a given account. The `account:act_as` capability is granted only to the
33
- * `owner`, `admin`, and `editor` account roles.
34
- */
35
- interface ActingAsVerification {
36
- authorized: boolean;
37
- role: 'owner' | 'admin' | 'editor';
38
- }
39
-
40
29
  /**
41
30
  * Result from the service-acting-as verification endpoint.
42
31
  * Confirms that a given service app holds an active delegation grant for
@@ -141,9 +130,6 @@ interface AuthMiddlewareOptions {
141
130
 
142
131
  export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base: T) {
143
132
  return class extends Base {
144
- /** @internal In-memory cache for acting-as verification results (TTL: 5 min) */
145
- _actingAsCache = new Map<string, { result: ActingAsVerification | null; expiresAt: number }>();
146
-
147
133
  /**
148
134
  * In-memory cache for service-acting-as verification.
149
135
  * Negative results are cached for 1min to avoid hammering the verify
@@ -161,55 +147,6 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
161
147
  super(...(args as [any]));
162
148
  }
163
149
 
164
- /**
165
- * Verify that a user is authorized to act as an account (direct membership
166
- * or inherited via an ancestor). Backed by `GET /accounts/verify-acting-as`.
167
- * Results are cached in-memory for 5 minutes to avoid repeated API calls.
168
- *
169
- * @internal Used by the auth() middleware — not part of the public API
170
- */
171
- async verifyActingAs(userId: string, accountId: string): Promise<ActingAsVerification | null> {
172
- const cacheKey = `${userId}:${accountId}`;
173
- const now = Date.now();
174
-
175
- // Check cache
176
- const cached = this._actingAsCache.get(cacheKey);
177
- if (cached && cached.expiresAt > now) {
178
- return cached.result;
179
- }
180
-
181
- // Query the API
182
- try {
183
- const result = await this.makeRequest<ActingAsVerification>(
184
- 'GET',
185
- '/accounts/verify-acting-as',
186
- { accountId, userId },
187
- { cache: false, retry: false, timeout: 5000 }
188
- );
189
-
190
- // Cache successful result for 5 minutes
191
- this._actingAsCache.set(cacheKey, {
192
- result: result && result.authorized ? result : null,
193
- expiresAt: now + 5 * 60 * 1000,
194
- });
195
-
196
- return result && result.authorized ? result : null;
197
- } catch (error) {
198
- logger.warn('[oxy.auth] verifyActingAs lookup failed — caching negative result', {
199
- component: 'auth',
200
- method: 'verifyActingAs',
201
- userId,
202
- accountId,
203
- }, error);
204
- // Cache negative result for 1 minute to avoid hammering on transient errors
205
- this._actingAsCache.set(cacheKey, {
206
- result: null,
207
- expiresAt: now + 1 * 60 * 1000,
208
- });
209
- return null;
210
- }
211
- }
212
-
213
150
  /**
214
151
  * Verify that a service app holds an active delegation grant authorising
215
152
  * it to act on behalf of `userId`. Returns the grant (with allowed scopes)
@@ -362,50 +299,6 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
362
299
 
363
300
  // Return an async middleware function
364
301
  return async (req: AuthReq, res: AuthRes, next: AuthNext) => {
365
- // Process X-Acting-As header for managed account identity delegation.
366
- // Called after successful authentication, before next(). If the header
367
- // is present, verifies authorization and swaps the request identity to
368
- // the managed account, preserving the original user for audit trails.
369
- const processActingAs = async (): Promise<boolean> => {
370
- const actingAsUserId = req.headers['x-acting-as'];
371
- if (!actingAsUserId || typeof actingAsUserId !== 'string') return true; // No header, proceed normally
372
- const currentUserId = req.userId;
373
- if (!currentUserId) return true; // No authenticated user yet — nothing to swap
374
-
375
- const verification = await oxyInstance.verifyActingAs(currentUserId, actingAsUserId);
376
- if (!verification) {
377
- const error = {
378
- error: 'ACTING_AS_UNAUTHORIZED',
379
- message: 'Not authorized to act as this account',
380
- code: 'ACTING_AS_UNAUTHORIZED',
381
- status: 403,
382
- };
383
- if (onError) {
384
- onError(error);
385
- } else {
386
- res.status(403).json(error);
387
- }
388
- return false;
389
- }
390
-
391
- // Preserve original user for audit trails
392
- req.originalUser = { id: currentUserId, ...(req.user ?? {}) };
393
- req.actingAs = { userId: actingAsUserId, role: verification.role };
394
-
395
- // Swap user identity to the managed account
396
- req.userId = actingAsUserId;
397
- req.user = { id: actingAsUserId, _id: actingAsUserId } as unknown as User;
398
-
399
- if (debug) {
400
- logger.debug(`[oxy.auth] Acting as ${actingAsUserId} (role=${verification.role}) original=${currentUserId}`, {
401
- component: 'auth',
402
- method: 'auth.processActingAs',
403
- });
404
- }
405
-
406
- return true;
407
- };
408
-
409
302
  try {
410
303
  // Extract token from Authorization header.
411
304
  // Node/Express normalizes `Authorization` to a string; we guard
@@ -490,9 +383,9 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
490
383
  // Signature verification uses a manual HMAC-SHA256 compare because
491
384
  // this file ships into RN/web bundles where `jsonwebtoken` is
492
385
  // unavailable. The middleware only ever runs on Node hosts (see
493
- // platformCrypto's doc-comment), and `loadNodeCrypto` is per-
494
- // platform: the RN variant throws so Metro never bundles a Node
495
- // built-in reference.
386
+ // `@oxyhq/protocol`'s `platform/crypto` doc-comment), and
387
+ // `loadNodeCrypto` is per-platform: the RN variant throws so Metro
388
+ // never bundles a Node built-in reference.
496
389
  try {
497
390
  await verifyServiceTokenSignature(token, jwtSecret);
498
391
  verifyServiceTokenClaims(decoded, {
@@ -712,9 +605,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
712
605
  });
713
606
  }
714
607
 
715
- // Process X-Acting-As header before proceeding
716
- if (await processActingAs()) return next();
717
- return;
608
+ return next();
718
609
  } catch (validationError) {
719
610
  if (debug) {
720
611
  logger.debug('[oxy.auth] Session validation failed', {
@@ -781,8 +672,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
781
672
  });
782
673
  }
783
674
 
784
- // Process X-Acting-As header before proceeding
785
- if (await processActingAs()) next();
675
+ next();
786
676
  } catch (error) {
787
677
  const handled = oxyInstance.handleError(error) as Error & {
788
678
  code?: string;
@@ -1128,8 +1018,6 @@ interface AuthReq {
1128
1018
  sessionId?: string | null;
1129
1019
  serviceApp?: ServiceApp;
1130
1020
  serviceActingAs?: { userId: string; scopes: string[] };
1131
- actingAs?: { userId: string; role: string };
1132
- originalUser?: { id: string } & Partial<User>;
1133
1021
  }
1134
1022
 
1135
1023
  interface AuthRes {
@@ -1146,7 +1034,6 @@ interface SocketLike {
1146
1034
  }
1147
1035
 
1148
1036
  interface OxyAuthInstance {
1149
- verifyActingAs(userId: string, accountId: string): Promise<ActingAsVerification | null>;
1150
1037
  verifyServiceActingAs(appId: string, userId: string): Promise<ServiceActingAsVerification | null>;
1151
1038
  validateSession(
1152
1039
  sessionId: string,
@@ -37,7 +37,7 @@ import type {
37
37
  VerifiableCredentialResponse,
38
38
  } from '@oxyhq/contracts';
39
39
  import { OxyServices } from '../../OxyServices';
40
- import { canonicalize } from '../../crypto/canonicalJson';
40
+ import { canonicalize, signMessage } from '@oxyhq/protocol';
41
41
  import { SignatureService } from '../../crypto/signatureService';
42
42
  import { parseAttestPayload, parseIdPayload, verifyPublicCardAttestation } from '../OxyServices.civic';
43
43
 
@@ -61,7 +61,7 @@ async function signCard(card: PublicCard): Promise<{ attestation: ExportAttestat
61
61
  const keyPair = ec.genKeyPair();
62
62
  const privateKey = keyPair.getPrivate('hex');
63
63
  const publicKey = keyPair.getPublic('hex');
64
- const signature = await SignatureService.signWithKey(canonicalize(card), privateKey);
64
+ const signature = await signMessage(canonicalize(card), privateKey);
65
65
  return {
66
66
  attestation: {
67
67
  issuer: 'did:web:api.oxy.so',
@@ -25,6 +25,7 @@ import type {
25
25
  ApplicationCredentialWithSecret,
26
26
  RotateApplicationCredentialResult,
27
27
  ApplicationUsageStats,
28
+ SwitchAccountResult,
28
29
  } from '../OxyServices.accounts';
29
30
 
30
31
  const setAccessTokenForTest = (oxy: OxyServices): void => {
@@ -174,6 +175,75 @@ describe('OxyServices.accounts', () => {
174
175
  });
175
176
  });
176
177
 
178
+ describe('switchToAccount', () => {
179
+ const switchResponse: SwitchAccountResult = {
180
+ sessionId: 'sess_switch',
181
+ deviceId: 'dev_switch',
182
+ expiresAt: '2026-06-30T01:00:00.000Z',
183
+ accessToken: 'access_switch',
184
+ user: { id: 'acc1', username: 'oxy-org', name: { displayName: 'Oxy Org' } },
185
+ authuser: 2,
186
+ };
187
+
188
+ it('posts to /:id/switch (no body, no cache), plants the token, sweeps the cache, and returns the session', async () => {
189
+ const setTokensSpy = jest.spyOn(oxy, 'setTokens');
190
+ const clearCacheSpy = jest.spyOn(oxy, 'clearCache');
191
+ makeRequestSpy.mockResolvedValue(switchResponse);
192
+
193
+ const result = await oxy.switchToAccount('acc1');
194
+
195
+ // Request shape: POST, exact path, no body, cache disabled.
196
+ expect(makeRequestSpy).toHaveBeenCalledWith(
197
+ 'POST',
198
+ '/accounts/acc1/switch',
199
+ undefined,
200
+ expect.objectContaining({ cache: false }),
201
+ );
202
+
203
+ // Session planting: the access token from the body is installed as the
204
+ // active token (mirrors claimSessionByToken / verifyChallenge).
205
+ expect(setTokensSpy).toHaveBeenCalledWith('access_switch');
206
+ expect(oxy.getAccessToken()).toBe('access_switch');
207
+ expect(oxy.hasValidToken()).toBe(true);
208
+
209
+ // Identity changed → the whole GET cache is swept so reads refetch as the
210
+ // new account, AND it happens AFTER the token is planted.
211
+ expect(clearCacheSpy).toHaveBeenCalledTimes(1);
212
+ expect(setTokensSpy.mock.invocationCallOrder[0]).toBeLessThan(
213
+ clearCacheSpy.mock.invocationCallOrder[0],
214
+ );
215
+
216
+ // The returned session carries the target account (id-normalised) + authuser.
217
+ expect(result).toEqual({ ...switchResponse, user: { id: 'acc1', username: 'oxy-org', name: { displayName: 'Oxy Org' } } });
218
+ expect(result.authuser).toBe(2);
219
+
220
+ setTokensSpy.mockRestore();
221
+ clearCacheSpy.mockRestore();
222
+ });
223
+
224
+ it('URL-encodes the accountId path segment', async () => {
225
+ makeRequestSpy.mockResolvedValue(switchResponse);
226
+ await oxy.switchToAccount('a b/c');
227
+ expect(makeRequestSpy.mock.calls[0][1]).toBe('/accounts/a%20b%2Fc/switch');
228
+ });
229
+
230
+ it('does NOT plant or sweep when the operator is not authorized (403 surfaces via handleError)', async () => {
231
+ const setTokensSpy = jest.spyOn(oxy, 'setTokens');
232
+ const clearCacheSpy = jest.spyOn(oxy, 'clearCache');
233
+ makeRequestSpy.mockRejectedValue(
234
+ Object.assign(new Error('forbidden'), { response: { status: 403 } }),
235
+ );
236
+
237
+ await expect(oxy.switchToAccount('acc1')).rejects.toThrow();
238
+ // A failed switch must NOT mutate session state.
239
+ expect(setTokensSpy).not.toHaveBeenCalled();
240
+ expect(clearCacheSpy).not.toHaveBeenCalled();
241
+
242
+ setTokensSpy.mockRestore();
243
+ clearCacheSpy.mockRestore();
244
+ });
245
+ });
246
+
177
247
  describe('createAccount', () => {
178
248
  it('posts the payload, unwraps `account`, and busts every list', async () => {
179
249
  makeRequestSpy.mockResolvedValue({ account: accountNodeFixture });
@@ -10,11 +10,6 @@ export interface OxyRequestUser {
10
10
  [key: string]: unknown;
11
11
  }
12
12
 
13
- export interface OxyActingAsContext {
14
- userId: string;
15
- role: 'owner' | 'admin' | 'editor';
16
- }
17
-
18
13
  export interface OxyServiceAppContext {
19
14
  appId: string;
20
15
  appName: string;
@@ -30,8 +25,6 @@ export interface OxyServiceActingAsContext {
30
25
  export interface OxyAuthRequest extends Request {
31
26
  userId?: string | null;
32
27
  user?: OxyRequestUser | null;
33
- originalUser?: OxyRequestUser | null;
34
- actingAs?: OxyActingAsContext;
35
28
  accessToken?: string;
36
29
  sessionId?: string | null;
37
30
  serviceApp?: OxyServiceAppContext;
@@ -24,7 +24,6 @@ export {
24
24
  requireOxyAuth,
25
25
  } from './auth';
26
26
  export type {
27
- OxyActingAsContext,
28
27
  OxyAuthenticatedRequest,
29
28
  OxyAuthMiddlewareOptions,
30
29
  OxyAuthRequest,
@@ -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
 
10
10
  import { jwtDecode } from 'jwt-decode';
@@ -72,27 +72,22 @@ export function fnv1a32(str: string): string {
72
72
  *
73
73
  * We use the decoded user id rather than the raw JWT so the token never lands
74
74
  * in a cache key (no token leakage through any cache-key logging, no key bloat).
75
- * The acting-as id is folded in because managed-account responses differ per
76
- * acting identity and `X-Acting-As` already changes the server response for
77
- * the same bearer token.
75
+ * Switching into a managed account mints a REAL new session whose access token
76
+ * carries the target account's id, so the identity tag changes naturally on a
77
+ * switch there is no separate acting-as discriminator to fold in.
78
78
  *
79
79
  * @param accessToken The current bearer access token, or `null` when anonymous.
80
- * @param actingAsUserId The active managed-account id, or `null`.
81
80
  */
82
- export function computeIdentityTag(
83
- accessToken: string | null,
84
- actingAsUserId: string | null,
85
- ): string {
86
- let principal = ANON_IDENTITY;
87
- if (accessToken) {
88
- try {
89
- const decoded = jwtDecode<CacheIdentityJwtPayload>(accessToken);
90
- principal = decoded.userId || decoded.id || `t${fnv1a32(accessToken)}`;
91
- } catch {
92
- // Undecodable token — still partition it away from anon and from other
93
- // tokens via a hash. Never silently fall back to ANON_IDENTITY.
94
- principal = `t${fnv1a32(accessToken)}`;
95
- }
81
+ export function computeIdentityTag(accessToken: string | null): string {
82
+ if (!accessToken) {
83
+ return ANON_IDENTITY;
84
+ }
85
+ try {
86
+ const decoded = jwtDecode<CacheIdentityJwtPayload>(accessToken);
87
+ return decoded.userId || decoded.id || `t${fnv1a32(accessToken)}`;
88
+ } catch {
89
+ // Undecodable token still partition it away from anon and from other
90
+ // tokens via a hash. Never silently fall back to ANON_IDENTITY.
91
+ return `t${fnv1a32(accessToken)}`;
96
92
  }
97
- return actingAsUserId ? `${principal}~as${actingAsUserId}` : principal;
98
93
  }
@@ -1,4 +1,4 @@
1
- import { loadAsyncStorage } from './platformCrypto';
1
+ import { loadAsyncStorage } from '@oxyhq/protocol';
2
2
 
3
3
  export interface DeviceFingerprint {
4
4
  userAgent: string;
@@ -88,20 +88,6 @@ export function isAndroid(): boolean {
88
88
  return getPlatformOS() === 'android';
89
89
  }
90
90
 
91
- /**
92
- * Check if running in React Native
93
- */
94
- export function isReactNative(): boolean {
95
- return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
96
- }
97
-
98
- /**
99
- * Check if running in Node.js
100
- */
101
- export function isNodeJS(): boolean {
102
- return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
103
- }
104
-
105
91
  /**
106
92
  * Set the platform OS explicitly
107
93
  * Called by React Native entry point to register the platform
@@ -1,107 +0,0 @@
1
- "use strict";
2
- /**
3
- * Canonical JSON (RFC 8785 / JCS-style) serialization.
4
- *
5
- * `canonicalize(value)` produces a deterministic string for any JSON-compatible
6
- * value so that a client which SIGNS a record and a server which VERIFIES it
7
- * agree byte-for-byte on the signing input — regardless of the order in which
8
- * object keys happen to be written, how the value was deserialized, or which
9
- * runtime built it.
10
- *
11
- * This is the load-bearing primitive for the self-sovereign identity layer's
12
- * signed records (`SignatureService.signRecord` + the API's record-verify path):
13
- * both sides import THIS function from `@oxyhq/core`, so cross-implementation
14
- * number/string formatting differences cannot cause a verify mismatch.
15
- *
16
- * Rules (the JSON Canonicalization Scheme subset we need):
17
- * - Objects: keys are sorted (ascending, by UTF-16 code unit — the default
18
- * `Array.prototype.sort` order) and serialized recursively. Properties whose
19
- * value is `undefined`, a function, or a symbol are OMITTED (matching
20
- * `JSON.stringify` object semantics).
21
- * - Arrays: element order is PRESERVED; `undefined`/function/symbol elements
22
- * serialize to `null` (matching `JSON.stringify` array semantics).
23
- * - `null`, booleans, strings, and finite numbers serialize via the standard
24
- * JSON representation.
25
- * - Values exposing a `toJSON()` method (e.g. `Date`) are replaced by its
26
- * result first, then serialized — so a `Date` and its ISO-string equivalent
27
- * canonicalize identically (the wire always carries the string form).
28
- * - Non-finite numbers (`NaN`, `Infinity`) and `bigint` are not part of the
29
- * JSON data model and throw, rather than silently producing `null`.
30
- *
31
- * Platform-agnostic — zero dependencies, no `require()`, no react/react-native/
32
- * expo. Safe in the dual CJS + ESM build.
33
- */
34
- Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.canonicalize = canonicalize;
36
- function hasToJSON(value) {
37
- return typeof value.toJSON === 'function';
38
- }
39
- /**
40
- * Serialize a single value into its canonical JSON fragment. Recursive; called
41
- * on each nested member. Object keys are sorted at every level.
42
- */
43
- function serialize(value) {
44
- if (value === null) {
45
- return 'null';
46
- }
47
- const valueType = typeof value;
48
- if (valueType === 'number') {
49
- if (!Number.isFinite(value)) {
50
- throw new Error('canonicalize: non-finite numbers cannot be serialized');
51
- }
52
- return JSON.stringify(value);
53
- }
54
- if (valueType === 'string' || valueType === 'boolean') {
55
- return JSON.stringify(value);
56
- }
57
- if (valueType === 'bigint') {
58
- throw new Error('canonicalize: bigint values cannot be serialized');
59
- }
60
- if (Array.isArray(value)) {
61
- const items = value.map((item) => {
62
- const itemType = typeof item;
63
- // JSON array semantics: undefined / function / symbol become null so the
64
- // element positions (and therefore the array length) are preserved.
65
- if (item === undefined || itemType === 'function' || itemType === 'symbol') {
66
- return 'null';
67
- }
68
- return serialize(item);
69
- });
70
- return `[${items.join(',')}]`;
71
- }
72
- if (valueType === 'object') {
73
- const obj = value;
74
- if (hasToJSON(obj)) {
75
- return serialize(obj.toJSON());
76
- }
77
- const record = obj;
78
- const parts = [];
79
- for (const key of Object.keys(record).sort()) {
80
- const member = record[key];
81
- const memberType = typeof member;
82
- // JSON object semantics: properties with undefined / function / symbol
83
- // values are omitted entirely.
84
- if (member === undefined || memberType === 'function' || memberType === 'symbol') {
85
- continue;
86
- }
87
- parts.push(`${JSON.stringify(key)}:${serialize(member)}`);
88
- }
89
- return `{${parts.join(',')}}`;
90
- }
91
- // undefined / function / symbol at the top level have no JSON representation.
92
- throw new Error(`canonicalize: cannot serialize a value of type ${valueType}`);
93
- }
94
- /**
95
- * Produce the canonical JSON string for `value`.
96
- *
97
- * Deterministic: two structurally-equal values yield identical strings even if
98
- * their object keys were written in different orders. Use this — never an
99
- * ad-hoc `JSON.stringify` of a hand-sorted object — as the signing input for
100
- * signed records, so client signing and server verification cannot drift.
101
- *
102
- * @throws if `value` (or any nested member used as the top-level/primitive)
103
- * contains a non-finite number or a `bigint`, which have no JSON form.
104
- */
105
- function canonicalize(value) {
106
- return serialize(value);
107
- }