@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
@@ -1,87 +0,0 @@
1
- /**
2
- * Platform Crypto / Storage — Default Variant (Node.js, Browser, generic bundlers)
3
- *
4
- * Provides lazy access to platform-specific crypto and storage modules.
5
- *
6
- * # Variants
7
- *
8
- * This module ships in two physical variants on disk, selected per consumer
9
- * by the bundler / runtime:
10
- *
11
- * - `platformCrypto.js` — this file. Used by Node.js, Vite, webpack,
12
- * Rollup, esbuild, and anything that does
13
- * not match Metro's `*.native.js`
14
- * source-extension preference.
15
- * - `platformCrypto.native.js` — sibling file. Picked up automatically by
16
- * Metro's resolver (which prefers
17
- * `*.<platform>.js` and `*.native.js` over
18
- * plain `*.js` when `preferNativePlatform`
19
- * is true — Expo sets this for all non-web
20
- * builds).
21
- *
22
- * The `package.json#exports` map also declares a `"react-native"` condition
23
- * pointing at the same `dist/esm/index.js` entry — that entry transitively
24
- * imports `./platformCrypto`, and Metro's per-file source-extension lookup
25
- * substitutes the `.native.js` sibling automatically inside `dist/`. This
26
- * means consumers never have to add resolver shims; Metro Just Works.
27
- *
28
- * Both variants expose the EXACT same public API; importers don't need to know
29
- * which one they got. The variant difference is purely about which underlying
30
- * native modules each one references:
31
- *
32
- * ┌──────────────────┬───────────────────────┬───────────────────────────────┐
33
- * │ Function │ Default variant │ React Native variant │
34
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
35
- * │ loadNodeCrypto │ `await import('crypto')` (Node built-in) │
36
- * │ │ │ throws — Node crypto is not │
37
- * │ │ │ available on Hermes/RN │
38
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
39
- * │ loadExpoCrypto │ throws — expo-crypto │ static `import 'expo-crypto'` │
40
- * │ │ is not part of a │ │
41
- * │ │ Node/Vite bundle │ │
42
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
43
- * │ loadSecureStore │ throws (web/Node have │ static `import 'expo-secure-` │
44
- * │ │ their own storage) │ store' │
45
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
46
- * │ loadAsyncStorage │ throws (web/Node have │ static `import '@react- │
47
- * │ │ their own storage) │ native-async-storage/...' │
48
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
49
- * │ getRandomBytesRN │ throws (RN-only) │ direct call into expo-crypto │
50
- * └──────────────────┴───────────────────────┴───────────────────────────────┘
51
- *
52
- * Crucially, the default variant references ONLY Node's `'crypto'`. It never
53
- * mentions `expo-*` or `@react-native-async-storage/*` — so Vite, webpack,
54
- * esbuild, Rollup, and Node itself can bundle / require it without ever
55
- * attempting to resolve those RN-only packages.
56
- *
57
- * The React Native variant references ONLY the RN packages. It never
58
- * mentions `'crypto'` — so Metro and Hermes have nothing to choke on.
59
- *
60
- * # Why not a single file with dynamic import?
61
- *
62
- * A previous iteration used a "bundler-opaque" `new Function('s', 'return
63
- * import(s)')` trick so a single file could service every platform. It
64
- * bundled cleanly on Metro but Hermes refused to PARSE the resulting
65
- * `import()` expression inside a Function-constructor body
66
- * (`SyntaxError: Invalid expression encountered` at the `(` of `import(`).
67
- * The platform-extension split is the only approach that lets each runtime
68
- * see a file containing only specifiers it can understand — no tricks, no
69
- * runtime parsing risks.
70
- */
71
- export declare function loadNodeCrypto(): Promise<typeof import('crypto')>;
72
- export declare function loadExpoCrypto(): Promise<typeof import('expo-crypto')>;
73
- export declare function loadSecureStore(): Promise<typeof import('expo-secure-store')>;
74
- export declare function loadAsyncStorage(): Promise<{
75
- default: {
76
- getItem: (key: string) => Promise<string | null>;
77
- setItem: (key: string, value: string) => Promise<void>;
78
- removeItem: (key: string) => Promise<void>;
79
- };
80
- }>;
81
- /**
82
- * Synchronous random-bytes via `expo-crypto.getRandomBytes`. Only available
83
- * in the React Native variant. The default variant throws because Node and
84
- * browsers have their own native CSPRNGs (`crypto.randomBytes` and
85
- * `crypto.getRandomValues` respectively) — callers should use those.
86
- */
87
- export declare function getRandomBytesRN(_byteCount: number): Uint8Array;
@@ -1,54 +0,0 @@
1
- /**
2
- * Platform Crypto / Storage — React Native Variant
3
- *
4
- * Companion to `./platformCrypto.ts`. See the doc-comment at the top of that
5
- * file for the full design.
6
- *
7
- * Metro auto-selects this file in any non-web build (`preferNativePlatform`
8
- * is `true` for iOS / Android, so `*.native.js` shadows `*.js` during
9
- * source-extension resolution inside `node_modules/@oxyhq/core/dist/`). On
10
- * iOS / Android `<base>.ios.js` / `<base>.android.js` would shadow this file
11
- * if they existed, but they don't — `.native.js` is the shared RN variant.
12
- *
13
- * - The default variant references Node's `'crypto'` and would crash Metro
14
- * if bundled into an RN app.
15
- * - This variant references the RN-only modules (`expo-crypto`,
16
- * `expo-secure-store`, `@react-native-async-storage/async-storage`)
17
- * as static imports, so Metro and Hermes both resolve and parse them
18
- * cleanly.
19
- *
20
- * Both variants expose the same surface; importers don't care which one
21
- * they got.
22
- *
23
- * # Why static imports?
24
- *
25
- * Every RN consumer of `@oxyhq/core` already lists or transitively pulls
26
- * in `expo-crypto`, `expo-secure-store`, and
27
- * `@react-native-async-storage/async-storage` (they're stable Expo modules
28
- * present in `services`, `accounts`, `inbox`, and `test-app`). A static
29
- * import is what Metro wants to see anyway, and Hermes parses it like any
30
- * other ES module — no `Function`-constructor parser exotic-mode involved.
31
- *
32
- * This is also clearer to debug: Metro fails up-front with a normal
33
- * unresolved-module error if a consumer is missing a peer dep, instead of
34
- * a confusing runtime throw the first time a code path that needs the
35
- * module is exercised.
36
- */
37
- export declare function loadNodeCrypto(): Promise<typeof import('crypto')>;
38
- export declare function loadExpoCrypto(): Promise<typeof import('expo-crypto')>;
39
- export declare function loadSecureStore(): Promise<typeof import('expo-secure-store')>;
40
- type AsyncStorageLike = {
41
- getItem: (key: string) => Promise<string | null>;
42
- setItem: (key: string, value: string) => Promise<void>;
43
- removeItem: (key: string) => Promise<void>;
44
- };
45
- export declare function loadAsyncStorage(): Promise<{
46
- default: AsyncStorageLike;
47
- }>;
48
- /**
49
- * Synchronous random-bytes via `expo-crypto.getRandomBytes`. Available
50
- * synchronously because `expo-crypto` is statically imported by this file
51
- * — no async initialization race.
52
- */
53
- export declare function getRandomBytesRN(byteCount: number): Uint8Array;
54
- export {};
@@ -1,116 +0,0 @@
1
- /**
2
- * Canonical JSON tests.
3
- *
4
- * The whole point of `canonicalize` is that two structurally-equal values
5
- * produce identical strings regardless of how their keys were ordered, so a
6
- * client which signs and a server which verifies agree on the signing input.
7
- * These tests pin that determinism, the array-order guarantee, the nesting
8
- * behaviour, and the JSON value/omit semantics.
9
- */
10
-
11
- import { canonicalize } from '../canonicalJson';
12
-
13
- describe('canonicalize', () => {
14
- describe('object key ordering', () => {
15
- it('produces identical output regardless of insertion order', () => {
16
- const a = canonicalize({ b: 1, a: 2, c: 3 });
17
- const b = canonicalize({ c: 3, a: 2, b: 1 });
18
- const c = canonicalize({ a: 2, b: 1, c: 3 });
19
- expect(a).toBe(b);
20
- expect(b).toBe(c);
21
- expect(a).toBe('{"a":2,"b":1,"c":3}');
22
- });
23
-
24
- it('sorts keys recursively at every level', () => {
25
- const value = {
26
- z: { y: 1, x: 2 },
27
- a: { c: 3, b: { e: 5, d: 4 } },
28
- };
29
- expect(canonicalize(value)).toBe(
30
- '{"a":{"b":{"d":4,"e":5},"c":3},"z":{"x":2,"y":1}}',
31
- );
32
- });
33
-
34
- it('is order-insensitive across deep nesting', () => {
35
- const first = canonicalize({
36
- outer: { inner: { p: 1, q: 2 }, lead: 'x' },
37
- meta: { issuedAt: 10, version: 1 },
38
- });
39
- const second = canonicalize({
40
- meta: { version: 1, issuedAt: 10 },
41
- outer: { lead: 'x', inner: { q: 2, p: 1 } },
42
- });
43
- expect(first).toBe(second);
44
- });
45
- });
46
-
47
- describe('array ordering', () => {
48
- it('preserves array element order (never sorts arrays)', () => {
49
- expect(canonicalize([3, 1, 2])).toBe('[3,1,2]');
50
- expect(canonicalize(['b', 'a', 'c'])).toBe('["b","a","c"]');
51
- });
52
-
53
- it('distinguishes arrays that differ only in order', () => {
54
- expect(canonicalize([1, 2])).not.toBe(canonicalize([2, 1]));
55
- });
56
-
57
- it('canonicalizes objects inside arrays without reordering the array', () => {
58
- const value = [
59
- { b: 1, a: 2 },
60
- { d: 3, c: 4 },
61
- ];
62
- expect(canonicalize(value)).toBe('[{"a":2,"b":1},{"c":4,"d":3}]');
63
- });
64
- });
65
-
66
- describe('primitives', () => {
67
- it('serializes null, booleans, strings and numbers as JSON', () => {
68
- expect(canonicalize(null)).toBe('null');
69
- expect(canonicalize(true)).toBe('true');
70
- expect(canonicalize(false)).toBe('false');
71
- expect(canonicalize('hi')).toBe('"hi"');
72
- expect(canonicalize(42)).toBe('42');
73
- expect(canonicalize(-1.5)).toBe('-1.5');
74
- expect(canonicalize(0)).toBe('0');
75
- });
76
-
77
- it('escapes strings the same way JSON does', () => {
78
- expect(canonicalize('a"b\\c\n')).toBe(JSON.stringify('a"b\\c\n'));
79
- });
80
- });
81
-
82
- describe('JSON value/omit semantics', () => {
83
- it('omits object properties whose value is undefined', () => {
84
- expect(canonicalize({ a: 1, b: undefined, c: 3 })).toBe('{"a":1,"c":3}');
85
- });
86
-
87
- it('renders undefined array elements as null (preserving length)', () => {
88
- expect(canonicalize([1, undefined, 3])).toBe('[1,null,3]');
89
- });
90
-
91
- it('respects toJSON (Date and its ISO string canonicalize identically)', () => {
92
- const date = new Date('2026-06-26T00:00:00.000Z');
93
- expect(canonicalize(date)).toBe(JSON.stringify(date.toISOString()));
94
- expect(canonicalize({ at: date })).toBe(
95
- canonicalize({ at: '2026-06-26T00:00:00.000Z' }),
96
- );
97
- });
98
- });
99
-
100
- describe('rejects values outside the JSON data model', () => {
101
- it('throws on non-finite numbers', () => {
102
- expect(() => canonicalize(NaN)).toThrow();
103
- expect(() => canonicalize(Infinity)).toThrow();
104
- expect(() => canonicalize({ x: Infinity })).toThrow();
105
- });
106
-
107
- it('throws on bigint', () => {
108
- expect(() => canonicalize(BigInt(1))).toThrow();
109
- });
110
-
111
- it('throws on a bare undefined / function at the top level', () => {
112
- expect(() => canonicalize(undefined)).toThrow();
113
- expect(() => canonicalize(() => 1)).toThrow();
114
- });
115
- });
116
- });
@@ -1,120 +0,0 @@
1
- /**
2
- * Canonical JSON (RFC 8785 / JCS-style) serialization.
3
- *
4
- * `canonicalize(value)` produces a deterministic string for any JSON-compatible
5
- * value so that a client which SIGNS a record and a server which VERIFIES it
6
- * agree byte-for-byte on the signing input — regardless of the order in which
7
- * object keys happen to be written, how the value was deserialized, or which
8
- * runtime built it.
9
- *
10
- * This is the load-bearing primitive for the self-sovereign identity layer's
11
- * signed records (`SignatureService.signRecord` + the API's record-verify path):
12
- * both sides import THIS function from `@oxyhq/core`, so cross-implementation
13
- * number/string formatting differences cannot cause a verify mismatch.
14
- *
15
- * Rules (the JSON Canonicalization Scheme subset we need):
16
- * - Objects: keys are sorted (ascending, by UTF-16 code unit — the default
17
- * `Array.prototype.sort` order) and serialized recursively. Properties whose
18
- * value is `undefined`, a function, or a symbol are OMITTED (matching
19
- * `JSON.stringify` object semantics).
20
- * - Arrays: element order is PRESERVED; `undefined`/function/symbol elements
21
- * serialize to `null` (matching `JSON.stringify` array semantics).
22
- * - `null`, booleans, strings, and finite numbers serialize via the standard
23
- * JSON representation.
24
- * - Values exposing a `toJSON()` method (e.g. `Date`) are replaced by its
25
- * result first, then serialized — so a `Date` and its ISO-string equivalent
26
- * canonicalize identically (the wire always carries the string form).
27
- * - Non-finite numbers (`NaN`, `Infinity`) and `bigint` are not part of the
28
- * JSON data model and throw, rather than silently producing `null`.
29
- *
30
- * Platform-agnostic — zero dependencies, no `require()`, no react/react-native/
31
- * expo. Safe in the dual CJS + ESM build.
32
- */
33
-
34
- /** Object exposing a `toJSON()` serialization hook (e.g. `Date`). */
35
- interface ToJsonable {
36
- toJSON: () => unknown;
37
- }
38
-
39
- function hasToJSON(value: object): value is ToJsonable {
40
- return typeof (value as { toJSON?: unknown }).toJSON === 'function';
41
- }
42
-
43
- /**
44
- * Serialize a single value into its canonical JSON fragment. Recursive; called
45
- * on each nested member. Object keys are sorted at every level.
46
- */
47
- function serialize(value: unknown): string {
48
- if (value === null) {
49
- return 'null';
50
- }
51
-
52
- const valueType = typeof value;
53
-
54
- if (valueType === 'number') {
55
- if (!Number.isFinite(value)) {
56
- throw new Error('canonicalize: non-finite numbers cannot be serialized');
57
- }
58
- return JSON.stringify(value);
59
- }
60
-
61
- if (valueType === 'string' || valueType === 'boolean') {
62
- return JSON.stringify(value);
63
- }
64
-
65
- if (valueType === 'bigint') {
66
- throw new Error('canonicalize: bigint values cannot be serialized');
67
- }
68
-
69
- if (Array.isArray(value)) {
70
- const items = value.map((item) => {
71
- const itemType = typeof item;
72
- // JSON array semantics: undefined / function / symbol become null so the
73
- // element positions (and therefore the array length) are preserved.
74
- if (item === undefined || itemType === 'function' || itemType === 'symbol') {
75
- return 'null';
76
- }
77
- return serialize(item);
78
- });
79
- return `[${items.join(',')}]`;
80
- }
81
-
82
- if (valueType === 'object') {
83
- const obj = value as object;
84
- if (hasToJSON(obj)) {
85
- return serialize(obj.toJSON());
86
- }
87
-
88
- const record = obj as Record<string, unknown>;
89
- const parts: string[] = [];
90
- for (const key of Object.keys(record).sort()) {
91
- const member = record[key];
92
- const memberType = typeof member;
93
- // JSON object semantics: properties with undefined / function / symbol
94
- // values are omitted entirely.
95
- if (member === undefined || memberType === 'function' || memberType === 'symbol') {
96
- continue;
97
- }
98
- parts.push(`${JSON.stringify(key)}:${serialize(member)}`);
99
- }
100
- return `{${parts.join(',')}}`;
101
- }
102
-
103
- // undefined / function / symbol at the top level have no JSON representation.
104
- throw new Error(`canonicalize: cannot serialize a value of type ${valueType}`);
105
- }
106
-
107
- /**
108
- * Produce the canonical JSON string for `value`.
109
- *
110
- * Deterministic: two structurally-equal values yield identical strings even if
111
- * their object keys were written in different orders. Use this — never an
112
- * ad-hoc `JSON.stringify` of a hand-sorted object — as the signing input for
113
- * signed records, so client signing and server verification cannot drift.
114
- *
115
- * @throws if `value` (or any nested member used as the top-level/primitive)
116
- * contains a non-finite number or a `bigint`, which have no JSON form.
117
- */
118
- export function canonicalize(value: unknown): string {
119
- return serialize(value);
120
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * Platform Crypto / Storage — React Native Variant
3
- *
4
- * Companion to `./platformCrypto.ts`. See the doc-comment at the top of that
5
- * file for the full design.
6
- *
7
- * Metro auto-selects this file in any non-web build (`preferNativePlatform`
8
- * is `true` for iOS / Android, so `*.native.js` shadows `*.js` during
9
- * source-extension resolution inside `node_modules/@oxyhq/core/dist/`). On
10
- * iOS / Android `<base>.ios.js` / `<base>.android.js` would shadow this file
11
- * if they existed, but they don't — `.native.js` is the shared RN variant.
12
- *
13
- * - The default variant references Node's `'crypto'` and would crash Metro
14
- * if bundled into an RN app.
15
- * - This variant references the RN-only modules (`expo-crypto`,
16
- * `expo-secure-store`, `@react-native-async-storage/async-storage`)
17
- * as static imports, so Metro and Hermes both resolve and parse them
18
- * cleanly.
19
- *
20
- * Both variants expose the same surface; importers don't care which one
21
- * they got.
22
- *
23
- * # Why static imports?
24
- *
25
- * Every RN consumer of `@oxyhq/core` already lists or transitively pulls
26
- * in `expo-crypto`, `expo-secure-store`, and
27
- * `@react-native-async-storage/async-storage` (they're stable Expo modules
28
- * present in `services`, `accounts`, `inbox`, and `test-app`). A static
29
- * import is what Metro wants to see anyway, and Hermes parses it like any
30
- * other ES module — no `Function`-constructor parser exotic-mode involved.
31
- *
32
- * This is also clearer to debug: Metro fails up-front with a normal
33
- * unresolved-module error if a consumer is missing a peer dep, instead of
34
- * a confusing runtime throw the first time a code path that needs the
35
- * module is exercised.
36
- */
37
-
38
- import * as ExpoCrypto from 'expo-crypto';
39
- import * as SecureStore from 'expo-secure-store';
40
- import AsyncStorage from '@react-native-async-storage/async-storage';
41
-
42
- // ---------------------------------------------------------------------------
43
- // Node `crypto` — never available in RN.
44
- // ---------------------------------------------------------------------------
45
-
46
- export async function loadNodeCrypto(): Promise<typeof import('crypto')> {
47
- // Unreachable in practice: every caller gates with `isNodeJS()` before
48
- // invoking this. If it somehow does fire, throw immediately with a clear
49
- // diagnostic rather than letting Metro / Hermes attempt to find a
50
- // non-existent module at runtime.
51
- throw new Error(
52
- "[oxy.platformCrypto] Node's built-in 'crypto' module is not available " +
53
- 'in a React Native runtime. Use the RN-specific helpers ' +
54
- '(loadExpoCrypto, getRandomBytesRN) or the Web Crypto API (`globalThis.crypto`).',
55
- );
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // expo-crypto — RN cryptographic primitives.
60
- // ---------------------------------------------------------------------------
61
-
62
- export async function loadExpoCrypto(): Promise<typeof import('expo-crypto')> {
63
- return ExpoCrypto;
64
- }
65
-
66
- // ---------------------------------------------------------------------------
67
- // expo-secure-store — RN keychain / keystore.
68
- // ---------------------------------------------------------------------------
69
-
70
- export async function loadSecureStore(): Promise<typeof import('expo-secure-store')> {
71
- return SecureStore;
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // @react-native-async-storage/async-storage — RN persistent KV storage.
76
- // ---------------------------------------------------------------------------
77
-
78
- type AsyncStorageLike = {
79
- getItem: (key: string) => Promise<string | null>;
80
- setItem: (key: string, value: string) => Promise<void>;
81
- removeItem: (key: string) => Promise<void>;
82
- };
83
-
84
- export async function loadAsyncStorage(): Promise<{ default: AsyncStorageLike }> {
85
- // Mirror the shape callers historically used (`module.default.<method>`)
86
- // so the call sites don't have to know whether the underlying module
87
- // ships ESM or CJS-with-default.
88
- const storage = AsyncStorage as unknown as AsyncStorageLike;
89
- return {
90
- default: storage,
91
- };
92
- }
93
-
94
- /**
95
- * Synchronous random-bytes via `expo-crypto.getRandomBytes`. Available
96
- * synchronously because `expo-crypto` is statically imported by this file
97
- * — no async initialization race.
98
- */
99
- export function getRandomBytesRN(byteCount: number): Uint8Array {
100
- return ExpoCrypto.getRandomBytes(byteCount);
101
- }
@@ -1,145 +0,0 @@
1
- /**
2
- * Platform Crypto / Storage — Default Variant (Node.js, Browser, generic bundlers)
3
- *
4
- * Provides lazy access to platform-specific crypto and storage modules.
5
- *
6
- * # Variants
7
- *
8
- * This module ships in two physical variants on disk, selected per consumer
9
- * by the bundler / runtime:
10
- *
11
- * - `platformCrypto.js` — this file. Used by Node.js, Vite, webpack,
12
- * Rollup, esbuild, and anything that does
13
- * not match Metro's `*.native.js`
14
- * source-extension preference.
15
- * - `platformCrypto.native.js` — sibling file. Picked up automatically by
16
- * Metro's resolver (which prefers
17
- * `*.<platform>.js` and `*.native.js` over
18
- * plain `*.js` when `preferNativePlatform`
19
- * is true — Expo sets this for all non-web
20
- * builds).
21
- *
22
- * The `package.json#exports` map also declares a `"react-native"` condition
23
- * pointing at the same `dist/esm/index.js` entry — that entry transitively
24
- * imports `./platformCrypto`, and Metro's per-file source-extension lookup
25
- * substitutes the `.native.js` sibling automatically inside `dist/`. This
26
- * means consumers never have to add resolver shims; Metro Just Works.
27
- *
28
- * Both variants expose the EXACT same public API; importers don't need to know
29
- * which one they got. The variant difference is purely about which underlying
30
- * native modules each one references:
31
- *
32
- * ┌──────────────────┬───────────────────────┬───────────────────────────────┐
33
- * │ Function │ Default variant │ React Native variant │
34
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
35
- * │ loadNodeCrypto │ `await import('crypto')` (Node built-in) │
36
- * │ │ │ throws — Node crypto is not │
37
- * │ │ │ available on Hermes/RN │
38
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
39
- * │ loadExpoCrypto │ throws — expo-crypto │ static `import 'expo-crypto'` │
40
- * │ │ is not part of a │ │
41
- * │ │ Node/Vite bundle │ │
42
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
43
- * │ loadSecureStore │ throws (web/Node have │ static `import 'expo-secure-` │
44
- * │ │ their own storage) │ store' │
45
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
46
- * │ loadAsyncStorage │ throws (web/Node have │ static `import '@react- │
47
- * │ │ their own storage) │ native-async-storage/...' │
48
- * ├──────────────────┼───────────────────────┼───────────────────────────────┤
49
- * │ getRandomBytesRN │ throws (RN-only) │ direct call into expo-crypto │
50
- * └──────────────────┴───────────────────────┴───────────────────────────────┘
51
- *
52
- * Crucially, the default variant references ONLY Node's `'crypto'`. It never
53
- * mentions `expo-*` or `@react-native-async-storage/*` — so Vite, webpack,
54
- * esbuild, Rollup, and Node itself can bundle / require it without ever
55
- * attempting to resolve those RN-only packages.
56
- *
57
- * The React Native variant references ONLY the RN packages. It never
58
- * mentions `'crypto'` — so Metro and Hermes have nothing to choke on.
59
- *
60
- * # Why not a single file with dynamic import?
61
- *
62
- * A previous iteration used a "bundler-opaque" `new Function('s', 'return
63
- * import(s)')` trick so a single file could service every platform. It
64
- * bundled cleanly on Metro but Hermes refused to PARSE the resulting
65
- * `import()` expression inside a Function-constructor body
66
- * (`SyntaxError: Invalid expression encountered` at the `(` of `import(`).
67
- * The platform-extension split is the only approach that lets each runtime
68
- * see a file containing only specifiers it can understand — no tricks, no
69
- * runtime parsing risks.
70
- */
71
-
72
- import { isReactNative } from './platform';
73
-
74
- // ---------------------------------------------------------------------------
75
- // Node `crypto` — Node built-in
76
- //
77
- // `await import('crypto')` here is a real, static-from-tsc's-perspective
78
- // dynamic import. Node ESM, Vite, webpack, and esbuild all resolve it fine.
79
- // Metro never sees this file because the `.react-native.js` sibling shadows
80
- // it, so Metro never tries to resolve `'crypto'`.
81
- // ---------------------------------------------------------------------------
82
-
83
- let cachedNodeCrypto: typeof import('crypto') | null = null;
84
-
85
- export async function loadNodeCrypto(): Promise<typeof import('crypto')> {
86
- if (cachedNodeCrypto) {
87
- return cachedNodeCrypto;
88
- }
89
- cachedNodeCrypto = await import('node:crypto');
90
- return cachedNodeCrypto;
91
- }
92
-
93
- // ---------------------------------------------------------------------------
94
- // RN-only modules — never called from this variant.
95
- //
96
- // These throw a clear error if anything ever reaches them outside RN. In
97
- // practice every caller gates with `isReactNative()` before calling, so
98
- // these are belt-and-braces.
99
- // ---------------------------------------------------------------------------
100
-
101
- function notReactNativeError(module: string): Error {
102
- return new Error(
103
- `[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.`,
104
- );
105
- }
106
-
107
- export async function loadExpoCrypto(): Promise<typeof import('expo-crypto')> {
108
- if (isReactNative()) {
109
- // Should be unreachable: when running on RN, Metro / the `react-native`
110
- // exports condition serves the sibling variant. If we got here, the
111
- // package-exports map is misconfigured for this host. Throw with a
112
- // helpful diagnostic rather than fall back to a broken dynamic import.
113
- throw new Error(
114
- '[oxy.platformCrypto] React Native runtime resolved the default ' +
115
- '(non-RN) variant of @oxyhq/core/utils/platformCrypto. Check the ' +
116
- "consumer's bundler resolution — Metro should pick the sibling " +
117
- '.react-native.js file via package exports.',
118
- );
119
- }
120
- throw notReactNativeError('expo-crypto');
121
- }
122
-
123
- export async function loadSecureStore(): Promise<typeof import('expo-secure-store')> {
124
- throw notReactNativeError('expo-secure-store');
125
- }
126
-
127
- export async function loadAsyncStorage(): Promise<{
128
- default: {
129
- getItem: (key: string) => Promise<string | null>;
130
- setItem: (key: string, value: string) => Promise<void>;
131
- removeItem: (key: string) => Promise<void>;
132
- };
133
- }> {
134
- throw notReactNativeError('@react-native-async-storage/async-storage');
135
- }
136
-
137
- /**
138
- * Synchronous random-bytes via `expo-crypto.getRandomBytes`. Only available
139
- * in the React Native variant. The default variant throws because Node and
140
- * browsers have their own native CSPRNGs (`crypto.randomBytes` and
141
- * `crypto.getRandomValues` respectively) — callers should use those.
142
- */
143
- export function getRandomBytesRN(_byteCount: number): Uint8Array {
144
- throw notReactNativeError('expo-crypto.getRandomBytes (sync)');
145
- }