@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,187 @@
1
+ "use strict";
2
+ /**
3
+ * A small, dependency-free fixed-window per-IP rate limiter for the node app's
4
+ * owner-authorized write routes.
5
+ *
6
+ * The node is a single-writer model (only the owner key may write), so the
7
+ * limiter is a defence-in-depth budget on the unauthenticated edge — it caps the
8
+ * request rate BEFORE signature verification so a flood of bogus envelopes can't
9
+ * pin CPU on crypto. It is intentionally process-local (a single node serves one
10
+ * owner's repo); there is no shared store to coordinate.
11
+ *
12
+ * Fixed-window counting, one budget per client: each key gets `max` requests per
13
+ * `windowMs`, and the window resets lazily on the first request after it elapses.
14
+ * The key is a SALTED HASH of the client address, never the address itself — see
15
+ * {@link clientRateLimitKey}.
16
+ *
17
+ * Bounded memory (defence against a key-rotation DoS — spoofed IPs / many DIDs
18
+ * growing the map without limit → memory exhaustion):
19
+ * - An ACTIVE periodic sweep on an `unref()`'d interval deletes every entry
20
+ * whose window has fully elapsed, so keys that are never touched again do not
21
+ * leak forever (lazy expiry-on-access alone cannot reclaim them). The
22
+ * interval is `unref()`'d so it never keeps the node process alive, and
23
+ * {@link RateLimiter.stop} clears it for a clean lifecycle teardown.
24
+ * - A hard cap on the number of tracked keys ({@link RateLimitConfig.maxEntries})
25
+ * evicts the OLDEST window (insertion-order LRU) when exceeded — a synchronous
26
+ * backstop against a burst that arrives between sweeps.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.DEFAULT_MAX_RATE_LIMIT_ENTRIES = exports.DEFAULT_WRITE_RATE_LIMIT = void 0;
30
+ exports.clientRateLimitKey = clientRateLimitKey;
31
+ exports.createRateLimiter = createRateLimiter;
32
+ const node_crypto_1 = require("node:crypto");
33
+ // The package's `lib` includes `DOM` (the isomorphic root code uses Web Crypto),
34
+ // so the ambient `setInterval` overload TypeScript picks for a bare call is the
35
+ // browser one returning `number` — which has no `.unref()`. This `node/` subpath
36
+ // is Node-only; reach the Node timer globals through their `@types/node`
37
+ // signatures so the handle is correctly `NodeJS.Timeout` (no cast, no shadowing).
38
+ // Resolved at call time (not captured at module load) so test fake-timers that
39
+ // swap the globals still drive the sweep.
40
+ function nodeSetInterval(handler, ms) {
41
+ const set = globalThis.setInterval;
42
+ return set(handler, ms);
43
+ }
44
+ function nodeClearInterval(timer) {
45
+ const clear = globalThis.clearInterval;
46
+ clear(timer);
47
+ }
48
+ /**
49
+ * The HMAC key under which client addresses are hashed into rate-limit keys.
50
+ * 256 CSPRNG bits, minted once when this module is first loaded and held only in
51
+ * memory — never read from a config, never written anywhere, never sent.
52
+ *
53
+ * ## Why the salt is deliberately EPHEMERAL, and what a stable one would cost
54
+ *
55
+ * A rate-limit window is short-lived (`windowMs`, seconds to a minute) and this
56
+ * limiter is process-local by design — a node serves one owner's repo and there
57
+ * is no shared store to coordinate. So nothing here needs a key to mean the same
58
+ * thing after a restart, or to mean the same thing on another node. That makes a
59
+ * per-process salt not merely sufficient but BETTER than a configured one:
60
+ *
61
+ * - there is no value to distribute, so there is nothing for a node operator to
62
+ * get wrong, nothing to rotate, and nothing to leak from an env file, a
63
+ * process listing or a container image;
64
+ * - the mapping dies with the process, so the same address hashes to a
65
+ * different key after every restart and the keys correlate to nothing once
66
+ * the process exits.
67
+ *
68
+ * A stable salt (an env var, a file) would buy exactly one thing this limiter
69
+ * does not want — a client identifier that survives a restart and can be compared
70
+ * across nodes — in exchange for a config burden and a secret at rest. That is
71
+ * the trade, and it is why this is not configurable.
72
+ *
73
+ * ## What the hash does and does not buy, stated honestly
74
+ *
75
+ * It removes the raw address from the process's data structures: the limiter's
76
+ * Map holds digests, so an address is no longer sitting in memory as a key for
77
+ * the lifetime of a window, and nothing downstream can casually read one back
78
+ * out. What it does NOT claim is secrecy against an attacker who already has the
79
+ * live process — the salt is in the same heap, and with it the IPv4 space is
80
+ * enumerable. That is the general reason hashing is not an acceptable AT-REST
81
+ * form for an address anywhere in Oxy; this value is never at rest.
82
+ */
83
+ const CLIENT_KEY_SALT = (0, node_crypto_1.randomBytes)(32);
84
+ /**
85
+ * The rate-limit key for a request: a salted hash of the client address, or the
86
+ * `'unknown'` sentinel when Express resolved no address at all (a request whose
87
+ * address is unknown cannot be budgeted individually, so all of them share one
88
+ * bucket — the same behaviour this limiter has always had).
89
+ *
90
+ * Truncated to 96 bits, which keeps a tracked entry to a short string beside its
91
+ * two numbers (the memory-bounding rationale on {@link RateLimitConfig.maxEntries}
92
+ * assumes exactly that). At the 10 000-entry cap a collision — two clients
93
+ * sharing one budget — has probability around 10⁸/2⁹⁷, i.e. never.
94
+ *
95
+ * Residue, named rather than left implicit: the address is hashed VERBATIM, so an
96
+ * IPv6 client that rotates through its /64 still mints a fresh key per address,
97
+ * exactly as it did before this was hashed. oxy-api's `hashedIpKey` buckets IPv6
98
+ * to /56 first to close that; doing the same here is a rate-limiting change with
99
+ * its own reasoning (it makes a whole prefix share one budget) and is deliberately
100
+ * not folded into a privacy fix.
101
+ *
102
+ * Exported for {@link createRateLimiter}'s own tests, not part of
103
+ * `@oxy.so/protocol/node`'s public surface — it is not re-exported by the barrel.
104
+ */
105
+ function clientRateLimitKey(req) {
106
+ const ip = req.ip;
107
+ if (!ip) {
108
+ return 'unknown';
109
+ }
110
+ // Single-purpose salt: it derives this key and nothing else, so there is no
111
+ // second derivation to namespace against (oxy-api's `hashedIpKey` prefixes
112
+ // `rl|` because its salt is shared with deviceId derivation). A future second
113
+ // use of this salt would need a namespace, or its own salt.
114
+ return (0, node_crypto_1.createHmac)('sha256', CLIENT_KEY_SALT).update(ip).digest('hex').slice(0, 24);
115
+ }
116
+ /** Default budget for owner write routes (generous — single-writer model). */
117
+ exports.DEFAULT_WRITE_RATE_LIMIT = { windowMs: 60000, max: 60 };
118
+ /**
119
+ * Default hard cap on tracked keys. Sized so the map's worst-case footprint
120
+ * stays small (each entry is a short string key + two numbers) while never
121
+ * evicting a legitimately active key for the single-writer node — the owner
122
+ * drives traffic from a handful of IPs, far below this ceiling.
123
+ */
124
+ exports.DEFAULT_MAX_RATE_LIMIT_ENTRIES = 10000;
125
+ /**
126
+ * Build an Express middleware enforcing a fixed-window per-client rate limit.
127
+ * Exceeding the budget responds `429 { error: 'rate_limited' }` and does not call
128
+ * `next`. The key is {@link clientRateLimitKey} — a salted hash of Express's
129
+ * resolved client IP, so no address is held in the tracked map.
130
+ *
131
+ * The returned middleware owns a background sweep timer; call {@link RateLimiter.stop}
132
+ * to release it (e.g. on app shutdown).
133
+ */
134
+ function createRateLimiter(config) {
135
+ const windows = new Map();
136
+ const maxEntries = config.maxEntries ?? exports.DEFAULT_MAX_RATE_LIMIT_ENTRIES;
137
+ // Active sweep: delete every entry whose window has fully elapsed. Running on
138
+ // a timer (rather than only on request arrival) reclaims keys that are never
139
+ // touched again, so a churn of distinct IPs cannot leak memory once traffic
140
+ // for those keys stops. One pass per window is sufficient: an entry lives at
141
+ // most `2 * windowMs` before a sweep removes it.
142
+ function sweepExpired() {
143
+ const now = Date.now();
144
+ for (const [key, counter] of windows) {
145
+ if (now - counter.start >= config.windowMs) {
146
+ windows.delete(key);
147
+ }
148
+ }
149
+ }
150
+ const sweepTimer = nodeSetInterval(sweepExpired, config.windowMs);
151
+ // Never let the sweep keep the node process alive on its own.
152
+ sweepTimer.unref();
153
+ function rateLimit(req, res, next) {
154
+ const now = Date.now();
155
+ const key = clientRateLimitKey(req);
156
+ const counter = windows.get(key);
157
+ if (!counter || now - counter.start >= config.windowMs) {
158
+ // Hard cap backstop: if a burst of distinct keys outran the sweep, evict
159
+ // the oldest-inserted window before admitting a new key. A `Map` preserves
160
+ // insertion order, so its first key is the oldest tracked entry.
161
+ if (!counter && windows.size >= maxEntries) {
162
+ const oldest = windows.keys().next().value;
163
+ if (oldest !== undefined) {
164
+ windows.delete(oldest);
165
+ }
166
+ }
167
+ windows.set(key, { start: now, count: 1 });
168
+ next();
169
+ return;
170
+ }
171
+ if (counter.count >= config.max) {
172
+ const retryAfterSec = Math.ceil((counter.start + config.windowMs - now) / 1000);
173
+ res.setHeader('Retry-After', String(Math.max(retryAfterSec, 1)));
174
+ res.status(429).json({ error: 'rate_limited' });
175
+ return;
176
+ }
177
+ counter.count += 1;
178
+ next();
179
+ }
180
+ // Attach the lifecycle hook to the middleware, yielding the `RateLimiter`
181
+ // callable-with-`stop` without a cast.
182
+ return Object.assign(rateLimit, {
183
+ stop() {
184
+ nodeClearInterval(sweepTimer);
185
+ },
186
+ });
187
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Record verification for a data node — reuses the protocol envelope primitives
4
+ * so a record verifies on the node with the EXACT code Oxy uses. No crypto is
5
+ * re-implemented:
6
+ *
7
+ * - {@link verifyEnvelopeSignature} recomputes the canonical signing input (the
8
+ * bytes the signature covers) from the envelope's own fields and checks the
9
+ * secp256k1 DER signature against the envelope's embedded `publicKey`.
10
+ * - {@link computeRecordId} recomputes `recordId = sha256(signingInput)` — the
11
+ * content address used as the chain's `prev` pointer.
12
+ *
13
+ * The envelope shape is validated with the shared `signedRecordEnvelopeSchema`.
14
+ * A node is a v2 hash chain, so only v2 envelopes (carrying
15
+ * `seq`/`prev`/`collection`/`rkey`) are accepted; v1 singletons have no chain
16
+ * coordinates.
17
+ *
18
+ * Verification here proves the signature is internally consistent with the
19
+ * embedded `publicKey`. Whether that key is authorized for the node is the OWNER
20
+ * check (the injected owner-key authority) — on a node the authority is the
21
+ * configured owner public key, not a DID lookup.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.verifyNodeRecordEnvelope = verifyNodeRecordEnvelope;
25
+ const contracts_1 = require("@oxy.so/contracts");
26
+ const recordId_1 = require("../envelope/recordId");
27
+ const sign_1 = require("../envelope/sign");
28
+ /**
29
+ * Validate, signature-check, and content-address a candidate signed record for a
30
+ * v2 hash-chain node.
31
+ *
32
+ * On success the parsed envelope and its `recordId` are returned; the caller
33
+ * (the node app) still enforces owner authority and chain continuity before the
34
+ * record is appended.
35
+ */
36
+ async function verifyNodeRecordEnvelope(input) {
37
+ const parsed = contracts_1.signedRecordEnvelopeSchema.safeParse(input);
38
+ if (!parsed.success) {
39
+ return { ok: false, reason: 'invalid_envelope' };
40
+ }
41
+ const envelope = parsed.data;
42
+ if (envelope.version !== 2) {
43
+ return { ok: false, reason: 'not_v2' };
44
+ }
45
+ const signatureValid = await (0, sign_1.verifyEnvelopeSignature)(envelope);
46
+ if (!signatureValid) {
47
+ return { ok: false, reason: 'bad_signature' };
48
+ }
49
+ const recordId = await (0, recordId_1.computeRecordId)(envelope);
50
+ return { ok: true, envelope, recordId };
51
+ }
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ /**
3
+ * Platform Crypto / Storage — Default Variant (Node.js, Browser, generic bundlers)
4
+ *
5
+ * Provides lazy access to platform-specific crypto and storage modules.
6
+ *
7
+ * # Variants
8
+ *
9
+ * This module ships in two physical variants on disk, selected per consumer
10
+ * by the bundler / runtime:
11
+ *
12
+ * - `crypto.js` — this file. Used by Node.js, Vite, webpack,
13
+ * Rollup, esbuild, and anything that does not match
14
+ * Metro's `*.native.js` source-extension preference.
15
+ * - `crypto.native.js` — sibling file. Picked up automatically by Metro's
16
+ * resolver (which prefers `*.<platform>.js` and
17
+ * `*.native.js` over plain `*.js` when
18
+ * `preferNativePlatform` is true — Expo sets this for
19
+ * all non-web builds).
20
+ *
21
+ * The `package.json#exports` map also declares a `"react-native"` condition
22
+ * pointing at the same `dist/esm/index.js` entry — that entry transitively
23
+ * imports `./platform/crypto`, and Metro's per-file source-extension lookup
24
+ * substitutes the `.native.js` sibling automatically inside `dist/`. The
25
+ * package's top-level `"react-native"` map additionally pins the built
26
+ * `platform/crypto.js` (under both `dist/cjs` and `dist/esm`) to its
27
+ * `crypto.native.js` sibling belt-and-braces. This means consumers never have
28
+ * to add resolver shims; Metro Just Works.
29
+ *
30
+ * Both variants expose the EXACT same public API; importers don't need to know
31
+ * which one they got. The variant difference is purely about which underlying
32
+ * native modules each one references:
33
+ *
34
+ * ┌──────────────────┬───────────────────────┬───────────────────────────────┐
35
+ * │ Function │ Default variant │ React Native variant │
36
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
37
+ * │ loadNodeCrypto │ `await import('crypto')` (Node built-in) │
38
+ * │ │ │ throws — Node crypto is not │
39
+ * │ │ │ available on Hermes/RN │
40
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
41
+ * │ loadExpoCrypto │ throws — expo-crypto │ optional `require('expo- │
42
+ * │ │ is not part of a │ crypto')` │
43
+ * │ │ Node/Vite bundle │ │
44
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
45
+ * │ loadSecureStore │ throws (web/Node have │ optional `require('expo- │
46
+ * │ │ their own storage) │ secure-store')` │
47
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
48
+ * │ loadAsyncStorage │ throws (web/Node have │ optional `require('@react- │
49
+ * │ │ their own storage) │ native-async-storage/...')` │
50
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
51
+ * │ getRandomBytesRN │ throws (RN-only) │ direct call into expo-crypto │
52
+ * └──────────────────┴───────────────────────┴───────────────────────────────┘
53
+ *
54
+ * Crucially, the default variant references ONLY Node's `'crypto'`. It never
55
+ * mentions `expo-*` or `@react-native-async-storage/*` — so Vite, webpack,
56
+ * esbuild, Rollup, and Node itself can bundle / require it without ever
57
+ * attempting to resolve those RN-only packages.
58
+ *
59
+ * The React Native variant references ONLY the RN packages, each behind
60
+ * Metro's optional-dependency mechanism (a literal `require()` inside a `try`)
61
+ * because they are declared OPTIONAL peer dependencies. It never mentions
62
+ * `'crypto'` — so Metro and Hermes have nothing to choke on.
63
+ *
64
+ * # Why not a single file with dynamic import?
65
+ *
66
+ * A previous iteration used a "bundler-opaque" `new Function('s', 'return
67
+ * import(s)')` trick so a single file could service every platform. It
68
+ * bundled cleanly on Metro but Hermes refused to PARSE the resulting
69
+ * `import()` expression inside a Function-constructor body
70
+ * (`SyntaxError: Invalid expression encountered` at the `(` of `import(`).
71
+ * The platform-extension split is the only approach that lets each runtime
72
+ * see a file containing only specifiers it can understand — no tricks, no
73
+ * runtime parsing risks.
74
+ */
75
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
76
+ if (k2 === undefined) k2 = k;
77
+ var desc = Object.getOwnPropertyDescriptor(m, k);
78
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
79
+ desc = { enumerable: true, get: function() { return m[k]; } };
80
+ }
81
+ Object.defineProperty(o, k2, desc);
82
+ }) : (function(o, m, k, k2) {
83
+ if (k2 === undefined) k2 = k;
84
+ o[k2] = m[k];
85
+ }));
86
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
87
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
88
+ }) : function(o, v) {
89
+ o["default"] = v;
90
+ });
91
+ var __importStar = (this && this.__importStar) || (function () {
92
+ var ownKeys = function(o) {
93
+ ownKeys = Object.getOwnPropertyNames || function (o) {
94
+ var ar = [];
95
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
96
+ return ar;
97
+ };
98
+ return ownKeys(o);
99
+ };
100
+ return function (mod) {
101
+ if (mod && mod.__esModule) return mod;
102
+ var result = {};
103
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
104
+ __setModuleDefault(result, mod);
105
+ return result;
106
+ };
107
+ })();
108
+ Object.defineProperty(exports, "__esModule", { value: true });
109
+ exports.loadNodeCrypto = loadNodeCrypto;
110
+ exports.loadExpoCrypto = loadExpoCrypto;
111
+ exports.loadSecureStore = loadSecureStore;
112
+ exports.loadAsyncStorage = loadAsyncStorage;
113
+ exports.getRandomBytesRN = getRandomBytesRN;
114
+ exports.loadSharedIdentityBridge = loadSharedIdentityBridge;
115
+ const platform_1 = require("./platform");
116
+ // ---------------------------------------------------------------------------
117
+ // Node `crypto` — Node built-in
118
+ //
119
+ // `await import('crypto')` here is a real, static-from-tsc's-perspective
120
+ // dynamic import. Node ESM, Vite, webpack, and esbuild all resolve it fine.
121
+ // Metro never sees this file because the `.native.js` sibling shadows
122
+ // it, so Metro never tries to resolve `'crypto'`.
123
+ // ---------------------------------------------------------------------------
124
+ let cachedNodeCrypto = null;
125
+ async function loadNodeCrypto() {
126
+ if (cachedNodeCrypto) {
127
+ return cachedNodeCrypto;
128
+ }
129
+ cachedNodeCrypto = await Promise.resolve().then(() => __importStar(require('node:crypto')));
130
+ return cachedNodeCrypto;
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // RN-only modules — never called from this variant.
134
+ //
135
+ // These throw a clear error if anything ever reaches them outside RN. In
136
+ // practice every caller gates with `isReactNative()` before calling, so
137
+ // these are belt-and-braces.
138
+ //
139
+ // Return types use the structural interfaces from expoTypes.ts rather than
140
+ // `typeof import('expo-crypto')` / `typeof import('expo-secure-store')`.
141
+ // This prevents TypeScript from traversing into expo-modules-core under
142
+ // NodeNext module resolution, which would otherwise pollute the global type
143
+ // environment in server/Node consumers (TS2322 on NodeJS.Timeout vs number).
144
+ // ---------------------------------------------------------------------------
145
+ function notReactNativeError(module) {
146
+ return new Error(`[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.`);
147
+ }
148
+ async function loadExpoCrypto() {
149
+ if ((0, platform_1.isReactNative)()) {
150
+ // Should be unreachable: when running on RN, Metro / the `react-native`
151
+ // exports condition serves the sibling variant. If we got here, the
152
+ // package-exports map is misconfigured for this host. Throw with a
153
+ // helpful diagnostic rather than fall back to a broken dynamic import.
154
+ throw new Error('[oxy.protocol.crypto] React Native runtime resolved the default ' +
155
+ '(non-RN) variant of @oxy.so/protocol/platform/crypto. Check the ' +
156
+ "consumer's bundler resolution — Metro should pick the sibling " +
157
+ '.native.js file via package exports.');
158
+ }
159
+ throw notReactNativeError('expo-crypto');
160
+ }
161
+ async function loadSecureStore() {
162
+ throw notReactNativeError('expo-secure-store');
163
+ }
164
+ async function loadAsyncStorage() {
165
+ throw notReactNativeError('@react-native-async-storage/async-storage');
166
+ }
167
+ /**
168
+ * Synchronous random-bytes via `expo-crypto.getRandomBytes`. Only available
169
+ * in the React Native variant. The default variant throws because Node and
170
+ * browsers have their own native CSPRNGs (`crypto.randomBytes` and
171
+ * `crypto.getRandomValues` respectively) — callers should use those.
172
+ */
173
+ function getRandomBytesRN(_byteCount) {
174
+ throw notReactNativeError('expo-crypto.getRandomBytes (sync)');
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Shared identity bridge — `@oxy.so/expo-oxy-identity` (native-only).
178
+ //
179
+ // The default (web / Node) variant has no cross-app identity channel, so this
180
+ // always resolves to `null`. `@oxy.so/core`'s `KeyManager` treats `null` as "no
181
+ // bridge" and falls back to its package-private store — which is correct on web
182
+ // (there is no shared identity there).
183
+ // ---------------------------------------------------------------------------
184
+ function loadSharedIdentityBridge() {
185
+ return Promise.resolve(null);
186
+ }
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ /**
3
+ * Platform Crypto / Storage — React Native Variant
4
+ *
5
+ * Companion to `./crypto.ts`. See the doc-comment at the top of that file for
6
+ * the full design.
7
+ *
8
+ * Metro auto-selects this file in any non-web build (`preferNativePlatform`
9
+ * is `true` for iOS / Android, so `*.native.js` shadows `*.js` during
10
+ * source-extension resolution inside `node_modules/@oxy.so/protocol/dist/`). On
11
+ * iOS / Android `<base>.ios.js` / `<base>.android.js` would shadow this file
12
+ * if they existed, but they don't — `.native.js` is the shared RN variant.
13
+ *
14
+ * - The default variant references Node's `'crypto'` and would crash Metro
15
+ * if bundled into an RN app.
16
+ * - This variant references the RN-only modules (`expo-crypto`,
17
+ * `expo-secure-store`, `@react-native-async-storage/async-storage`),
18
+ * each behind Metro's optional-dependency mechanism (see below).
19
+ *
20
+ * Both variants expose the same surface; importers don't care which one
21
+ * they got.
22
+ *
23
+ * # Why `try { require('literal') } catch` and not a static import?
24
+ *
25
+ * Those three RN modules are declared OPTIONAL peer dependencies in
26
+ * `package.json`. A static `import` contradicts that: an optional peer that is
27
+ * omitted does not degrade, it fails to RESOLVE, and Metro aborts the whole
28
+ * bundle. Because `@oxy.so/core`'s `crypto/polyfill` imports `@oxy.so/protocol`
29
+ * from its root entry, this file is in the eager graph of EVERY React Native
30
+ * app on `@oxy.so/core` — so a single undeclared optional peer broke the native
31
+ * bundle of every app that did not happen to install it, with a resolution
32
+ * error pointing at a dependency the app never mentions.
33
+ *
34
+ * Metro treats a `require()` of a STRING LITERAL that sits inside a `try`
35
+ * block as an optional dependency: it resolves it when present, and when
36
+ * absent emits a stub that throws on evaluation instead of failing the build.
37
+ * The `catch` turns that into a `null` module handle, and the loader below
38
+ * throws an actionable error naming the missing package the first time the
39
+ * capability is actually used. Bundle-time hard failure becomes a
40
+ * capability-scoped runtime failure — which is exactly what "optional peer"
41
+ * is supposed to mean.
42
+ *
43
+ * Two constraints this shape has to respect, both learned the hard way:
44
+ *
45
+ * - The specifier MUST be a literal. A runtime-computed `require(variable)`
46
+ * is unresolvable for Metro (that is the bug the shared-identity bridge
47
+ * below documents) and silently yields nothing in a consuming repo.
48
+ * - The load MUST stay synchronous. `getRandomBytesRN` backs
49
+ * `globalThis.crypto.getRandomValues` in `@oxy.so/core`'s polyfill, which
50
+ * cannot await anything.
51
+ *
52
+ * `expo-modules-core` is a NON-optional peer (every RN app has it via `expo`),
53
+ * so it stays a plain static import.
54
+ */
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.loadNodeCrypto = loadNodeCrypto;
57
+ exports.loadExpoCrypto = loadExpoCrypto;
58
+ exports.loadSecureStore = loadSecureStore;
59
+ exports.loadAsyncStorage = loadAsyncStorage;
60
+ exports.getRandomBytesRN = getRandomBytesRN;
61
+ exports.loadSharedIdentityBridge = loadSharedIdentityBridge;
62
+ const expo_modules_core_1 = require("expo-modules-core");
63
+ let expoCryptoModule = null;
64
+ let expoCryptoError;
65
+ try {
66
+ expoCryptoModule = require('expo-crypto');
67
+ }
68
+ catch (error) {
69
+ expoCryptoError = error;
70
+ }
71
+ let secureStoreModule = null;
72
+ let secureStoreError;
73
+ try {
74
+ secureStoreModule = require('expo-secure-store');
75
+ }
76
+ catch (error) {
77
+ secureStoreError = error;
78
+ }
79
+ let asyncStorageModule = null;
80
+ let asyncStorageError;
81
+ try {
82
+ // Babel's default-import interop unwraps `.default` for us on a static
83
+ // import; a raw `require` has to do it by hand. The `?? namespace` fallback
84
+ // covers a host that hands back a real ESM namespace with no `default`.
85
+ const namespace = require('@react-native-async-storage/async-storage');
86
+ asyncStorageModule = namespace.default ?? namespace;
87
+ }
88
+ catch (error) {
89
+ asyncStorageError = error;
90
+ }
91
+ /**
92
+ * Actionable error for a missing optional peer. Carries the underlying Metro
93
+ * resolution message so the failure is never silent — the `catch` above only
94
+ * defers the report to the point where the capability is actually needed.
95
+ */
96
+ function missingOptionalPeerError(packageName, capability, cause) {
97
+ const sentences = [
98
+ `[oxy.protocol.crypto] '${packageName}' is not installed, so ${capability} is unavailable in this app.`,
99
+ 'It is an optional peer dependency of @oxy.so/protocol that the React Native runtime needs —',
100
+ `install it with \`npx expo install ${packageName}\`.`,
101
+ ];
102
+ if (cause instanceof Error) {
103
+ sentences.push(`Underlying error: ${cause.message}`);
104
+ }
105
+ return new Error(sentences.join(' '));
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // Node `crypto` — never available in RN.
109
+ // ---------------------------------------------------------------------------
110
+ async function loadNodeCrypto() {
111
+ // Unreachable in practice: every caller gates with `isNodeJS()` before
112
+ // invoking this. If it somehow does fire, throw immediately with a clear
113
+ // diagnostic rather than letting Metro / Hermes attempt to find a
114
+ // non-existent module at runtime.
115
+ throw new Error("[oxy.protocol.crypto] Node's built-in 'crypto' module is not available " +
116
+ 'in a React Native runtime. Use the RN-specific helpers ' +
117
+ '(loadExpoCrypto, getRandomBytesRN) or the Web Crypto API (`globalThis.crypto`).');
118
+ }
119
+ // ---------------------------------------------------------------------------
120
+ // expo-crypto — RN cryptographic primitives.
121
+ //
122
+ // The real module satisfies `ExpoCryptoLike` structurally; the structural
123
+ // interface narrows the surface so consumers never pull expo's own types into
124
+ // their compilation (see expoTypes.ts).
125
+ // ---------------------------------------------------------------------------
126
+ async function loadExpoCrypto() {
127
+ if (!expoCryptoModule) {
128
+ throw missingOptionalPeerError('expo-crypto', 'React Native cryptography', expoCryptoError);
129
+ }
130
+ return expoCryptoModule;
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // expo-secure-store — RN keychain / keystore.
134
+ // ---------------------------------------------------------------------------
135
+ async function loadSecureStore() {
136
+ if (!secureStoreModule) {
137
+ throw missingOptionalPeerError('expo-secure-store', 'on-device identity storage', secureStoreError);
138
+ }
139
+ return secureStoreModule;
140
+ }
141
+ // ---------------------------------------------------------------------------
142
+ // @react-native-async-storage/async-storage — RN persistent KV storage.
143
+ // ---------------------------------------------------------------------------
144
+ async function loadAsyncStorage() {
145
+ if (!asyncStorageModule) {
146
+ throw missingOptionalPeerError('@react-native-async-storage/async-storage', 'device/session persistence', asyncStorageError);
147
+ }
148
+ // Mirror the shape callers historically used (`module.default.<method>`)
149
+ // so the call sites don't have to know whether the underlying module
150
+ // ships ESM or CJS-with-default.
151
+ return { default: asyncStorageModule };
152
+ }
153
+ /**
154
+ * Synchronous random-bytes via `expo-crypto.getRandomBytes`.
155
+ *
156
+ * Synchronous by contract: `@oxy.so/core`'s crypto polyfill uses this to back
157
+ * `globalThis.crypto.getRandomValues`, which cannot await. That is why
158
+ * `expo-crypto` is resolved with a synchronous `require` at module scope rather
159
+ * than a dynamic `import()`.
160
+ */
161
+ function getRandomBytesRN(byteCount) {
162
+ if (!expoCryptoModule) {
163
+ throw missingOptionalPeerError('expo-crypto', 'the React Native CSPRNG (crypto.getRandomValues)', expoCryptoError);
164
+ }
165
+ return expoCryptoModule.getRandomBytes(byteCount);
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Shared identity bridge — `@oxy.so/expo-oxy-identity` (native-only, OPTIONAL).
169
+ //
170
+ // `@oxy.so/expo-oxy-identity` is the in-repo Expo module autolinked into the
171
+ // identity apps (Commons + the reader RPs). We resolve its NATIVE module
172
+ // directly via expo-modules-core's `requireOptionalNativeModule('OxyIdentity')`
173
+ // — a static import Metro always resolves — instead of dynamically importing the
174
+ // module's JS wrapper. A runtime-computed `import(moduleName)` compiled to a
175
+ // `require(variable)` in the CJS build, which Metro cannot resolve in a consuming
176
+ // repo (the bridge silently resolved `null` there — the cross-app SSO bug). Since
177
+ // the native module is what actually holds the shared identity, going through the
178
+ // native registry is both correct and Metro-safe. `requireOptionalNativeModule`
179
+ // returns `null` (never throws) when the module is not autolinked (web, or apps
180
+ // that don't ship it), so `@oxy.so/core`'s `KeyManager` cleanly falls back to its
181
+ // package-private store.
182
+ // ---------------------------------------------------------------------------
183
+ let sharedIdentityBridgePromise = null;
184
+ function loadSharedIdentityBridge() {
185
+ if (!sharedIdentityBridgePromise) {
186
+ sharedIdentityBridgePromise = Promise.resolve().then(() => {
187
+ const native = (0, expo_modules_core_1.requireOptionalNativeModule)('OxyIdentity');
188
+ if (native &&
189
+ typeof native.getShared === 'function' &&
190
+ typeof native.putShared === 'function' &&
191
+ typeof native.hasShared === 'function' &&
192
+ typeof native.clearShared === 'function') {
193
+ return {
194
+ getShared: native.getShared.bind(native),
195
+ putShared: native.putShared.bind(native),
196
+ hasShared: native.hasShared.bind(native),
197
+ clearShared: native.clearShared.bind(native),
198
+ };
199
+ }
200
+ return null;
201
+ });
202
+ }
203
+ return sharedIdentityBridgePromise;
204
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * Structural interfaces for Expo platform modules.
4
+ *
5
+ * These replace `typeof import('expo-crypto')` and
6
+ * `typeof import('expo-secure-store')` in the built declaration files of
7
+ * `@oxy.so/protocol` and `@oxy.so/core`.
8
+ *
9
+ * ## Why structural interfaces instead of `typeof import('expo-*')`?
10
+ *
11
+ * Under NodeNext module resolution (used by `@oxy.so/api` and `@oxy.so/node`),
12
+ * `expo-crypto` ships with `"exports": {}` (empty exports map). TypeScript
13
+ * traverses into the package anyway via the `types` field, which transitively
14
+ * loads `expo-modules-core`. That pollution makes `setInterval`/`setTimeout`
15
+ * resolve to DOM's `number` return type rather than Node's `NodeJS.Timeout`,
16
+ * producing ~10 spurious `TS2322` / `TS2339` errors in every consumer that
17
+ * uses Node timer APIs — none of which reference protocol types at all.
18
+ *
19
+ * Structural interfaces break the transitive expo-modules-core dependency
20
+ * entirely: consumers that don't have Expo installed see clean types, and
21
+ * the actual RN runtime (which DOES have Expo installed) still works because
22
+ * the real modules satisfy these interfaces structurally.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });