@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
@@ -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,125 @@ describe('OxyServices.accounts', () => {
174
175
  });
175
176
  });
176
177
 
178
+ describe('switchToAccount', () => {
179
+ // The switch route NO LONGER returns `authuser` or sets the device cookie —
180
+ // it can't (it's at /accounts/*, outside the Path=/auth cookie scope). The SDK
181
+ // establishes the cookie + resolves the slot via a follow-up POST /auth/session.
182
+ const switchResponse: SwitchAccountResult = {
183
+ sessionId: 'sess_switch',
184
+ deviceId: 'dev_switch',
185
+ expiresAt: '2026-06-30T01:00:00.000Z',
186
+ accessToken: 'access_switch',
187
+ user: { id: 'acc1', username: 'oxy-org', name: { displayName: 'Oxy Org' } },
188
+ };
189
+ // POST /auth/session establishes the cookie in a correctly-allocated NEW slot
190
+ // and returns that slot + a fresh access token off the same session.
191
+ const sessionResponse = { accessToken: 'access_session', authuser: 1 };
192
+
193
+ // Route makeRequest by path: the switch call vs the /auth/session establishment.
194
+ const routeByPath = (response = switchResponse) =>
195
+ makeRequestSpy.mockImplementation((_method: string, path: string) =>
196
+ Promise.resolve(path === '/auth/session' ? sessionResponse : response),
197
+ );
198
+
199
+ it('posts to /:id/switch then establishes the cookie via POST /auth/session, planting both tokens and sweeping the cache', async () => {
200
+ const setTokensSpy = jest.spyOn(oxy, 'setTokens');
201
+ const clearCacheSpy = jest.spyOn(oxy, 'clearCache');
202
+ routeByPath();
203
+
204
+ const result = await oxy.switchToAccount('acc1');
205
+
206
+ // Switch request shape: POST, exact path, no body, cache disabled.
207
+ expect(makeRequestSpy).toHaveBeenCalledWith(
208
+ 'POST',
209
+ '/accounts/acc1/switch',
210
+ undefined,
211
+ expect.objectContaining({ cache: false }),
212
+ );
213
+ // Then the canonical refresh-cookie establishment under /auth (where the
214
+ // device's oxy_rt_* slots ARE visible, so a NEW slot is allocated).
215
+ expect(makeRequestSpy).toHaveBeenCalledWith(
216
+ 'POST',
217
+ '/auth/session',
218
+ undefined,
219
+ expect.objectContaining({ cache: false }),
220
+ );
221
+
222
+ // The switch token is planted first; /auth/session's fresh token re-planted.
223
+ expect(setTokensSpy).toHaveBeenNthCalledWith(1, 'access_switch');
224
+ expect(setTokensSpy).toHaveBeenNthCalledWith(2, 'access_session');
225
+ expect(oxy.getAccessToken()).toBe('access_session');
226
+ expect(oxy.hasValidToken()).toBe(true);
227
+
228
+ // Cache swept once, AFTER both tokens are planted.
229
+ expect(clearCacheSpy).toHaveBeenCalledTimes(1);
230
+ expect(setTokensSpy.mock.invocationCallOrder[1]).toBeLessThan(
231
+ clearCacheSpy.mock.invocationCallOrder[0],
232
+ );
233
+
234
+ // The returned session carries the target account (id-normalised) and the
235
+ // slot resolved by /auth/session — NEVER the clobbering slot 0.
236
+ expect(result.sessionId).toBe('sess_switch');
237
+ expect(result.user).toEqual({ id: 'acc1', username: 'oxy-org', name: { displayName: 'Oxy Org' } });
238
+ expect(result.authuser).toBe(1);
239
+
240
+ setTokensSpy.mockRestore();
241
+ clearCacheSpy.mockRestore();
242
+ });
243
+
244
+ it('URL-encodes the accountId path segment', async () => {
245
+ routeByPath();
246
+ await oxy.switchToAccount('a b/c');
247
+ expect(makeRequestSpy.mock.calls[0][1]).toBe('/accounts/a%20b%2Fc/switch');
248
+ });
249
+
250
+ it('keeps the switch active in-session when /auth/session fails (best-effort cookie)', async () => {
251
+ const setTokensSpy = jest.spyOn(oxy, 'setTokens');
252
+ const clearCacheSpy = jest.spyOn(oxy, 'clearCache');
253
+ makeRequestSpy.mockImplementation((_method: string, path: string) =>
254
+ path === '/auth/session'
255
+ ? Promise.reject(Object.assign(new Error('origin'), { response: { status: 403 } }))
256
+ : Promise.resolve(switchResponse),
257
+ );
258
+
259
+ const result = await oxy.switchToAccount('acc1');
260
+
261
+ // The in-session switch survives: the switch token stays planted and the
262
+ // cache is still swept. The switched account just won't survive a reload
263
+ // until the cookie is next established (no authuser resolved).
264
+ expect(setTokensSpy).toHaveBeenCalledWith('access_switch');
265
+ expect(oxy.getAccessToken()).toBe('access_switch');
266
+ expect(clearCacheSpy).toHaveBeenCalledTimes(1);
267
+ expect(result.authuser).toBeUndefined();
268
+
269
+ setTokensSpy.mockRestore();
270
+ clearCacheSpy.mockRestore();
271
+ });
272
+
273
+ it('does NOT plant or sweep when the operator is not authorized (403 surfaces via handleError)', async () => {
274
+ const setTokensSpy = jest.spyOn(oxy, 'setTokens');
275
+ const clearCacheSpy = jest.spyOn(oxy, 'clearCache');
276
+ makeRequestSpy.mockRejectedValue(
277
+ Object.assign(new Error('forbidden'), { response: { status: 403 } }),
278
+ );
279
+
280
+ await expect(oxy.switchToAccount('acc1')).rejects.toThrow();
281
+ // A failed switch must NOT mutate session state, and must NOT attempt the
282
+ // /auth/session establishment.
283
+ expect(setTokensSpy).not.toHaveBeenCalled();
284
+ expect(clearCacheSpy).not.toHaveBeenCalled();
285
+ expect(makeRequestSpy).not.toHaveBeenCalledWith(
286
+ 'POST',
287
+ '/auth/session',
288
+ undefined,
289
+ expect.anything(),
290
+ );
291
+
292
+ setTokensSpy.mockRestore();
293
+ clearCacheSpy.mockRestore();
294
+ });
295
+ });
296
+
177
297
  describe('createAccount', () => {
178
298
  it('posts the payload, unwraps `account`, and busts every list', async () => {
179
299
  makeRequestSpy.mockResolvedValue({ account: accountNodeFixture });
@@ -514,6 +514,25 @@ export interface AssetUpdateVisibilityResponse {
514
514
  };
515
515
  }
516
516
 
517
+ /**
518
+ * Minimal, service-token-scoped asset metadata returned by
519
+ * `POST /assets/service/by-ids`.
520
+ *
521
+ * Resolves an Oxy asset `id` to its content-addressed identity (`sha256`),
522
+ * MIME type, byte `size`, and storage `status`. Used by server-to-server
523
+ * callers (e.g. Mention's MTN Protocol blob-ref resolution) that hold a
524
+ * `files:read`-scoped service token rather than a user session. Unknown or
525
+ * deleted ids are omitted from the response (never error the whole batch),
526
+ * so the result may be shorter than the requested id list.
527
+ */
528
+ export interface ServiceAssetMetadata {
529
+ id: string;
530
+ sha256: string;
531
+ mime: string;
532
+ size: number;
533
+ status: 'active' | 'trash';
534
+ }
535
+
517
536
  /**
518
537
  * Account storage usage (server-side usage, not local AsyncStorage)
519
538
  */
@@ -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
- }
@@ -1,165 +0,0 @@
1
- "use strict";
2
- /**
3
- * Platform Crypto / Storage — Default Variant (Node.js, Browser, generic bundlers)
4
- *
5
- * Provides lazy access to platform-specific crypto and storage modules.
6
- *
7
- * # Variants
8
- *
9
- * This module ships in two physical variants on disk, selected per consumer
10
- * by the bundler / runtime:
11
- *
12
- * - `platformCrypto.js` — this file. Used by Node.js, Vite, webpack,
13
- * Rollup, esbuild, and anything that does
14
- * not match Metro's `*.native.js`
15
- * source-extension preference.
16
- * - `platformCrypto.native.js` — sibling file. Picked up automatically by
17
- * Metro's resolver (which prefers
18
- * `*.<platform>.js` and `*.native.js` over
19
- * plain `*.js` when `preferNativePlatform`
20
- * is true — Expo sets this for all non-web
21
- * builds).
22
- *
23
- * The `package.json#exports` map also declares a `"react-native"` condition
24
- * pointing at the same `dist/esm/index.js` entry — that entry transitively
25
- * imports `./platformCrypto`, and Metro's per-file source-extension lookup
26
- * substitutes the `.native.js` sibling automatically inside `dist/`. This
27
- * means consumers never have to add resolver shims; Metro Just Works.
28
- *
29
- * Both variants expose the EXACT same public API; importers don't need to know
30
- * which one they got. The variant difference is purely about which underlying
31
- * native modules each one references:
32
- *
33
- * ┌──────────────────┬───────────────────────┬───────────────────────────────┐
34
- * │ Function │ Default variant │ React Native variant │
35
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
36
- * │ loadNodeCrypto │ `await import('crypto')` (Node built-in) │
37
- * │ │ │ throws — Node crypto is not │
38
- * │ │ │ available on Hermes/RN │
39
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
40
- * │ loadExpoCrypto │ throws — expo-crypto │ static `import 'expo-crypto'` │
41
- * │ │ is not part of a │ │
42
- * │ │ Node/Vite bundle │ │
43
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
44
- * │ loadSecureStore │ throws (web/Node have │ static `import 'expo-secure-` │
45
- * │ │ their own storage) │ store' │
46
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
47
- * │ loadAsyncStorage │ throws (web/Node have │ static `import '@react- │
48
- * │ │ their own storage) │ native-async-storage/...' │
49
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
50
- * │ getRandomBytesRN │ throws (RN-only) │ direct call into expo-crypto │
51
- * └──────────────────┴───────────────────────┴───────────────────────────────┘
52
- *
53
- * Crucially, the default variant references ONLY Node's `'crypto'`. It never
54
- * mentions `expo-*` or `@react-native-async-storage/*` — so Vite, webpack,
55
- * esbuild, Rollup, and Node itself can bundle / require it without ever
56
- * attempting to resolve those RN-only packages.
57
- *
58
- * The React Native variant references ONLY the RN packages. It never
59
- * mentions `'crypto'` — so Metro and Hermes have nothing to choke on.
60
- *
61
- * # Why not a single file with dynamic import?
62
- *
63
- * A previous iteration used a "bundler-opaque" `new Function('s', 'return
64
- * import(s)')` trick so a single file could service every platform. It
65
- * bundled cleanly on Metro but Hermes refused to PARSE the resulting
66
- * `import()` expression inside a Function-constructor body
67
- * (`SyntaxError: Invalid expression encountered` at the `(` of `import(`).
68
- * The platform-extension split is the only approach that lets each runtime
69
- * see a file containing only specifiers it can understand — no tricks, no
70
- * runtime parsing risks.
71
- */
72
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
73
- if (k2 === undefined) k2 = k;
74
- var desc = Object.getOwnPropertyDescriptor(m, k);
75
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
76
- desc = { enumerable: true, get: function() { return m[k]; } };
77
- }
78
- Object.defineProperty(o, k2, desc);
79
- }) : (function(o, m, k, k2) {
80
- if (k2 === undefined) k2 = k;
81
- o[k2] = m[k];
82
- }));
83
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
84
- Object.defineProperty(o, "default", { enumerable: true, value: v });
85
- }) : function(o, v) {
86
- o["default"] = v;
87
- });
88
- var __importStar = (this && this.__importStar) || (function () {
89
- var ownKeys = function(o) {
90
- ownKeys = Object.getOwnPropertyNames || function (o) {
91
- var ar = [];
92
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
93
- return ar;
94
- };
95
- return ownKeys(o);
96
- };
97
- return function (mod) {
98
- if (mod && mod.__esModule) return mod;
99
- var result = {};
100
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
101
- __setModuleDefault(result, mod);
102
- return result;
103
- };
104
- })();
105
- Object.defineProperty(exports, "__esModule", { value: true });
106
- exports.loadNodeCrypto = loadNodeCrypto;
107
- exports.loadExpoCrypto = loadExpoCrypto;
108
- exports.loadSecureStore = loadSecureStore;
109
- exports.loadAsyncStorage = loadAsyncStorage;
110
- exports.getRandomBytesRN = getRandomBytesRN;
111
- const platform_1 = require("./platform");
112
- // ---------------------------------------------------------------------------
113
- // Node `crypto` — Node built-in
114
- //
115
- // `await import('crypto')` here is a real, static-from-tsc's-perspective
116
- // dynamic import. Node ESM, Vite, webpack, and esbuild all resolve it fine.
117
- // Metro never sees this file because the `.react-native.js` sibling shadows
118
- // it, so Metro never tries to resolve `'crypto'`.
119
- // ---------------------------------------------------------------------------
120
- let cachedNodeCrypto = null;
121
- async function loadNodeCrypto() {
122
- if (cachedNodeCrypto) {
123
- return cachedNodeCrypto;
124
- }
125
- cachedNodeCrypto = await Promise.resolve().then(() => __importStar(require('node:crypto')));
126
- return cachedNodeCrypto;
127
- }
128
- // ---------------------------------------------------------------------------
129
- // RN-only modules — never called from this variant.
130
- //
131
- // These throw a clear error if anything ever reaches them outside RN. In
132
- // practice every caller gates with `isReactNative()` before calling, so
133
- // these are belt-and-braces.
134
- // ---------------------------------------------------------------------------
135
- function notReactNativeError(module) {
136
- return new Error(`[oxy.platformCrypto] Tried to load '${module}' outside React Native. This module is only available in a React Native runtime; bundling routed this consumer to the default (Node/web) variant. This indicates a missing platform gate (\`isReactNative()\`) in the calling code.`);
137
- }
138
- async function loadExpoCrypto() {
139
- if ((0, platform_1.isReactNative)()) {
140
- // Should be unreachable: when running on RN, Metro / the `react-native`
141
- // exports condition serves the sibling variant. If we got here, the
142
- // package-exports map is misconfigured for this host. Throw with a
143
- // helpful diagnostic rather than fall back to a broken dynamic import.
144
- throw new Error('[oxy.platformCrypto] React Native runtime resolved the default ' +
145
- '(non-RN) variant of @oxyhq/core/utils/platformCrypto. Check the ' +
146
- "consumer's bundler resolution — Metro should pick the sibling " +
147
- '.react-native.js file via package exports.');
148
- }
149
- throw notReactNativeError('expo-crypto');
150
- }
151
- async function loadSecureStore() {
152
- throw notReactNativeError('expo-secure-store');
153
- }
154
- async function loadAsyncStorage() {
155
- throw notReactNativeError('@react-native-async-storage/async-storage');
156
- }
157
- /**
158
- * Synchronous random-bytes via `expo-crypto.getRandomBytes`. Only available
159
- * in the React Native variant. The default variant throws because Node and
160
- * browsers have their own native CSPRNGs (`crypto.randomBytes` and
161
- * `crypto.getRandomValues` respectively) — callers should use those.
162
- */
163
- function getRandomBytesRN(_byteCount) {
164
- throw notReactNativeError('expo-crypto.getRandomBytes (sync)');
165
- }