@oxy.so/protocol 1.1.0 → 1.1.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/platform/crypto.js +6 -10
- package/dist/cjs/platform/crypto.native.js +14 -46
- package/dist/cjs/platform/optionalPeer.js +22 -0
- package/dist/cjs/platform/random.js +18 -0
- package/dist/cjs/platform/random.native.js +52 -0
- package/dist/cjs/random.js +25 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/platform/crypto.js +4 -9
- package/dist/esm/platform/crypto.native.js +10 -43
- package/dist/esm/platform/optionalPeer.js +19 -0
- package/dist/esm/platform/random.js +15 -0
- package/dist/esm/platform/random.native.js +48 -0
- package/dist/esm/random.js +19 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/platform/crypto.d.ts +1 -7
- package/dist/types/platform/crypto.native.d.ts +2 -10
- package/dist/types/platform/optionalPeer.d.ts +9 -0
- package/dist/types/platform/random.d.ts +13 -0
- package/dist/types/platform/random.native.d.ts +33 -0
- package/dist/types/random.d.ts +19 -0
- package/package.json +20 -3
- package/src/__tests__/randomEntry.test.ts +107 -0
- package/src/platform/crypto.native.ts +10 -48
- package/src/platform/crypto.ts +4 -9
- package/src/platform/optionalPeer.ts +23 -0
- package/src/platform/random.native.ts +56 -0
- package/src/platform/random.ts +18 -0
- package/src/random.ts +20 -0
|
@@ -83,11 +83,5 @@ export declare function loadAsyncStorage(): Promise<{
|
|
|
83
83
|
removeItem: (key: string) => Promise<void>;
|
|
84
84
|
};
|
|
85
85
|
}>;
|
|
86
|
-
|
|
87
|
-
* Synchronous random-bytes via `expo-crypto.getRandomBytes`. Only available
|
|
88
|
-
* in the React Native variant. The default variant throws because Node and
|
|
89
|
-
* browsers have their own native CSPRNGs (`crypto.randomBytes` and
|
|
90
|
-
* `crypto.getRandomValues` respectively) — callers should use those.
|
|
91
|
-
*/
|
|
92
|
-
export declare function getRandomBytesRN(_byteCount: number): Uint8Array;
|
|
86
|
+
export { getRandomBytesRN } from './random';
|
|
93
87
|
export declare function loadSharedIdentityBridge(): Promise<SharedIdentityBridge | null>;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Those three RN modules are declared OPTIONAL peer dependencies in
|
|
25
25
|
* `package.json`. A static `import` contradicts that: an optional peer that is
|
|
26
26
|
* omitted does not degrade, it fails to RESOLVE, and Metro aborts the whole
|
|
27
|
-
* bundle. Because `@oxy.so/core`
|
|
27
|
+
* bundle. Because `@oxy.so/core` imports `@oxy.so/protocol`
|
|
28
28
|
* from its root entry, this file is in the eager graph of EVERY React Native
|
|
29
29
|
* app on `@oxy.so/core` — so a single undeclared optional peer broke the native
|
|
30
30
|
* bundle of every app that did not happen to install it, with a resolution
|
|
@@ -65,13 +65,5 @@ export declare function loadSecureStore(): Promise<ExpoSecureStoreLike>;
|
|
|
65
65
|
export declare function loadAsyncStorage(): Promise<{
|
|
66
66
|
default: AsyncStorageLike;
|
|
67
67
|
}>;
|
|
68
|
-
|
|
69
|
-
* Synchronous random-bytes via `expo-crypto.getRandomBytes`.
|
|
70
|
-
*
|
|
71
|
-
* Synchronous by contract: `@oxy.so/core`'s crypto polyfill uses this to back
|
|
72
|
-
* `globalThis.crypto.getRandomValues`, which cannot await. That is why
|
|
73
|
-
* `expo-crypto` is resolved with a synchronous `require` at module scope rather
|
|
74
|
-
* than a dynamic `import()`.
|
|
75
|
-
*/
|
|
76
|
-
export declare function getRandomBytesRN(byteCount: number): Uint8Array;
|
|
68
|
+
export { getRandomBytesRN } from './random.native';
|
|
77
69
|
export declare function loadSharedIdentityBridge(): Promise<SharedIdentityBridge | null>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actionable error for a missing optional React Native peer.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the React Native platform variants (`crypto.native.ts`,
|
|
5
|
+
* `random.native.ts`). It carries the underlying Metro resolution message so
|
|
6
|
+
* the failure is never silent: the variants' `try { require(...) } catch` only
|
|
7
|
+
* defers the report to the point where the capability is actually used.
|
|
8
|
+
*/
|
|
9
|
+
export declare function missingOptionalPeerError(packageName: string, capability: string, cause: unknown): Error;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous CSPRNG source — default variant (Node.js, browsers, bundlers).
|
|
3
|
+
*
|
|
4
|
+
* Companion to `./random.native.ts`, which Metro substitutes on iOS / Android.
|
|
5
|
+
* Node and browsers own a native CSPRNG (`node:crypto`, `globalThis.crypto`),
|
|
6
|
+
* so `getRandomBytesRN` only exists here to keep both variants' surfaces
|
|
7
|
+
* identical, and throws if anything reaches it outside React Native.
|
|
8
|
+
*
|
|
9
|
+
* Like its sibling, this module imports nothing: it is part of the
|
|
10
|
+
* `@oxy.so/protocol/random` entry that `@oxy.so/core`'s crypto polyfill loads
|
|
11
|
+
* BEFORE any crypto library is evaluated.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getRandomBytesRN(_byteCount: number): Uint8Array;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous CSPRNG source — React Native variant.
|
|
3
|
+
*
|
|
4
|
+
* Companion to `./random.ts`; Metro substitutes this file on iOS / Android
|
|
5
|
+
* (see `./crypto.ts` for how the `.native` split works).
|
|
6
|
+
*
|
|
7
|
+
* This module is deliberately tiny and imports NOTHING but `expo-crypto`: it
|
|
8
|
+
* backs `@oxy.so/core`'s `globalThis.crypto.getRandomValues` polyfill, which
|
|
9
|
+
* must be fully installed before any module that captures `globalThis.crypto`
|
|
10
|
+
* at evaluation time (`@noble/hashes` 1.x's `crypto.js` does exactly that) is
|
|
11
|
+
* evaluated. Reaching a crypto library from here would let that library
|
|
12
|
+
* capture the missing global first and throw
|
|
13
|
+
* `crypto.getRandomValues must be defined` for the lifetime of the app.
|
|
14
|
+
*
|
|
15
|
+
* `expo-crypto` is an OPTIONAL peer, so it is resolved with a string-literal
|
|
16
|
+
* `require` inside a `try` (Metro's optional-dependency form; see
|
|
17
|
+
* `./crypto.native.ts`), and the load stays synchronous because
|
|
18
|
+
* `getRandomValues` cannot await.
|
|
19
|
+
*/
|
|
20
|
+
import type { ExpoCryptoLike } from './expoTypes';
|
|
21
|
+
/**
|
|
22
|
+
* The resolved `expo-crypto` module, or an actionable error naming the missing
|
|
23
|
+
* peer and the capability that needed it. Shared with `./crypto.native.ts` so
|
|
24
|
+
* the optional peer is resolved in exactly one place.
|
|
25
|
+
*/
|
|
26
|
+
export declare function requireExpoCrypto(capability: string): ExpoCryptoLike;
|
|
27
|
+
/**
|
|
28
|
+
* Synchronous random bytes via `expo-crypto.getRandomBytes`.
|
|
29
|
+
*
|
|
30
|
+
* Synchronous by contract: `@oxy.so/core`'s crypto polyfill uses this to back
|
|
31
|
+
* `globalThis.crypto.getRandomValues`, which cannot await.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getRandomBytesRN(byteCount: number): Uint8Array;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@oxy.so/protocol/random` — the platform randomness source, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* `@oxy.so/core`'s crypto polyfill installs `globalThis.crypto.getRandomValues`
|
|
5
|
+
* on hosts that lack it (React Native / Hermes). `@noble/hashes` 1.x captures
|
|
6
|
+
* `globalThis.crypto` ONCE, when its `crypto.js` is evaluated, so the polyfill
|
|
7
|
+
* has to be installed before any `@noble/*` module is evaluated. ES imports are
|
|
8
|
+
* evaluated before the importing module's body, so whatever the polyfill
|
|
9
|
+
* imports is evaluated first: importing the ROOT entry (which reaches
|
|
10
|
+
* `@noble/curves` through the envelope signer) let noble capture `undefined`
|
|
11
|
+
* and broke identity creation on Android.
|
|
12
|
+
*
|
|
13
|
+
* This entry therefore reaches no crypto library, no `@oxy.so/*` package and no
|
|
14
|
+
* third-party module other than the optional `expo-crypto` peer (RN variant
|
|
15
|
+
* only). `src/__tests__/randomEntry.test.ts` walks its module graph to keep it
|
|
16
|
+
* that way.
|
|
17
|
+
*/
|
|
18
|
+
export { isNodeJS, isReactNative } from './platform/platform';
|
|
19
|
+
export { getRandomBytesRN } from './platform/random';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/protocol",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Oxy Protocol — the app-agnostic base substrate: signed-record envelope, canonical JSON, signature/verification, and platform crypto. Reused by any Oxy app to decentralize its own content.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
],
|
|
13
13
|
"node": [
|
|
14
14
|
"dist/types/node/index.d.ts"
|
|
15
|
+
],
|
|
16
|
+
"random": [
|
|
17
|
+
"dist/types/random.d.ts"
|
|
15
18
|
]
|
|
16
19
|
}
|
|
17
20
|
},
|
|
@@ -56,11 +59,25 @@
|
|
|
56
59
|
},
|
|
57
60
|
"default": "./dist/esm/secp256k1.js"
|
|
58
61
|
},
|
|
62
|
+
"./random": {
|
|
63
|
+
"react-native": "./dist/esm/random.js",
|
|
64
|
+
"import": {
|
|
65
|
+
"types": "./dist/types/random.d.ts",
|
|
66
|
+
"default": "./dist/esm/random.js"
|
|
67
|
+
},
|
|
68
|
+
"require": {
|
|
69
|
+
"types": "./dist/types/random.d.ts",
|
|
70
|
+
"default": "./dist/cjs/random.js"
|
|
71
|
+
},
|
|
72
|
+
"default": "./dist/esm/random.js"
|
|
73
|
+
},
|
|
59
74
|
"./package.json": "./package.json"
|
|
60
75
|
},
|
|
61
76
|
"react-native": {
|
|
62
77
|
"./dist/esm/platform/crypto.js": "./dist/esm/platform/crypto.native.js",
|
|
63
|
-
"./dist/cjs/platform/crypto.js": "./dist/cjs/platform/crypto.native.js"
|
|
78
|
+
"./dist/cjs/platform/crypto.js": "./dist/cjs/platform/crypto.native.js",
|
|
79
|
+
"./dist/esm/platform/random.js": "./dist/esm/platform/random.native.js",
|
|
80
|
+
"./dist/cjs/platform/random.js": "./dist/cjs/platform/random.native.js"
|
|
64
81
|
},
|
|
65
82
|
"files": [
|
|
66
83
|
"NOTICE",
|
|
@@ -114,7 +131,7 @@
|
|
|
114
131
|
},
|
|
115
132
|
"dependencies": {
|
|
116
133
|
"@noble/curves": "^1.9.7",
|
|
117
|
-
"@oxy.so/contracts": "^1.
|
|
134
|
+
"@oxy.so/contracts": "^1.4.0",
|
|
118
135
|
"zod": "^3.25.64"
|
|
119
136
|
},
|
|
120
137
|
"peerDependencies": {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@oxy.so/protocol/random` must reach no module other than its own platform
|
|
3
|
+
* files and the optional `expo-crypto` peer.
|
|
4
|
+
*
|
|
5
|
+
* `@oxy.so/core`'s crypto polyfill imports this entry to install
|
|
6
|
+
* `globalThis.crypto.getRandomValues` on React Native. Everything the polyfill
|
|
7
|
+
* imports is evaluated BEFORE the polyfill body, and `@noble/hashes` 1.x
|
|
8
|
+
* captures `globalThis.crypto` once, at evaluation. When the polyfill imported
|
|
9
|
+
* the root entry instead, noble was reached through the envelope signer,
|
|
10
|
+
* captured `undefined`, and every Android identity creation failed with
|
|
11
|
+
* `crypto.getRandomValues must be defined`. This guard keeps the entry's graph
|
|
12
|
+
* dependency-free, including the `.native` siblings Metro substitutes.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const SRC_DIR = resolve(__dirname, '..');
|
|
19
|
+
const RANDOM_ENTRY = join(SRC_DIR, 'random.ts');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Every value-level module specifier: static imports/re-exports, literal
|
|
23
|
+
* `require()`s and `import()`s. Comments are stripped first so prose that
|
|
24
|
+
* mentions a module (these files document why they avoid them) is not counted.
|
|
25
|
+
*/
|
|
26
|
+
function valueSpecifiers(rawSource: string): string[] {
|
|
27
|
+
const source = rawSource.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
28
|
+
const specifiers: string[] = [];
|
|
29
|
+
const patterns = [
|
|
30
|
+
/(?:^|\n)\s*(?:import|export)\s+(?!type\s)[\s\S]*?\sfrom\s+['"]([^'"]+)['"]/g,
|
|
31
|
+
/(?:^|\n)\s*import\s+['"]([^'"]+)['"]/g,
|
|
32
|
+
/\brequire\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
33
|
+
/\bimport\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
34
|
+
];
|
|
35
|
+
for (const pattern of patterns) {
|
|
36
|
+
let match = pattern.exec(source);
|
|
37
|
+
while (match !== null) {
|
|
38
|
+
specifiers.push(match[1]);
|
|
39
|
+
match = pattern.exec(source);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return specifiers;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveRelative(fromFile: string, specifier: string): string {
|
|
46
|
+
const base = resolve(dirname(fromFile), specifier);
|
|
47
|
+
for (const candidate of [`${base}.ts`, join(base, 'index.ts')]) {
|
|
48
|
+
if (existsSync(candidate)) {
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`unresolved relative import '${specifier}' in ${fromFile}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function walk(entry: string): { files: string[]; external: string[] } {
|
|
56
|
+
const seen = new Set<string>();
|
|
57
|
+
const external = new Set<string>();
|
|
58
|
+
const queue = [entry];
|
|
59
|
+
while (queue.length > 0) {
|
|
60
|
+
const file = queue.shift() as string;
|
|
61
|
+
if (seen.has(file)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
seen.add(file);
|
|
65
|
+
const nativeSibling = file.replace(/\.ts$/, '.native.ts');
|
|
66
|
+
if (!file.endsWith('.native.ts') && existsSync(nativeSibling)) {
|
|
67
|
+
queue.push(nativeSibling);
|
|
68
|
+
}
|
|
69
|
+
for (const specifier of valueSpecifiers(readFileSync(file, 'utf8'))) {
|
|
70
|
+
if (specifier.startsWith('.')) {
|
|
71
|
+
queue.push(resolveRelative(file, specifier));
|
|
72
|
+
} else {
|
|
73
|
+
external.add(specifier);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { files: [...seen], external: [...external] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('@oxy.so/protocol/random entry', () => {
|
|
81
|
+
it('reaches no third-party module other than the optional expo-crypto peer', () => {
|
|
82
|
+
expect(walk(RANDOM_ENTRY).external).toEqual(['expo-crypto']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('includes the React Native variant that Metro substitutes', () => {
|
|
86
|
+
// Sanity check on the walker: without the native sibling the guard above
|
|
87
|
+
// would pass vacuously on the default (import-free) variant.
|
|
88
|
+
expect(walk(RANDOM_ENTRY).files).toEqual(
|
|
89
|
+
expect.arrayContaining([join(SRC_DIR, 'platform', 'random.native.ts')]),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('is exported as a package subpath', () => {
|
|
94
|
+
const manifest = JSON.parse(readFileSync(resolve(SRC_DIR, '..', 'package.json'), 'utf8')) as {
|
|
95
|
+
exports: Record<string, unknown>;
|
|
96
|
+
};
|
|
97
|
+
expect(manifest.exports['./random']).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('exposes the platform predicates and the RN randomness source', () => {
|
|
101
|
+
const entry = require('../random') as typeof import('../random');
|
|
102
|
+
expect(typeof entry.isNodeJS).toBe('function');
|
|
103
|
+
expect(typeof entry.isReactNative).toBe('function');
|
|
104
|
+
expect(entry.isNodeJS()).toBe(true);
|
|
105
|
+
expect(() => entry.getRandomBytesRN(8)).toThrow(/outside React Native/);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Those three RN modules are declared OPTIONAL peer dependencies in
|
|
25
25
|
* `package.json`. A static `import` contradicts that: an optional peer that is
|
|
26
26
|
* omitted does not degrade, it fails to RESOLVE, and Metro aborts the whole
|
|
27
|
-
* bundle. Because `@oxy.so/core`
|
|
27
|
+
* bundle. Because `@oxy.so/core` imports `@oxy.so/protocol`
|
|
28
28
|
* from its root entry, this file is in the eager graph of EVERY React Native
|
|
29
29
|
* app on `@oxy.so/core` — so a single undeclared optional peer broke the native
|
|
30
30
|
* bundle of every app that did not happen to install it, with a resolution
|
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
|
|
55
55
|
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
56
56
|
import type { ExpoCryptoLike, ExpoSecureStoreLike, SharedIdentityBridge } from './expoTypes';
|
|
57
|
+
import { missingOptionalPeerError } from './optionalPeer';
|
|
58
|
+
import { requireExpoCrypto } from './random.native';
|
|
57
59
|
|
|
58
60
|
// Re-export the interfaces so consumers can import them from the same
|
|
59
61
|
// entry-point they use for the loaders (mirrors the default variant).
|
|
@@ -77,14 +79,6 @@ type AsyncStorageLike = {
|
|
|
77
79
|
removeItem: (key: string) => Promise<void>;
|
|
78
80
|
};
|
|
79
81
|
|
|
80
|
-
let expoCryptoModule: ExpoCryptoLike | null = null;
|
|
81
|
-
let expoCryptoError: unknown;
|
|
82
|
-
try {
|
|
83
|
-
expoCryptoModule = require('expo-crypto') as ExpoCryptoLike;
|
|
84
|
-
} catch (error) {
|
|
85
|
-
expoCryptoError = error;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
82
|
let secureStoreModule: ExpoSecureStoreLike | null = null;
|
|
89
83
|
let secureStoreError: unknown;
|
|
90
84
|
try {
|
|
@@ -107,23 +101,6 @@ try {
|
|
|
107
101
|
asyncStorageError = error;
|
|
108
102
|
}
|
|
109
103
|
|
|
110
|
-
/**
|
|
111
|
-
* Actionable error for a missing optional peer. Carries the underlying Metro
|
|
112
|
-
* resolution message so the failure is never silent — the `catch` above only
|
|
113
|
-
* defers the report to the point where the capability is actually needed.
|
|
114
|
-
*/
|
|
115
|
-
function missingOptionalPeerError(packageName: string, capability: string, cause: unknown): Error {
|
|
116
|
-
const sentences = [
|
|
117
|
-
`[oxy.protocol.crypto] '${packageName}' is not installed, so ${capability} is unavailable in this app.`,
|
|
118
|
-
'It is an optional peer dependency of @oxy.so/protocol that the React Native runtime needs —',
|
|
119
|
-
`install it with \`npx expo install ${packageName}\`.`,
|
|
120
|
-
];
|
|
121
|
-
if (cause instanceof Error) {
|
|
122
|
-
sentences.push(`Underlying error: ${cause.message}`);
|
|
123
|
-
}
|
|
124
|
-
return new Error(sentences.join(' '));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
104
|
// ---------------------------------------------------------------------------
|
|
128
105
|
// Node `crypto` — never available in RN.
|
|
129
106
|
// ---------------------------------------------------------------------------
|
|
@@ -149,10 +126,7 @@ export async function loadNodeCrypto(): Promise<typeof import('crypto')> {
|
|
|
149
126
|
// ---------------------------------------------------------------------------
|
|
150
127
|
|
|
151
128
|
export async function loadExpoCrypto(): Promise<ExpoCryptoLike> {
|
|
152
|
-
|
|
153
|
-
throw missingOptionalPeerError('expo-crypto', 'React Native cryptography', expoCryptoError);
|
|
154
|
-
}
|
|
155
|
-
return expoCryptoModule;
|
|
129
|
+
return requireExpoCrypto('React Native cryptography');
|
|
156
130
|
}
|
|
157
131
|
|
|
158
132
|
// ---------------------------------------------------------------------------
|
|
@@ -188,24 +162,12 @@ export async function loadAsyncStorage(): Promise<{ default: AsyncStorageLike }>
|
|
|
188
162
|
return { default: asyncStorageModule };
|
|
189
163
|
}
|
|
190
164
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
* than a dynamic `import()`.
|
|
198
|
-
*/
|
|
199
|
-
export function getRandomBytesRN(byteCount: number): Uint8Array {
|
|
200
|
-
if (!expoCryptoModule) {
|
|
201
|
-
throw missingOptionalPeerError(
|
|
202
|
-
'expo-crypto',
|
|
203
|
-
'the React Native CSPRNG (crypto.getRandomValues)',
|
|
204
|
-
expoCryptoError,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
return expoCryptoModule.getRandomBytes(byteCount);
|
|
208
|
-
}
|
|
165
|
+
// Synchronous random bytes (and the single `expo-crypto` resolution) live in
|
|
166
|
+
// the dependency-free `./random.native` module, so `@oxy.so/core`'s crypto
|
|
167
|
+
// polyfill can load them through `@oxy.so/protocol/random` without evaluating
|
|
168
|
+
// any crypto library first. The explicit `.native` specifier keeps tsc and
|
|
169
|
+
// Metro pointed at the same file.
|
|
170
|
+
export { getRandomBytesRN } from './random.native';
|
|
209
171
|
|
|
210
172
|
// ---------------------------------------------------------------------------
|
|
211
173
|
// Shared identity bridge — `@oxy.so/expo-oxy-identity` (native-only, OPTIONAL).
|
package/src/platform/crypto.ts
CHANGED
|
@@ -148,15 +148,10 @@ export async function loadAsyncStorage(): Promise<{
|
|
|
148
148
|
throw notReactNativeError('@react-native-async-storage/async-storage');
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
* `crypto.getRandomValues` respectively) — callers should use those.
|
|
156
|
-
*/
|
|
157
|
-
export function getRandomBytesRN(_byteCount: number): Uint8Array {
|
|
158
|
-
throw notReactNativeError('expo-crypto.getRandomBytes (sync)');
|
|
159
|
-
}
|
|
151
|
+
// Synchronous random bytes live in the dependency-free `./random` module so
|
|
152
|
+
// `@oxy.so/core`'s crypto polyfill can load them through the
|
|
153
|
+
// `@oxy.so/protocol/random` entry without evaluating any crypto library first.
|
|
154
|
+
export { getRandomBytesRN } from './random';
|
|
160
155
|
|
|
161
156
|
// ---------------------------------------------------------------------------
|
|
162
157
|
// Shared identity bridge — `@oxy.so/expo-oxy-identity` (native-only).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actionable error for a missing optional React Native peer.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the React Native platform variants (`crypto.native.ts`,
|
|
5
|
+
* `random.native.ts`). It carries the underlying Metro resolution message so
|
|
6
|
+
* the failure is never silent: the variants' `try { require(...) } catch` only
|
|
7
|
+
* defers the report to the point where the capability is actually used.
|
|
8
|
+
*/
|
|
9
|
+
export function missingOptionalPeerError(
|
|
10
|
+
packageName: string,
|
|
11
|
+
capability: string,
|
|
12
|
+
cause: unknown,
|
|
13
|
+
): Error {
|
|
14
|
+
const sentences = [
|
|
15
|
+
`[oxy.protocol.crypto] '${packageName}' is not installed, so ${capability} is unavailable in this app.`,
|
|
16
|
+
'It is an optional peer dependency of @oxy.so/protocol that the React Native runtime needs —',
|
|
17
|
+
`install it with \`npx expo install ${packageName}\`.`,
|
|
18
|
+
];
|
|
19
|
+
if (cause instanceof Error) {
|
|
20
|
+
sentences.push(`Underlying error: ${cause.message}`);
|
|
21
|
+
}
|
|
22
|
+
return new Error(sentences.join(' '));
|
|
23
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous CSPRNG source — React Native variant.
|
|
3
|
+
*
|
|
4
|
+
* Companion to `./random.ts`; Metro substitutes this file on iOS / Android
|
|
5
|
+
* (see `./crypto.ts` for how the `.native` split works).
|
|
6
|
+
*
|
|
7
|
+
* This module is deliberately tiny and imports NOTHING but `expo-crypto`: it
|
|
8
|
+
* backs `@oxy.so/core`'s `globalThis.crypto.getRandomValues` polyfill, which
|
|
9
|
+
* must be fully installed before any module that captures `globalThis.crypto`
|
|
10
|
+
* at evaluation time (`@noble/hashes` 1.x's `crypto.js` does exactly that) is
|
|
11
|
+
* evaluated. Reaching a crypto library from here would let that library
|
|
12
|
+
* capture the missing global first and throw
|
|
13
|
+
* `crypto.getRandomValues must be defined` for the lifetime of the app.
|
|
14
|
+
*
|
|
15
|
+
* `expo-crypto` is an OPTIONAL peer, so it is resolved with a string-literal
|
|
16
|
+
* `require` inside a `try` (Metro's optional-dependency form; see
|
|
17
|
+
* `./crypto.native.ts`), and the load stays synchronous because
|
|
18
|
+
* `getRandomValues` cannot await.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ExpoCryptoLike } from './expoTypes';
|
|
22
|
+
import { missingOptionalPeerError } from './optionalPeer';
|
|
23
|
+
|
|
24
|
+
declare const require: (moduleName: string) => unknown;
|
|
25
|
+
|
|
26
|
+
let expoCryptoModule: ExpoCryptoLike | null = null;
|
|
27
|
+
let expoCryptoError: unknown;
|
|
28
|
+
try {
|
|
29
|
+
expoCryptoModule = require('expo-crypto') as ExpoCryptoLike;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
expoCryptoError = error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The resolved `expo-crypto` module, or an actionable error naming the missing
|
|
36
|
+
* peer and the capability that needed it. Shared with `./crypto.native.ts` so
|
|
37
|
+
* the optional peer is resolved in exactly one place.
|
|
38
|
+
*/
|
|
39
|
+
export function requireExpoCrypto(capability: string): ExpoCryptoLike {
|
|
40
|
+
if (!expoCryptoModule) {
|
|
41
|
+
throw missingOptionalPeerError('expo-crypto', capability, expoCryptoError);
|
|
42
|
+
}
|
|
43
|
+
return expoCryptoModule;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Synchronous random bytes via `expo-crypto.getRandomBytes`.
|
|
48
|
+
*
|
|
49
|
+
* Synchronous by contract: `@oxy.so/core`'s crypto polyfill uses this to back
|
|
50
|
+
* `globalThis.crypto.getRandomValues`, which cannot await.
|
|
51
|
+
*/
|
|
52
|
+
export function getRandomBytesRN(byteCount: number): Uint8Array {
|
|
53
|
+
return requireExpoCrypto('the React Native CSPRNG (crypto.getRandomValues)').getRandomBytes(
|
|
54
|
+
byteCount,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous CSPRNG source — default variant (Node.js, browsers, bundlers).
|
|
3
|
+
*
|
|
4
|
+
* Companion to `./random.native.ts`, which Metro substitutes on iOS / Android.
|
|
5
|
+
* Node and browsers own a native CSPRNG (`node:crypto`, `globalThis.crypto`),
|
|
6
|
+
* so `getRandomBytesRN` only exists here to keep both variants' surfaces
|
|
7
|
+
* identical, and throws if anything reaches it outside React Native.
|
|
8
|
+
*
|
|
9
|
+
* Like its sibling, this module imports nothing: it is part of the
|
|
10
|
+
* `@oxy.so/protocol/random` entry that `@oxy.so/core`'s crypto polyfill loads
|
|
11
|
+
* BEFORE any crypto library is evaluated.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function getRandomBytesRN(_byteCount: number): Uint8Array {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"[oxy.protocol.crypto] Tried to load 'expo-crypto.getRandomBytes (sync)' 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.",
|
|
17
|
+
);
|
|
18
|
+
}
|
package/src/random.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@oxy.so/protocol/random` — the platform randomness source, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* `@oxy.so/core`'s crypto polyfill installs `globalThis.crypto.getRandomValues`
|
|
5
|
+
* on hosts that lack it (React Native / Hermes). `@noble/hashes` 1.x captures
|
|
6
|
+
* `globalThis.crypto` ONCE, when its `crypto.js` is evaluated, so the polyfill
|
|
7
|
+
* has to be installed before any `@noble/*` module is evaluated. ES imports are
|
|
8
|
+
* evaluated before the importing module's body, so whatever the polyfill
|
|
9
|
+
* imports is evaluated first: importing the ROOT entry (which reaches
|
|
10
|
+
* `@noble/curves` through the envelope signer) let noble capture `undefined`
|
|
11
|
+
* and broke identity creation on Android.
|
|
12
|
+
*
|
|
13
|
+
* This entry therefore reaches no crypto library, no `@oxy.so/*` package and no
|
|
14
|
+
* third-party module other than the optional `expo-crypto` peer (RN variant
|
|
15
|
+
* only). `src/__tests__/randomEntry.test.ts` walks its module graph to keep it
|
|
16
|
+
* that way.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export { isNodeJS, isReactNative } from './platform/platform';
|
|
20
|
+
export { getRandomBytesRN } from './platform/random';
|