@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,105 @@
1
+ /**
2
+ * Node-protocol shape constants — the wire-level contract of an Oxy-protocol
3
+ * data node, shared by the generic node app factory ({@link ./nodeApp}), the
4
+ * HTTP {@link ./nodeClient.NodeClient}, and any app's node deployment
5
+ * (`@oxy.so/node`, a future `mention-node`).
6
+ *
7
+ * These were previously hardcoded inside `@oxy.so/node`; they live here so the
8
+ * SAME values drive a server and a client without either side re-declaring (and
9
+ * drifting) the contract. Deployment-specific knobs (owner key, port, data dir)
10
+ * are still resolved per-deployment from the environment — only the protocol's
11
+ * own shape constants live here.
12
+ */
13
+
14
+ /**
15
+ * The node-protocol version advertised at the well-known manifest. Bumped only
16
+ * on a breaking change to the wire shape of the log / head / record APIs. This
17
+ * is the DEFAULT `protocolId` a deployment advertises (overridable per app).
18
+ */
19
+ export const PROTOCOL_VERSION = 'oxy-node/1' as const;
20
+
21
+ /** Default well-known manifest path (the existing `@oxy.so/node` value). */
22
+ export const DEFAULT_WELL_KNOWN_PATH = '/.well-known/oxy-node.json';
23
+
24
+ /** Default DID-document service-type label advertised by a node deployment. */
25
+ export const DEFAULT_SERVICE_TYPE = 'OxyPersonalDataNode';
26
+
27
+ /**
28
+ * Default application namespace a node deployment serves. The records a node
29
+ * stores all live under this namespace (e.g. `app.oxy.*`); a `collections`
30
+ * allowlist (when set) MUST be within it.
31
+ */
32
+ export const DEFAULT_APP_NAMESPACE = 'app.oxy';
33
+
34
+ /** Default HTTP port when the port env var is unset (always overridable). */
35
+ export const DEFAULT_PORT = 4000;
36
+
37
+ /** Default and maximum number of log entries returned by `GET /oxy/log`. */
38
+ export const DEFAULT_LOG_LIMIT = 100;
39
+ export const MAX_LOG_LIMIT = 500;
40
+
41
+ /** Default upper bound on a single pinned blob's size when unset (25 MiB). */
42
+ export const DEFAULT_MAX_BLOB_BYTES = 25 * 1024 * 1024;
43
+
44
+ /** Maximum number of envelopes accepted in one `POST /sync/push` batch. */
45
+ export const MAX_SYNC_BATCH = 200;
46
+
47
+ /** Body-size ceiling for JSON request bodies (`/records`, `/sync/push`). */
48
+ export const JSON_BODY_LIMIT = '5mb';
49
+
50
+ /** HTTP headers carrying an owner-signed action authorization (blob pins). */
51
+ export const OWNER_AUTH_HEADERS = {
52
+ publicKey: 'x-oxy-node-public-key',
53
+ signature: 'x-oxy-node-signature',
54
+ timestamp: 'x-oxy-node-timestamp',
55
+ } as const;
56
+
57
+ /**
58
+ * Freshness window for an owner-signed action (e.g. a blob pin). A signed
59
+ * authorization header older/newer than this (accounting for clock skew) is
60
+ * rejected, bounding replay of a captured pin authorization.
61
+ */
62
+ export const OWNER_AUTH_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
63
+
64
+ /** Operating modes a node can advertise. */
65
+ export const NODE_MODES = ['self-hosted', 'managed'] as const;
66
+ export type NodeMode = (typeof NODE_MODES)[number];
67
+
68
+ /** The node operation an owner can authorize with a signed header. */
69
+ export const OWNER_ACTION_BLOB_PIN = 'blob-pin' as const;
70
+
71
+ /** A 32-byte (64 hex char) lowercase SHA-256 digest, used as the blob address. */
72
+ export const SHA256_HEX = /^[0-9a-f]{64}$/;
73
+
74
+ /* -------------------------------------------------------------------------- */
75
+ /* HTTP NodeClient — the node-facing routes a client drives */
76
+ /* -------------------------------------------------------------------------- */
77
+
78
+ /** Chain head endpoint (`GET`). */
79
+ export const NODE_HEAD_PATH = '/oxy/head';
80
+ /** Ordered log endpoint (`GET ?since=&limit=`). */
81
+ export const NODE_LOG_PATH = '/oxy/log';
82
+ /** Single-record write endpoint (`POST`, owner-signed envelope). */
83
+ export const NODE_RECORDS_PATH = '/records';
84
+ /** Batch push endpoint (`POST`, owner-signed envelopes). */
85
+ export const NODE_SYNC_PUSH_PATH = '/sync/push';
86
+ /** Content-addressed blob endpoint prefix (`GET|PUT /blobs/:hash`). */
87
+ export const NODE_BLOBS_PATH = '/blobs';
88
+
89
+ /** Default time-to-first-byte deadline for a NodeClient request (ms). */
90
+ export const DEFAULT_CLIENT_TIMEOUT_MS = 8_000;
91
+
92
+ /** Default redirect budget for a NodeClient request (each re-validated upstream). */
93
+ export const DEFAULT_CLIENT_MAX_REDIRECTS = 1;
94
+
95
+ /** Default bounded read for a `/oxy/head` response (tiny JSON). */
96
+ export const DEFAULT_HEAD_MAX_BYTES = 64 * 1024;
97
+
98
+ /** Default bounded read for a `/oxy/log` page response. */
99
+ export const DEFAULT_LOG_MAX_BYTES = 2 * 1024 * 1024;
100
+
101
+ /** Default bounded read for a small JSON write response (`/records`, blob pin). */
102
+ export const DEFAULT_WRITE_RESPONSE_MAX_BYTES = 64 * 1024;
103
+
104
+ /** Default bounded read for a fetched `<did>.json` document. */
105
+ export const DEFAULT_DID_DOC_MAX_BYTES = 256 * 1024;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * `createDidWebResolver` — a {@link VerificationMethodResolver} for arbitrary
3
+ * `did:web` subjects, backing the chain engine on multi-subject relay/ingest
4
+ * paths (where the signer is NOT a local account but any DID that publishes a
5
+ * DID document).
6
+ *
7
+ * It resolves a `did:web:<host>[:<path>]` subject to its `did.json`, fetched via
8
+ * an INJECTED {@link NodeFetch} — Oxy passes an adapter over `@oxy.so/core/server`
9
+ * `safeFetch` (so the SSRF/transport policy stays in core); a test passes a
10
+ * stub. The subject's current verification keys are read from the DID document's
11
+ * `verificationMethod[].publicKeyHex` (the active assertion methods), and the
12
+ * engine's `isAuthorizedKey` then applies the uniform self-issued rule.
13
+ *
14
+ * `did:web` → URL mapping (W3C did:web):
15
+ * - `did:web:example.com` → `https://example.com/.well-known/did.json`
16
+ * - `did:web:example.com:u:123` → `https://example.com/u/123/did.json`
17
+ * - a `%3A` in the host segment is decoded to `:` (an explicit port).
18
+ */
19
+
20
+ import {
21
+ didDocumentSchema,
22
+ type DidDocument,
23
+ type Secp256k1VerificationMethod,
24
+ } from '@oxy.so/contracts';
25
+ import type {
26
+ ResolvedVerificationMethods,
27
+ VerificationMethodResolver,
28
+ } from '../identity/resolver';
29
+ import { type NodeFetch, readBoundedJson } from './httpFetch';
30
+ import {
31
+ DEFAULT_CLIENT_MAX_REDIRECTS,
32
+ DEFAULT_CLIENT_TIMEOUT_MS,
33
+ DEFAULT_DID_DOC_MAX_BYTES,
34
+ } from './constants';
35
+
36
+ /** Options for {@link createDidWebResolver}. */
37
+ export interface DidWebResolverOptions {
38
+ /** Time-to-first-byte deadline for the `did.json` fetch (ms). */
39
+ headersTimeoutMs?: number;
40
+ /** Redirect budget for the `did.json` fetch (each re-validated by the transport). */
41
+ maxRedirects?: number;
42
+ /** Bounded read ceiling for the fetched `did.json`. */
43
+ maxBytes?: number;
44
+ /**
45
+ * Notified when a subject cannot be resolved (mapping/fetch/parse failure).
46
+ * `resolve` still returns `null` (the engine treats that as "no authorized
47
+ * key") — this hook gives the failure visibility without a silent catch.
48
+ */
49
+ onError?: (err: unknown, subjectDid: string) => void;
50
+ }
51
+
52
+ /**
53
+ * Map a `did:web` DID to its `did.json` URL, or `null` when `did` is not a
54
+ * well-formed `did:web` identifier.
55
+ */
56
+ export function didWebToUrl(did: string): string | null {
57
+ const prefix = 'did:web:';
58
+ if (!did.startsWith(prefix)) {
59
+ return null;
60
+ }
61
+ const msi = did.slice(prefix.length);
62
+ if (msi.length === 0) {
63
+ return null;
64
+ }
65
+ const [domainPart, ...pathParts] = msi.split(':');
66
+ const host = domainPart.replace(/%3A/gi, ':');
67
+ if (host.length === 0 || host.includes('/')) {
68
+ return null;
69
+ }
70
+ const base = `https://${host}`;
71
+ if (pathParts.length === 0) {
72
+ return `${base}/.well-known/did.json`;
73
+ }
74
+ if (pathParts.some((part) => part.length === 0)) {
75
+ return null;
76
+ }
77
+ return `${base}/${pathParts.join('/')}/did.json`;
78
+ }
79
+
80
+ /** True for a secp256k1 verification method (carries `publicKeyHex`). */
81
+ function isSecp256k1Vm(
82
+ vm: DidDocument['verificationMethod'][number],
83
+ ): vm is Secp256k1VerificationMethod {
84
+ return vm.type === 'EcdsaSecp256k1VerificationKey2019';
85
+ }
86
+
87
+ /**
88
+ * Collect the subject's current verification keys from its DID document: the
89
+ * `publicKeyHex` of every secp256k1 verification method referenced by
90
+ * `assertionMethod` (the keys that may sign assertions/records), deduped. Falls
91
+ * back to ALL secp256k1 `verificationMethod[]` keys when `assertionMethod`
92
+ * references nothing local. Non-secp256k1 methods (e.g. the atproto `Multikey`,
93
+ * which carries the SAME key in multibase form, not hex) are skipped — record
94
+ * signatures verify against the hex key.
95
+ */
96
+ function collectCurrentPublicKeys(doc: DidDocument): string[] {
97
+ const byId = new Map(
98
+ doc.verificationMethod
99
+ .filter(isSecp256k1Vm)
100
+ .map((vm) => [vm.id, vm.publicKeyHex] as const),
101
+ );
102
+ const keys: string[] = [];
103
+ for (const id of doc.assertionMethod) {
104
+ const key = byId.get(id);
105
+ if (key && !keys.includes(key)) {
106
+ keys.push(key);
107
+ }
108
+ }
109
+ if (keys.length === 0) {
110
+ for (const key of byId.values()) {
111
+ if (!keys.includes(key)) {
112
+ keys.push(key);
113
+ }
114
+ }
115
+ }
116
+ return keys;
117
+ }
118
+
119
+ /**
120
+ * Build a {@link VerificationMethodResolver} that resolves `did:web` subjects via
121
+ * the injected `fetch`. Returns `null` for any subject that is not a `did:web`
122
+ * DID, whose `did.json` cannot be fetched, or whose document fails schema
123
+ * validation — the engine then treats the signer as unauthorized.
124
+ */
125
+ export function createDidWebResolver(
126
+ fetch: NodeFetch,
127
+ options: DidWebResolverOptions = {},
128
+ ): VerificationMethodResolver {
129
+ const headersTimeoutMs = options.headersTimeoutMs ?? DEFAULT_CLIENT_TIMEOUT_MS;
130
+ const maxRedirects = options.maxRedirects ?? DEFAULT_CLIENT_MAX_REDIRECTS;
131
+ const maxBytes = options.maxBytes ?? DEFAULT_DID_DOC_MAX_BYTES;
132
+
133
+ return {
134
+ async resolve(subjectDid: string): Promise<ResolvedVerificationMethods | null> {
135
+ const url = didWebToUrl(subjectDid);
136
+ if (!url) {
137
+ return null;
138
+ }
139
+ try {
140
+ const res = await fetch(url, { method: 'GET', headersTimeoutMs, maxRedirects });
141
+ if (res.status < 200 || res.status >= 300) {
142
+ res.destroy();
143
+ return null;
144
+ }
145
+ const body = await readBoundedJson(res, maxBytes);
146
+ const parsed = didDocumentSchema.safeParse(body);
147
+ if (!parsed.success) {
148
+ return null;
149
+ }
150
+ // The DID document `id` MUST match the subject we asked for — a document
151
+ // served at the subject's URL but claiming another id is not authoritative.
152
+ if (parsed.data.id !== subjectDid) {
153
+ return null;
154
+ }
155
+ return { currentPublicKeys: collectCurrentPublicKeys(parsed.data) };
156
+ } catch (err) {
157
+ options.onError?.(err, subjectDid);
158
+ return null;
159
+ }
160
+ },
161
+ };
162
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The injected HTTP transport the node {@link ./nodeClient.NodeClient} and the
3
+ * {@link ./didWebResolver} drive — plus the bounded-read helpers that make a
4
+ * response stream safe to consume.
5
+ *
6
+ * The protocol package is app-agnostic and MUST NOT depend on `@oxy.so/core`
7
+ * (core depends on protocol). So instead of importing `@oxy.so/core/server`'s
8
+ * `safeFetch` directly, the node client/resolver accept a {@link NodeFetch} —
9
+ * Oxy supplies an adapter over `safeFetch` (HTTPS-only, DNS-pinned, private-IP
10
+ * denylist, bounded redirects); a test supplies an in-process stub. Either way
11
+ * the SSRF/transport policy stays in the injected implementation, and the
12
+ * bounded body read (the cap that stops a malicious node streaming forever)
13
+ * stays here, close to the parsing.
14
+ *
15
+ * A Node `IncomingMessage` (what `safeFetch` returns) satisfies
16
+ * {@link NodeFetchResponse.body} directly — it is an `AsyncIterable<Buffer>`,
17
+ * and `Buffer` is a `Uint8Array`.
18
+ */
19
+
20
+ /** Per-request options the client passes to the injected transport. */
21
+ export interface NodeFetchInit {
22
+ /** HTTP method (`GET` / `POST` / `PUT`). */
23
+ method: string;
24
+ /** Request headers. */
25
+ headers?: Record<string, string>;
26
+ /** Request body (for `POST` / `PUT`); omitted for `GET`. */
27
+ body?: Uint8Array;
28
+ /** Time-to-first-byte deadline in milliseconds. */
29
+ headersTimeoutMs?: number;
30
+ /** Redirect budget (each hop re-validated by the implementation). */
31
+ maxRedirects?: number;
32
+ }
33
+
34
+ /** The streamed, non-redirect response the transport returns. */
35
+ export interface NodeFetchResponse {
36
+ /** HTTP status code. */
37
+ status: number;
38
+ /** Response headers (a Node `IncomingHttpHeaders` satisfies this). */
39
+ headers: Record<string, string | string[] | undefined>;
40
+ /** Async-iterable byte body — read with the bounded helpers below. */
41
+ body: AsyncIterable<Uint8Array>;
42
+ /** Release the underlying stream when a bounded read is cut short. */
43
+ destroy(): void;
44
+ }
45
+
46
+ /**
47
+ * The injected transport. Oxy adapts `@oxy.so/core/server`'s `safeFetch` to this
48
+ * shape; tests pass an in-process stub.
49
+ */
50
+ export type NodeFetch = (url: string, init: NodeFetchInit) => Promise<NodeFetchResponse>;
51
+
52
+ /** Thrown when a response body exceeds the caller's byte ceiling. */
53
+ export class ResponseTooLargeError extends Error {
54
+ constructor(public readonly maxBytes: number) {
55
+ super(`response exceeded ${maxBytes} bytes`);
56
+ this.name = 'ResponseTooLargeError';
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Read a response body into a single buffer, aborting (and destroying the
62
+ * stream) the moment it exceeds `maxBytes`. The bound is the defence against a
63
+ * node that streams an unbounded body.
64
+ */
65
+ export async function readBoundedBytes(res: NodeFetchResponse, maxBytes: number): Promise<Buffer> {
66
+ const chunks: Buffer[] = [];
67
+ let total = 0;
68
+ try {
69
+ for await (const chunk of res.body) {
70
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
71
+ total += buf.length;
72
+ if (total > maxBytes) {
73
+ throw new ResponseTooLargeError(maxBytes);
74
+ }
75
+ chunks.push(buf);
76
+ }
77
+ } catch (err) {
78
+ res.destroy();
79
+ throw err;
80
+ }
81
+ return Buffer.concat(chunks);
82
+ }
83
+
84
+ /** Read a bounded response body and parse it as JSON. */
85
+ export async function readBoundedJson(res: NodeFetchResponse, maxBytes: number): Promise<unknown> {
86
+ const bytes = await readBoundedBytes(res, maxBytes);
87
+ return JSON.parse(bytes.toString('utf8'));
88
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `@oxy.so/protocol/node` — the runnable node substrate.
3
+ *
4
+ * The Node-only half of the protocol: the Express app factory that backs any
5
+ * Oxy-protocol data node ({@link createNodeApp}), the HTTP {@link NodeClient}
6
+ * that drives a node's routes, the `did:web` verification-method resolver, the
7
+ * record verifier, and the node-protocol shape constants. A SEPARATE subpath
8
+ * from the package root so this Express/Node-only code never enters React
9
+ * Native / web bundles that import `@oxy.so/protocol`.
10
+ *
11
+ * Reused by `@oxy.so/node` (the runnable node), a future `mention-node` (an
12
+ * env-only deployment of the same base), and oxy-api's node sync (which drives
13
+ * a node via `NodeClient`).
14
+ */
15
+
16
+ // ── Express app factory ───────────────────────────────────────────────────────
17
+ export { createNodeApp, BlobHashMismatchError } from './nodeApp';
18
+ export type {
19
+ NodeApp,
20
+ NodeAppDependencies,
21
+ NodeAppConfig,
22
+ NodeStoreLike,
23
+ OwnerAuth,
24
+ NodeLogger,
25
+ } from './nodeApp';
26
+
27
+ // ── Per-IP write rate limiter ──────────────────────────────────────────────────
28
+ export {
29
+ createRateLimiter,
30
+ DEFAULT_WRITE_RATE_LIMIT,
31
+ DEFAULT_MAX_RATE_LIMIT_ENTRIES,
32
+ } from './rateLimit';
33
+ export type { RateLimitConfig, RateLimiter } from './rateLimit';
34
+
35
+ // ── Record verification (signature + v2 + content address) ─────────────────────
36
+ export { verifyNodeRecordEnvelope } from './verifyRecord';
37
+ export type { NodeVerifyResult, NodeVerifyRejectionReason } from './verifyRecord';
38
+
39
+ // ── HTTP client ────────────────────────────────────────────────────────────────
40
+ export { NodeClient, NodeClientError, trimTrailingSlashes } from './nodeClient';
41
+ export type {
42
+ NodeClientOptions,
43
+ NodeHead,
44
+ NodeLogPage,
45
+ NodeWriteResult,
46
+ NodeBlobPutResult,
47
+ NodeBlobPinAuth,
48
+ } from './nodeClient';
49
+
50
+ // ── Injected transport contract + bounded readers ──────────────────────────────
51
+ export { readBoundedBytes, readBoundedJson, ResponseTooLargeError } from './httpFetch';
52
+ export type { NodeFetch, NodeFetchInit, NodeFetchResponse } from './httpFetch';
53
+
54
+ // ── did:web verification-method resolver ───────────────────────────────────────
55
+ export { createDidWebResolver, didWebToUrl } from './didWebResolver';
56
+ export type { DidWebResolverOptions } from './didWebResolver';
57
+
58
+ // ── Node-protocol shape constants ──────────────────────────────────────────────
59
+ export {
60
+ PROTOCOL_VERSION,
61
+ DEFAULT_WELL_KNOWN_PATH,
62
+ DEFAULT_SERVICE_TYPE,
63
+ DEFAULT_APP_NAMESPACE,
64
+ DEFAULT_PORT,
65
+ DEFAULT_LOG_LIMIT,
66
+ MAX_LOG_LIMIT,
67
+ DEFAULT_MAX_BLOB_BYTES,
68
+ MAX_SYNC_BATCH,
69
+ JSON_BODY_LIMIT,
70
+ OWNER_AUTH_HEADERS,
71
+ OWNER_AUTH_MAX_AGE_MS,
72
+ NODE_MODES,
73
+ OWNER_ACTION_BLOB_PIN,
74
+ SHA256_HEX,
75
+ NODE_HEAD_PATH,
76
+ NODE_LOG_PATH,
77
+ NODE_RECORDS_PATH,
78
+ NODE_SYNC_PUSH_PATH,
79
+ NODE_BLOBS_PATH,
80
+ DEFAULT_CLIENT_TIMEOUT_MS,
81
+ DEFAULT_CLIENT_MAX_REDIRECTS,
82
+ DEFAULT_HEAD_MAX_BYTES,
83
+ DEFAULT_LOG_MAX_BYTES,
84
+ DEFAULT_WRITE_RESPONSE_MAX_BYTES,
85
+ DEFAULT_DID_DOC_MAX_BYTES,
86
+ } from './constants';
87
+ export type { NodeMode } from './constants';