@oxy.so/core 1.8.0 → 1.8.1
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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/crypto/polyfill.js +21 -6
- package/dist/cjs/index.js +4 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/polyfill.js +19 -4
- package/dist/esm/index.js +4 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/polyfill.d.ts +17 -2
- package/package.json +2 -2
- package/src/crypto/__tests__/polyfill.test.ts +1 -1
- package/src/crypto/__tests__/polyfillOrder.test.ts +114 -0
- package/src/crypto/polyfill.ts +19 -4
- package/src/index.ts +4 -1
|
@@ -13,8 +13,23 @@
|
|
|
13
13
|
* 2. Node — backed by the built-in `node:crypto` module (`webcrypto`, else
|
|
14
14
|
* `randomFillSync`). This is what a Node runtime WITHOUT a global WebCrypto
|
|
15
15
|
* (Node 18 script entrypoints, some embedded hosts) falls back to.
|
|
16
|
-
* 3. React Native — `expo-crypto.getRandomBytes
|
|
17
|
-
*
|
|
16
|
+
* 3. React Native — `expo-crypto.getRandomBytes`, through the dependency-free
|
|
17
|
+
* `@oxy.so/protocol/random` entry.
|
|
18
|
+
*
|
|
19
|
+
* # Evaluation order — this module must import NOTHING that can capture the global
|
|
20
|
+
*
|
|
21
|
+
* `@noble/hashes` 1.x reads `globalThis.crypto` exactly once, when its
|
|
22
|
+
* `crypto.js` is evaluated, and `randomBytes` uses that captured binding for
|
|
23
|
+
* the lifetime of the app. ES imports are evaluated before the importing
|
|
24
|
+
* module's body, so every module this file imports runs BEFORE the shim below
|
|
25
|
+
* is installed. This file used to import `@oxy.so/protocol`'s root entry,
|
|
26
|
+
* which reaches `@noble/curves` → `@noble/hashes` through the envelope signer:
|
|
27
|
+
* on Hermes (no `globalThis.crypto` at startup) noble captured `undefined`
|
|
28
|
+
* before the shim existed, and every identity creation on Android failed with
|
|
29
|
+
* `crypto.getRandomValues must be defined`. Its imports are therefore limited
|
|
30
|
+
* to `buffer` and `@oxy.so/protocol/random`, both free of crypto libraries,
|
|
31
|
+
* and `index.ts` imports this file first. `__tests__/polyfillOrder.test.ts`
|
|
32
|
+
* guards both halves.
|
|
18
33
|
*
|
|
19
34
|
* Historically step (2) delegated to `@oxy.so/protocol`'s RN-only
|
|
20
35
|
* `getRandomBytesRN`, which THROWS on Node — so any Node host lacking a global
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/core",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"@noble/ciphers": "^1.3.0",
|
|
120
120
|
"@noble/hashes": "^1.8.0",
|
|
121
121
|
"@oxy.so/contracts": "^1.4.0",
|
|
122
|
-
"@oxy.so/protocol": "^1.1.
|
|
122
|
+
"@oxy.so/protocol": "^1.1.1",
|
|
123
123
|
"@scure/bip39": "^1.6.0",
|
|
124
124
|
"buffer": "^6.0.3",
|
|
125
125
|
"express-rate-limit": "^8.6.0",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
const mockGetRandomBytesRN = jest.fn<Uint8Array, [number]>();
|
|
14
14
|
const mockState = { isNodeJS: true };
|
|
15
15
|
|
|
16
|
-
jest.mock('@oxy.so/protocol', () => ({
|
|
16
|
+
jest.mock('@oxy.so/protocol/random', () => ({
|
|
17
17
|
isNodeJS: () => mockState.isNodeJS,
|
|
18
18
|
getRandomBytesRN: (byteCount: number) => mockGetRandomBytesRN(byteCount),
|
|
19
19
|
}));
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The crypto polyfill must be installed before ANY `@noble/*` module evaluates.
|
|
3
|
+
*
|
|
4
|
+
* `@noble/hashes` 1.x's `crypto.js` captures `globalThis.crypto` once, at module
|
|
5
|
+
* evaluation, and `randomBytes` uses that captured binding forever. Hermes has
|
|
6
|
+
* no `globalThis.crypto` at startup, so if noble is evaluated before
|
|
7
|
+
* `./polyfill` installs the shim, every random draw throws
|
|
8
|
+
* `crypto.getRandomValues must be defined` — which is exactly how identity
|
|
9
|
+
* creation broke on Android: the polyfill imported `@oxy.so/protocol`'s root
|
|
10
|
+
* entry, whose envelope signer reaches `@noble/curves` → `@noble/hashes`, and
|
|
11
|
+
* imports are evaluated before the polyfill body.
|
|
12
|
+
*
|
|
13
|
+
* Node's resolver picks noble's `cryptoNode.js` (the `node` export condition),
|
|
14
|
+
* which never captures the global, so a plain Node run cannot see the bug. The
|
|
15
|
+
* suites below substitute the REAL `crypto.js` that Metro resolves for React
|
|
16
|
+
* Native, remove `globalThis.crypto`, and load core in a fresh module registry.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
|
|
22
|
+
type RandomBytes = (length?: number) => Uint8Array;
|
|
23
|
+
|
|
24
|
+
const ORIGINAL_CRYPTO_DESCRIPTOR = Object.getOwnPropertyDescriptor(globalThis, 'crypto');
|
|
25
|
+
|
|
26
|
+
/** noble's browser / React Native `crypto.js`: the variant that captures the global. */
|
|
27
|
+
const NOBLE_CAPTURING_CRYPTO = join(dirname(require.resolve('@noble/hashes/utils')), 'crypto.js');
|
|
28
|
+
|
|
29
|
+
/** Remove `globalThis.crypto` the way a Hermes host starts: no property at all. */
|
|
30
|
+
function removeHostCrypto(): void {
|
|
31
|
+
if (!Reflect.deleteProperty(globalThis, 'crypto') || 'crypto' in globalThis) {
|
|
32
|
+
Object.defineProperty(globalThis, 'crypto', {
|
|
33
|
+
value: undefined,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function restoreHostCrypto(): void {
|
|
41
|
+
Reflect.deleteProperty(globalThis, 'crypto');
|
|
42
|
+
if (ORIGINAL_CRYPTO_DESCRIPTOR) {
|
|
43
|
+
Object.defineProperty(globalThis, 'crypto', ORIGINAL_CRYPTO_DESCRIPTOR);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run `body` in a fresh module registry that resolves `@noble/hashes/crypto` as
|
|
49
|
+
* a React Native bundle does, with no host `globalThis.crypto`.
|
|
50
|
+
*/
|
|
51
|
+
function withReactNativeNobleAndNoHostCrypto(body: () => void): void {
|
|
52
|
+
removeHostCrypto();
|
|
53
|
+
try {
|
|
54
|
+
jest.isolateModules(() => {
|
|
55
|
+
jest.doMock('@noble/hashes/crypto', () => jest.requireActual(NOBLE_CAPTURING_CRYPTO));
|
|
56
|
+
body();
|
|
57
|
+
});
|
|
58
|
+
} finally {
|
|
59
|
+
restoreHostCrypto();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('crypto polyfill evaluation order', () => {
|
|
64
|
+
it('reproduces the capture: noble evaluated before the polyfill has no randomness', () => {
|
|
65
|
+
// Guards the harness itself — if this ever stops throwing, the substitution
|
|
66
|
+
// no longer models the capturing variant and the suites below prove nothing.
|
|
67
|
+
withReactNativeNobleAndNoHostCrypto(() => {
|
|
68
|
+
const { randomBytes } = require('@noble/hashes/utils') as { randomBytes: RandomBytes };
|
|
69
|
+
require('../polyfill');
|
|
70
|
+
expect(() => randomBytes(32)).toThrow('crypto.getRandomValues must be defined');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('importing @oxy.so/core installs the shim before noble captures the global', () => {
|
|
75
|
+
withReactNativeNobleAndNoHostCrypto(() => {
|
|
76
|
+
require('../../index');
|
|
77
|
+
const { randomBytes } = require('@noble/hashes/utils') as { randomBytes: RandomBytes };
|
|
78
|
+
|
|
79
|
+
const bytes = randomBytes(32);
|
|
80
|
+
expect(bytes).toHaveLength(32);
|
|
81
|
+
expect(bytes.some((byte) => byte !== 0)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('generates an identity (key pair + recovery mnemonic) through core', () => {
|
|
86
|
+
withReactNativeNobleAndNoHostCrypto(() => {
|
|
87
|
+
const core = require('../../index') as typeof import('../../index');
|
|
88
|
+
const { generateMnemonic } = require('@scure/bip39') as typeof import('@scure/bip39');
|
|
89
|
+
const { wordlist } = require('@scure/bip39/wordlists/english') as {
|
|
90
|
+
wordlist: string[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// The two random draws `RecoveryPhraseService.generateIdentityWithRecovery`
|
|
94
|
+
// and `KeyManager.generateKeyPairSync` make — the ones Commons failed on.
|
|
95
|
+
expect(generateMnemonic(wordlist, 128).split(' ')).toHaveLength(12);
|
|
96
|
+
const keyPair = core.KeyManager.generateKeyPairSync();
|
|
97
|
+
expect(keyPair.privateKey).toMatch(/^[0-9a-f]{64}$/);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('the polyfill imports nothing that can reach a crypto library', () => {
|
|
102
|
+
const source = readFileSync(resolve(__dirname, '..', 'polyfill.ts'), 'utf8');
|
|
103
|
+
const specifiers = [...source.matchAll(/^import\s+[^'"]*?['"]([^'"]+)['"]/gm)].map(
|
|
104
|
+
(match) => match[1],
|
|
105
|
+
);
|
|
106
|
+
expect(specifiers.sort()).toEqual(['@oxy.so/protocol/random', 'buffer']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('the core entry imports the polyfill before anything else', () => {
|
|
110
|
+
const source = readFileSync(resolve(__dirname, '..', '..', 'index.ts'), 'utf8');
|
|
111
|
+
const firstModuleReference = source.match(/^(?:import|export)\s[^;]*?['"]([^'"]+)['"]/m);
|
|
112
|
+
expect(firstModuleReference?.[1]).toBe('./crypto/polyfill');
|
|
113
|
+
});
|
|
114
|
+
});
|
package/src/crypto/polyfill.ts
CHANGED
|
@@ -13,8 +13,23 @@
|
|
|
13
13
|
* 2. Node — backed by the built-in `node:crypto` module (`webcrypto`, else
|
|
14
14
|
* `randomFillSync`). This is what a Node runtime WITHOUT a global WebCrypto
|
|
15
15
|
* (Node 18 script entrypoints, some embedded hosts) falls back to.
|
|
16
|
-
* 3. React Native — `expo-crypto.getRandomBytes
|
|
17
|
-
*
|
|
16
|
+
* 3. React Native — `expo-crypto.getRandomBytes`, through the dependency-free
|
|
17
|
+
* `@oxy.so/protocol/random` entry.
|
|
18
|
+
*
|
|
19
|
+
* # Evaluation order — this module must import NOTHING that can capture the global
|
|
20
|
+
*
|
|
21
|
+
* `@noble/hashes` 1.x reads `globalThis.crypto` exactly once, when its
|
|
22
|
+
* `crypto.js` is evaluated, and `randomBytes` uses that captured binding for
|
|
23
|
+
* the lifetime of the app. ES imports are evaluated before the importing
|
|
24
|
+
* module's body, so every module this file imports runs BEFORE the shim below
|
|
25
|
+
* is installed. This file used to import `@oxy.so/protocol`'s root entry,
|
|
26
|
+
* which reaches `@noble/curves` → `@noble/hashes` through the envelope signer:
|
|
27
|
+
* on Hermes (no `globalThis.crypto` at startup) noble captured `undefined`
|
|
28
|
+
* before the shim existed, and every identity creation on Android failed with
|
|
29
|
+
* `crypto.getRandomValues must be defined`. Its imports are therefore limited
|
|
30
|
+
* to `buffer` and `@oxy.so/protocol/random`, both free of crypto libraries,
|
|
31
|
+
* and `index.ts` imports this file first. `__tests__/polyfillOrder.test.ts`
|
|
32
|
+
* guards both halves.
|
|
18
33
|
*
|
|
19
34
|
* Historically step (2) delegated to `@oxy.so/protocol`'s RN-only
|
|
20
35
|
* `getRandomBytesRN`, which THROWS on Node — so any Node host lacking a global
|
|
@@ -23,7 +38,7 @@
|
|
|
23
38
|
*/
|
|
24
39
|
|
|
25
40
|
import { Buffer } from 'buffer';
|
|
26
|
-
import { getRandomBytesRN, isNodeJS } from '@oxy.so/protocol';
|
|
41
|
+
import { getRandomBytesRN, isNodeJS } from '@oxy.so/protocol/random';
|
|
27
42
|
|
|
28
43
|
const getGlobalObject = (): typeof globalThis => {
|
|
29
44
|
if (typeof globalThis !== 'undefined') return globalThis;
|
|
@@ -125,7 +140,7 @@ const cryptoPolyfill: CryptoLike = {
|
|
|
125
140
|
return array;
|
|
126
141
|
}
|
|
127
142
|
// React Native (and any non-Node host without WebCrypto): synchronous
|
|
128
|
-
// expo-crypto via @oxy.so/protocol's RN `platform/
|
|
143
|
+
// expo-crypto via @oxy.so/protocol's RN `platform/random` variant.
|
|
129
144
|
const bytes = getRandomBytesRN(array.byteLength);
|
|
130
145
|
const uint8View = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
131
146
|
uint8View.set(bytes);
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* If a symbol does not appear here, it is NOT part of the public API.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
// Ensure crypto polyfills are loaded before anything else
|
|
20
|
+
// Ensure crypto polyfills are loaded before anything else. This MUST stay the
|
|
21
|
+
// first import: `@noble/hashes` captures `globalThis.crypto` when it is
|
|
22
|
+
// evaluated, so the shim has to exist before any module below reaches it (see
|
|
23
|
+
// the evaluation-order note in `./crypto/polyfill`).
|
|
21
24
|
import './crypto/polyfill';
|
|
22
25
|
|
|
23
26
|
// ---------------------------------------------------------------------------
|