@oxy.so/protocol 1.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 (122) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/dist/cjs/.tsbuildinfo +1 -0
  4. package/dist/cjs/chain/continuity.js +54 -0
  5. package/dist/cjs/chain/engine.js +34 -0
  6. package/dist/cjs/chain/recordStore.js +25 -0
  7. package/dist/cjs/chain/types.js +22 -0
  8. package/dist/cjs/chain/verify.js +82 -0
  9. package/dist/cjs/envelope/canonicalJson.js +107 -0
  10. package/dist/cjs/envelope/recordId.js +60 -0
  11. package/dist/cjs/envelope/sign.js +75 -0
  12. package/dist/cjs/envelope/signingInput.js +32 -0
  13. package/dist/cjs/identity/resolver.js +50 -0
  14. package/dist/cjs/index.js +71 -0
  15. package/dist/cjs/node/constants.js +85 -0
  16. package/dist/cjs/node/didWebResolver.js +126 -0
  17. package/dist/cjs/node/httpFetch.js +61 -0
  18. package/dist/cjs/node/index.js +71 -0
  19. package/dist/cjs/node/nodeApp.js +344 -0
  20. package/dist/cjs/node/nodeClient.js +204 -0
  21. package/dist/cjs/node/rateLimit.js +187 -0
  22. package/dist/cjs/node/verifyRecord.js +51 -0
  23. package/dist/cjs/platform/crypto.js +186 -0
  24. package/dist/cjs/platform/crypto.native.js +204 -0
  25. package/dist/cjs/platform/expoTypes.js +24 -0
  26. package/dist/cjs/platform/platform.js +33 -0
  27. package/dist/cjs/secp256k1.js +148 -0
  28. package/dist/cjs/transparency/checkpoint.js +79 -0
  29. package/dist/cjs/transparency/tree.js +197 -0
  30. package/dist/esm/.tsbuildinfo +1 -0
  31. package/dist/esm/chain/continuity.js +51 -0
  32. package/dist/esm/chain/engine.js +31 -0
  33. package/dist/esm/chain/recordStore.js +24 -0
  34. package/dist/esm/chain/types.js +19 -0
  35. package/dist/esm/chain/verify.js +78 -0
  36. package/dist/esm/envelope/canonicalJson.js +104 -0
  37. package/dist/esm/envelope/recordId.js +56 -0
  38. package/dist/esm/envelope/sign.js +69 -0
  39. package/dist/esm/envelope/signingInput.js +29 -0
  40. package/dist/esm/identity/resolver.js +47 -0
  41. package/dist/esm/index.js +36 -0
  42. package/dist/esm/node/constants.js +82 -0
  43. package/dist/esm/node/didWebResolver.js +122 -0
  44. package/dist/esm/node/httpFetch.js +55 -0
  45. package/dist/esm/node/index.js +28 -0
  46. package/dist/esm/node/nodeApp.js +336 -0
  47. package/dist/esm/node/nodeClient.js +198 -0
  48. package/dist/esm/node/rateLimit.js +182 -0
  49. package/dist/esm/node/verifyRecord.js +48 -0
  50. package/dist/esm/platform/crypto.js +145 -0
  51. package/dist/esm/platform/crypto.native.js +196 -0
  52. package/dist/esm/platform/expoTypes.js +23 -0
  53. package/dist/esm/platform/platform.js +29 -0
  54. package/dist/esm/secp256k1.js +137 -0
  55. package/dist/esm/transparency/checkpoint.js +73 -0
  56. package/dist/esm/transparency/tree.js +189 -0
  57. package/dist/types/.tsbuildinfo +1 -0
  58. package/dist/types/chain/continuity.d.ts +28 -0
  59. package/dist/types/chain/engine.d.ts +27 -0
  60. package/dist/types/chain/recordStore.d.ts +85 -0
  61. package/dist/types/chain/types.d.ts +79 -0
  62. package/dist/types/chain/verify.d.ts +45 -0
  63. package/dist/types/envelope/canonicalJson.d.ts +44 -0
  64. package/dist/types/envelope/recordId.d.ts +30 -0
  65. package/dist/types/envelope/sign.d.ts +47 -0
  66. package/dist/types/envelope/signingInput.d.ts +33 -0
  67. package/dist/types/identity/resolver.d.ts +67 -0
  68. package/dist/types/index.d.ts +32 -0
  69. package/dist/types/node/constants.d.ts +80 -0
  70. package/dist/types/node/didWebResolver.d.ts +47 -0
  71. package/dist/types/node/httpFetch.d.ts +60 -0
  72. package/dist/types/node/index.d.ts +28 -0
  73. package/dist/types/node/nodeApp.d.ts +120 -0
  74. package/dist/types/node/nodeClient.d.ts +135 -0
  75. package/dist/types/node/rateLimit.d.ts +95 -0
  76. package/dist/types/node/verifyRecord.d.ts +41 -0
  77. package/dist/types/platform/crypto.d.ts +93 -0
  78. package/dist/types/platform/crypto.native.d.ts +77 -0
  79. package/dist/types/platform/expoTypes.d.ts +99 -0
  80. package/dist/types/platform/platform.d.ts +25 -0
  81. package/dist/types/secp256k1.d.ts +45 -0
  82. package/dist/types/transparency/checkpoint.d.ts +71 -0
  83. package/dist/types/transparency/tree.d.ts +135 -0
  84. package/package.json +157 -0
  85. package/src/__tests__/canonicalJson.test.ts +116 -0
  86. package/src/__tests__/chain.test.ts +279 -0
  87. package/src/__tests__/didWebResolver.test.ts +132 -0
  88. package/src/__tests__/envelope.test.ts +267 -0
  89. package/src/__tests__/nodeApp.test.ts +410 -0
  90. package/src/__tests__/nodeClient.test.ts +177 -0
  91. package/src/__tests__/nodeHarness.ts +151 -0
  92. package/src/__tests__/optionalNativePeers.test.ts +233 -0
  93. package/src/__tests__/rateLimit.test.ts +268 -0
  94. package/src/__tests__/runnerGuard.test.ts +85 -0
  95. package/src/__tests__/secp256k1.test.ts +118 -0
  96. package/src/__tests__/transparency.test.ts +353 -0
  97. package/src/chain/continuity.ts +59 -0
  98. package/src/chain/engine.ts +43 -0
  99. package/src/chain/recordStore.ts +98 -0
  100. package/src/chain/types.ts +85 -0
  101. package/src/chain/verify.ts +102 -0
  102. package/src/envelope/canonicalJson.ts +120 -0
  103. package/src/envelope/recordId.ts +63 -0
  104. package/src/envelope/sign.ts +86 -0
  105. package/src/envelope/signingInput.ts +48 -0
  106. package/src/identity/resolver.ts +90 -0
  107. package/src/index.ts +101 -0
  108. package/src/node/constants.ts +105 -0
  109. package/src/node/didWebResolver.ts +162 -0
  110. package/src/node/httpFetch.ts +88 -0
  111. package/src/node/index.ts +87 -0
  112. package/src/node/nodeApp.ts +471 -0
  113. package/src/node/nodeClient.ts +322 -0
  114. package/src/node/rateLimit.ts +233 -0
  115. package/src/node/verifyRecord.ts +60 -0
  116. package/src/platform/crypto.native.ts +251 -0
  117. package/src/platform/crypto.ts +172 -0
  118. package/src/platform/expoTypes.ts +99 -0
  119. package/src/platform/platform.ts +31 -0
  120. package/src/secp256k1.ts +207 -0
  121. package/src/transparency/checkpoint.ts +109 -0
  122. package/src/transparency/tree.ts +258 -0
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Platform Crypto / Storage — React Native Variant
3
+ *
4
+ * Companion to `./crypto.ts`. See the doc-comment at the top of that file for
5
+ * 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/@oxy.so/protocol/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
+ * each behind Metro's optional-dependency mechanism (see below).
18
+ *
19
+ * Both variants expose the same surface; importers don't care which one
20
+ * they got.
21
+ *
22
+ * # Why `try { require('literal') } catch` and not a static import?
23
+ *
24
+ * Those three RN modules are declared OPTIONAL peer dependencies in
25
+ * `package.json`. A static `import` contradicts that: an optional peer that is
26
+ * omitted does not degrade, it fails to RESOLVE, and Metro aborts the whole
27
+ * bundle. Because `@oxy.so/core`'s `crypto/polyfill` imports `@oxy.so/protocol`
28
+ * from its root entry, this file is in the eager graph of EVERY React Native
29
+ * app on `@oxy.so/core` — so a single undeclared optional peer broke the native
30
+ * bundle of every app that did not happen to install it, with a resolution
31
+ * error pointing at a dependency the app never mentions.
32
+ *
33
+ * Metro treats a `require()` of a STRING LITERAL that sits inside a `try`
34
+ * block as an optional dependency: it resolves it when present, and when
35
+ * absent emits a stub that throws on evaluation instead of failing the build.
36
+ * The `catch` turns that into a `null` module handle, and the loader below
37
+ * throws an actionable error naming the missing package the first time the
38
+ * capability is actually used. Bundle-time hard failure becomes a
39
+ * capability-scoped runtime failure — which is exactly what "optional peer"
40
+ * is supposed to mean.
41
+ *
42
+ * Two constraints this shape has to respect, both learned the hard way:
43
+ *
44
+ * - The specifier MUST be a literal. A runtime-computed `require(variable)`
45
+ * is unresolvable for Metro (that is the bug the shared-identity bridge
46
+ * below documents) and silently yields nothing in a consuming repo.
47
+ * - The load MUST stay synchronous. `getRandomBytesRN` backs
48
+ * `globalThis.crypto.getRandomValues` in `@oxy.so/core`'s polyfill, which
49
+ * cannot await anything.
50
+ *
51
+ * `expo-modules-core` is a NON-optional peer (every RN app has it via `expo`),
52
+ * so it stays a plain static import.
53
+ */
54
+
55
+ import { requireOptionalNativeModule } from 'expo-modules-core';
56
+ import type { ExpoCryptoLike, ExpoSecureStoreLike, SharedIdentityBridge } from './expoTypes';
57
+
58
+ // Re-export the interfaces so consumers can import them from the same
59
+ // entry-point they use for the loaders (mirrors the default variant).
60
+ export type { ExpoCryptoLike, ExpoSecureStoreLike, SharedIdentityBridge };
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Optional peer resolution.
64
+ //
65
+ // `require` is declared locally rather than pulled from `@types/node`'s global:
66
+ // this file only ever runs under Metro, and the local declaration returns
67
+ // `unknown` so every module handle is narrowed explicitly instead of leaking
68
+ // `any` from `NodeRequire`.
69
+ // ---------------------------------------------------------------------------
70
+
71
+ declare const require: (moduleName: string) => unknown;
72
+
73
+ /** Persistent KV storage surface used from `@react-native-async-storage/async-storage`. */
74
+ type AsyncStorageLike = {
75
+ getItem: (key: string) => Promise<string | null>;
76
+ setItem: (key: string, value: string) => Promise<void>;
77
+ removeItem: (key: string) => Promise<void>;
78
+ };
79
+
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
+ let secureStoreModule: ExpoSecureStoreLike | null = null;
89
+ let secureStoreError: unknown;
90
+ try {
91
+ secureStoreModule = require('expo-secure-store') as ExpoSecureStoreLike;
92
+ } catch (error) {
93
+ secureStoreError = error;
94
+ }
95
+
96
+ let asyncStorageModule: AsyncStorageLike | null = null;
97
+ let asyncStorageError: unknown;
98
+ try {
99
+ // Babel's default-import interop unwraps `.default` for us on a static
100
+ // import; a raw `require` has to do it by hand. The `?? namespace` fallback
101
+ // covers a host that hands back a real ESM namespace with no `default`.
102
+ const namespace = require('@react-native-async-storage/async-storage') as AsyncStorageLike & {
103
+ default?: AsyncStorageLike;
104
+ };
105
+ asyncStorageModule = namespace.default ?? namespace;
106
+ } catch (error) {
107
+ asyncStorageError = error;
108
+ }
109
+
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
+ // ---------------------------------------------------------------------------
128
+ // Node `crypto` — never available in RN.
129
+ // ---------------------------------------------------------------------------
130
+
131
+ export async function loadNodeCrypto(): Promise<typeof import('crypto')> {
132
+ // Unreachable in practice: every caller gates with `isNodeJS()` before
133
+ // invoking this. If it somehow does fire, throw immediately with a clear
134
+ // diagnostic rather than letting Metro / Hermes attempt to find a
135
+ // non-existent module at runtime.
136
+ throw new Error(
137
+ "[oxy.protocol.crypto] Node's built-in 'crypto' module is not available " +
138
+ 'in a React Native runtime. Use the RN-specific helpers ' +
139
+ '(loadExpoCrypto, getRandomBytesRN) or the Web Crypto API (`globalThis.crypto`).',
140
+ );
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // expo-crypto — RN cryptographic primitives.
145
+ //
146
+ // The real module satisfies `ExpoCryptoLike` structurally; the structural
147
+ // interface narrows the surface so consumers never pull expo's own types into
148
+ // their compilation (see expoTypes.ts).
149
+ // ---------------------------------------------------------------------------
150
+
151
+ export async function loadExpoCrypto(): Promise<ExpoCryptoLike> {
152
+ if (!expoCryptoModule) {
153
+ throw missingOptionalPeerError('expo-crypto', 'React Native cryptography', expoCryptoError);
154
+ }
155
+ return expoCryptoModule;
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // expo-secure-store — RN keychain / keystore.
160
+ // ---------------------------------------------------------------------------
161
+
162
+ export async function loadSecureStore(): Promise<ExpoSecureStoreLike> {
163
+ if (!secureStoreModule) {
164
+ throw missingOptionalPeerError(
165
+ 'expo-secure-store',
166
+ 'on-device identity storage',
167
+ secureStoreError,
168
+ );
169
+ }
170
+ return secureStoreModule;
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // @react-native-async-storage/async-storage — RN persistent KV storage.
175
+ // ---------------------------------------------------------------------------
176
+
177
+ export async function loadAsyncStorage(): Promise<{ default: AsyncStorageLike }> {
178
+ if (!asyncStorageModule) {
179
+ throw missingOptionalPeerError(
180
+ '@react-native-async-storage/async-storage',
181
+ 'device/session persistence',
182
+ asyncStorageError,
183
+ );
184
+ }
185
+ // Mirror the shape callers historically used (`module.default.<method>`)
186
+ // so the call sites don't have to know whether the underlying module
187
+ // ships ESM or CJS-with-default.
188
+ return { default: asyncStorageModule };
189
+ }
190
+
191
+ /**
192
+ * Synchronous random-bytes via `expo-crypto.getRandomBytes`.
193
+ *
194
+ * Synchronous by contract: `@oxy.so/core`'s crypto polyfill uses this to back
195
+ * `globalThis.crypto.getRandomValues`, which cannot await. That is why
196
+ * `expo-crypto` is resolved with a synchronous `require` at module scope rather
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
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Shared identity bridge — `@oxy.so/expo-oxy-identity` (native-only, OPTIONAL).
212
+ //
213
+ // `@oxy.so/expo-oxy-identity` is the in-repo Expo module autolinked into the
214
+ // identity apps (Commons + the reader RPs). We resolve its NATIVE module
215
+ // directly via expo-modules-core's `requireOptionalNativeModule('OxyIdentity')`
216
+ // — a static import Metro always resolves — instead of dynamically importing the
217
+ // module's JS wrapper. A runtime-computed `import(moduleName)` compiled to a
218
+ // `require(variable)` in the CJS build, which Metro cannot resolve in a consuming
219
+ // repo (the bridge silently resolved `null` there — the cross-app SSO bug). Since
220
+ // the native module is what actually holds the shared identity, going through the
221
+ // native registry is both correct and Metro-safe. `requireOptionalNativeModule`
222
+ // returns `null` (never throws) when the module is not autolinked (web, or apps
223
+ // that don't ship it), so `@oxy.so/core`'s `KeyManager` cleanly falls back to its
224
+ // package-private store.
225
+ // ---------------------------------------------------------------------------
226
+
227
+ let sharedIdentityBridgePromise: Promise<SharedIdentityBridge | null> | null = null;
228
+
229
+ export function loadSharedIdentityBridge(): Promise<SharedIdentityBridge | null> {
230
+ if (!sharedIdentityBridgePromise) {
231
+ sharedIdentityBridgePromise = Promise.resolve().then(() => {
232
+ const native = requireOptionalNativeModule<Partial<SharedIdentityBridge>>('OxyIdentity');
233
+ if (
234
+ native &&
235
+ typeof native.getShared === 'function' &&
236
+ typeof native.putShared === 'function' &&
237
+ typeof native.hasShared === 'function' &&
238
+ typeof native.clearShared === 'function'
239
+ ) {
240
+ return {
241
+ getShared: native.getShared.bind(native),
242
+ putShared: native.putShared.bind(native),
243
+ hasShared: native.hasShared.bind(native),
244
+ clearShared: native.clearShared.bind(native),
245
+ } satisfies SharedIdentityBridge;
246
+ }
247
+ return null;
248
+ });
249
+ }
250
+ return sharedIdentityBridgePromise;
251
+ }
@@ -0,0 +1,172 @@
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
+ * - `crypto.js` — this file. Used by Node.js, Vite, webpack,
12
+ * Rollup, esbuild, and anything that does not match
13
+ * Metro's `*.native.js` source-extension preference.
14
+ * - `crypto.native.js` — sibling file. Picked up automatically by Metro's
15
+ * resolver (which prefers `*.<platform>.js` and
16
+ * `*.native.js` over plain `*.js` when
17
+ * `preferNativePlatform` is true — Expo sets this for
18
+ * all non-web builds).
19
+ *
20
+ * The `package.json#exports` map also declares a `"react-native"` condition
21
+ * pointing at the same `dist/esm/index.js` entry — that entry transitively
22
+ * imports `./platform/crypto`, and Metro's per-file source-extension lookup
23
+ * substitutes the `.native.js` sibling automatically inside `dist/`. The
24
+ * package's top-level `"react-native"` map additionally pins the built
25
+ * `platform/crypto.js` (under both `dist/cjs` and `dist/esm`) to its
26
+ * `crypto.native.js` sibling belt-and-braces. This means consumers never have
27
+ * 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 │ optional `require('expo- │
41
+ * │ │ is not part of a │ crypto')` │
42
+ * │ │ Node/Vite bundle │ │
43
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
44
+ * │ loadSecureStore │ throws (web/Node have │ optional `require('expo- │
45
+ * │ │ their own storage) │ secure-store')` │
46
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
47
+ * │ loadAsyncStorage │ throws (web/Node have │ optional `require('@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, each behind
59
+ * Metro's optional-dependency mechanism (a literal `require()` inside a `try`)
60
+ * because they are declared OPTIONAL peer dependencies. It never mentions
61
+ * `'crypto'` — so Metro and Hermes have nothing to choke on.
62
+ *
63
+ * # Why not a single file with dynamic import?
64
+ *
65
+ * A previous iteration used a "bundler-opaque" `new Function('s', 'return
66
+ * import(s)')` trick so a single file could service every platform. It
67
+ * bundled cleanly on Metro but Hermes refused to PARSE the resulting
68
+ * `import()` expression inside a Function-constructor body
69
+ * (`SyntaxError: Invalid expression encountered` at the `(` of `import(`).
70
+ * The platform-extension split is the only approach that lets each runtime
71
+ * see a file containing only specifiers it can understand — no tricks, no
72
+ * runtime parsing risks.
73
+ */
74
+
75
+ import { isReactNative } from './platform';
76
+ import type { ExpoCryptoLike, ExpoSecureStoreLike, SharedIdentityBridge } from './expoTypes';
77
+
78
+ // Re-export the interfaces so consumers can import them from the same
79
+ // entry-point they use for the loaders.
80
+ export type { ExpoCryptoLike, ExpoSecureStoreLike, SharedIdentityBridge };
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Node `crypto` — Node built-in
84
+ //
85
+ // `await import('crypto')` here is a real, static-from-tsc's-perspective
86
+ // dynamic import. Node ESM, Vite, webpack, and esbuild all resolve it fine.
87
+ // Metro never sees this file because the `.native.js` sibling shadows
88
+ // it, so Metro never tries to resolve `'crypto'`.
89
+ // ---------------------------------------------------------------------------
90
+
91
+ let cachedNodeCrypto: typeof import('crypto') | null = null;
92
+
93
+ export async function loadNodeCrypto(): Promise<typeof import('crypto')> {
94
+ if (cachedNodeCrypto) {
95
+ return cachedNodeCrypto;
96
+ }
97
+ cachedNodeCrypto = await import('node:crypto');
98
+ return cachedNodeCrypto;
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // RN-only modules — never called from this variant.
103
+ //
104
+ // These throw a clear error if anything ever reaches them outside RN. In
105
+ // practice every caller gates with `isReactNative()` before calling, so
106
+ // these are belt-and-braces.
107
+ //
108
+ // Return types use the structural interfaces from expoTypes.ts rather than
109
+ // `typeof import('expo-crypto')` / `typeof import('expo-secure-store')`.
110
+ // This prevents TypeScript from traversing into expo-modules-core under
111
+ // NodeNext module resolution, which would otherwise pollute the global type
112
+ // environment in server/Node consumers (TS2322 on NodeJS.Timeout vs number).
113
+ // ---------------------------------------------------------------------------
114
+
115
+ function notReactNativeError(module: string): Error {
116
+ return new Error(
117
+ `[oxy.protocol.crypto] 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.`,
118
+ );
119
+ }
120
+
121
+ export async function loadExpoCrypto(): Promise<ExpoCryptoLike> {
122
+ if (isReactNative()) {
123
+ // Should be unreachable: when running on RN, Metro / the `react-native`
124
+ // exports condition serves the sibling variant. If we got here, the
125
+ // package-exports map is misconfigured for this host. Throw with a
126
+ // helpful diagnostic rather than fall back to a broken dynamic import.
127
+ throw new Error(
128
+ '[oxy.protocol.crypto] React Native runtime resolved the default ' +
129
+ '(non-RN) variant of @oxy.so/protocol/platform/crypto. Check the ' +
130
+ "consumer's bundler resolution — Metro should pick the sibling " +
131
+ '.native.js file via package exports.',
132
+ );
133
+ }
134
+ throw notReactNativeError('expo-crypto');
135
+ }
136
+
137
+ export async function loadSecureStore(): Promise<ExpoSecureStoreLike> {
138
+ throw notReactNativeError('expo-secure-store');
139
+ }
140
+
141
+ export async function loadAsyncStorage(): Promise<{
142
+ default: {
143
+ getItem: (key: string) => Promise<string | null>;
144
+ setItem: (key: string, value: string) => Promise<void>;
145
+ removeItem: (key: string) => Promise<void>;
146
+ };
147
+ }> {
148
+ throw notReactNativeError('@react-native-async-storage/async-storage');
149
+ }
150
+
151
+ /**
152
+ * Synchronous random-bytes via `expo-crypto.getRandomBytes`. Only available
153
+ * in the React Native variant. The default variant throws because Node and
154
+ * browsers have their own native CSPRNGs (`crypto.randomBytes` and
155
+ * `crypto.getRandomValues` respectively) — callers should use those.
156
+ */
157
+ export function getRandomBytesRN(_byteCount: number): Uint8Array {
158
+ throw notReactNativeError('expo-crypto.getRandomBytes (sync)');
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Shared identity bridge — `@oxy.so/expo-oxy-identity` (native-only).
163
+ //
164
+ // The default (web / Node) variant has no cross-app identity channel, so this
165
+ // always resolves to `null`. `@oxy.so/core`'s `KeyManager` treats `null` as "no
166
+ // bridge" and falls back to its package-private store — which is correct on web
167
+ // (there is no shared identity there).
168
+ // ---------------------------------------------------------------------------
169
+
170
+ export function loadSharedIdentityBridge(): Promise<SharedIdentityBridge | null> {
171
+ return Promise.resolve(null);
172
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Structural interfaces for Expo platform modules.
3
+ *
4
+ * These replace `typeof import('expo-crypto')` and
5
+ * `typeof import('expo-secure-store')` in the built declaration files of
6
+ * `@oxy.so/protocol` and `@oxy.so/core`.
7
+ *
8
+ * ## Why structural interfaces instead of `typeof import('expo-*')`?
9
+ *
10
+ * Under NodeNext module resolution (used by `@oxy.so/api` and `@oxy.so/node`),
11
+ * `expo-crypto` ships with `"exports": {}` (empty exports map). TypeScript
12
+ * traverses into the package anyway via the `types` field, which transitively
13
+ * loads `expo-modules-core`. That pollution makes `setInterval`/`setTimeout`
14
+ * resolve to DOM's `number` return type rather than Node's `NodeJS.Timeout`,
15
+ * producing ~10 spurious `TS2322` / `TS2339` errors in every consumer that
16
+ * uses Node timer APIs — none of which reference protocol types at all.
17
+ *
18
+ * Structural interfaces break the transitive expo-modules-core dependency
19
+ * entirely: consumers that don't have Expo installed see clean types, and
20
+ * the actual RN runtime (which DOES have Expo installed) still works because
21
+ * the real modules satisfy these interfaces structurally.
22
+ */
23
+
24
+ /**
25
+ * Minimal structural interface for the subset of `expo-crypto` used by
26
+ * `@oxy.so/protocol` (SHA-256 hashing in RN) and `@oxy.so/core` (key-manager
27
+ * random-byte generation).
28
+ *
29
+ * The real `expo-crypto` namespace satisfies this interface structurally.
30
+ */
31
+ export interface ExpoCryptoLike {
32
+ /** Generate `byteCount` cryptographically-random bytes synchronously. */
33
+ getRandomBytes(byteCount: number): Uint8Array;
34
+ /** Generate `byteCount` cryptographically-random bytes asynchronously. */
35
+ getRandomBytesAsync(byteCount: number): Promise<Uint8Array>;
36
+ /**
37
+ * Compute a digest of `data` using the given `algorithm` string
38
+ * (e.g. `CryptoDigestAlgorithm.SHA256`). Used by `recordId.ts` in the
39
+ * React Native runtime path for content-address hashing.
40
+ */
41
+ digestStringAsync(algorithm: string, data: string, options?: unknown): Promise<string>;
42
+ /**
43
+ * Algorithm constants (e.g. `CryptoDigestAlgorithm.SHA256 === 'SHA-256'`).
44
+ * Represented as a plain string-keyed record so the interface does not
45
+ * depend on the enum definition inside expo-crypto.
46
+ */
47
+ readonly CryptoDigestAlgorithm: Record<string, string>;
48
+ }
49
+
50
+ /**
51
+ * Minimal structural interface for the subset of `expo-secure-store` used by
52
+ * `@oxy.so/core` `KeyManager` for on-device identity storage.
53
+ *
54
+ * The real `expo-secure-store` namespace satisfies this interface structurally.
55
+ *
56
+ * `options` are typed as `object` (rather than the concrete `SecureStoreOptions`
57
+ * from expo-secure-store) so callers can pass any plain options bag without
58
+ * importing expo-secure-store's type declarations. TypeScript method bivariance
59
+ * makes the real `setItemAsync(opts?: SecureStoreOptions)` compatible with this
60
+ * `setItemAsync(opts?: object)` signature.
61
+ */
62
+ export interface ExpoSecureStoreLike {
63
+ setItemAsync(key: string, value: string, options?: object): Promise<void>;
64
+ getItemAsync(key: string, options?: object): Promise<string | null>;
65
+ deleteItemAsync(key: string, options?: object): Promise<void>;
66
+ /**
67
+ * Keychain / Keystore accessibility constant: item accessible only when the
68
+ * device is unlocked, and only on this device (no iCloud backup).
69
+ * Value: `KeychainAccessibilityConstant` (a number alias in expo-secure-store).
70
+ */
71
+ readonly WHEN_UNLOCKED_THIS_DEVICE_ONLY: number;
72
+ /**
73
+ * Keychain / Keystore accessibility constant: item accessible whenever the
74
+ * device is unlocked (may be restored to a different device via backup).
75
+ */
76
+ readonly WHEN_UNLOCKED: number;
77
+ }
78
+
79
+ /**
80
+ * Structural interface for the `@oxy.so/expo-oxy-identity` native module — the
81
+ * cross-app shared Oxy identity bridge.
82
+ *
83
+ * On Android the keypair crosses the process boundary through a
84
+ * signature-protected `ContentProvider` hosted by Commons; on iOS every method
85
+ * is a no-op (the Keychain Access Group path in `@oxy.so/core`'s `KeyManager`
86
+ * owns iOS sharing). Typed structurally here so `@oxy.so/protocol` and
87
+ * `@oxy.so/core` can reference the bridge without a hard dependency on the
88
+ * optional native module.
89
+ */
90
+ export interface SharedIdentityBridge {
91
+ /** Read the shared keypair, or null when none is available on this device. */
92
+ getShared(): Promise<{ privateKey: string; publicKey: string } | null>;
93
+ /** Persist the shared keypair into this app's hardware-backed store (Commons only). */
94
+ putShared(privateKey: string, publicKey: string): Promise<void>;
95
+ /** Whether a shared identity is readable on this device. */
96
+ hasShared(): Promise<boolean>;
97
+ /** Remove the shared identity from this app's local store (best-effort). */
98
+ clearShared(): Promise<void>;
99
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Platform Detection — runtime predicates.
3
+ *
4
+ * Detects the host runtime WITHOUT importing from 'react-native', so the
5
+ * protocol's crypto modules can be used in web / Node.js / React Native
6
+ * environments without bundlers failing on react-native imports.
7
+ *
8
+ * Only the two predicates the protocol's platform-crypto loaders need live
9
+ * here. Richer platform detection (`getPlatformOS`, `isWeb`, `isNative`, …)
10
+ * is an SDK concern and stays in `@oxy.so/core`.
11
+ */
12
+
13
+ /**
14
+ * Check if running in React Native.
15
+ *
16
+ * Selects the React Native crypto variant (`expo-crypto` /
17
+ * `expo-secure-store` / async-storage) over the Node/web variant.
18
+ */
19
+ export function isReactNative(): boolean {
20
+ return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
21
+ }
22
+
23
+ /**
24
+ * Check if running in Node.js.
25
+ *
26
+ * Gates use of Node's built-in `crypto` (the synchronous SHA-256 path and
27
+ * `randomBytes`) and the `await import('node:crypto')` loader.
28
+ */
29
+ export function isNodeJS(): boolean {
30
+ return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
31
+ }