@hraness/kb 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,627 @@
1
+ import {
2
+ createServer,
3
+ request as requestHttp,
4
+ type ClientRequest,
5
+ type IncomingHttpHeaders,
6
+ type IncomingMessage,
7
+ type RequestOptions,
8
+ type Server as HttpServer,
9
+ type ServerResponse,
10
+ } from "node:http";
11
+ import { connect as connectTcp, Socket } from "node:net";
12
+ import type { Duplex } from "node:stream";
13
+
14
+ import { BoundedByteBuffer } from "./bounded-byte-buffer.js";
15
+ import {
16
+ createPinnedLookup,
17
+ FetchFailure,
18
+ resolveSafeNetworkTarget,
19
+ type NetworkResolver,
20
+ type ResolvedNetworkAddress,
21
+ } from "./network.js";
22
+
23
+ export type NetworkProxyOptions = {
24
+ readonly allowPrivateNetwork: boolean;
25
+ readonly timeoutMs?: number;
26
+ readonly maxHeaderBytes?: number;
27
+ readonly maxConnections?: number;
28
+ readonly maxRequestBodyBytes?: number;
29
+ readonly maxTransferredBytes?: number;
30
+ /** Test seams. Production callers must leave these unset. */
31
+ readonly resolveHostname?: NetworkResolver;
32
+ readonly connectAddress?: (address: ResolvedNetworkAddress, port: number) => Socket;
33
+ };
34
+
35
+ export type LocalNetworkProxy = {
36
+ readonly url: string;
37
+ readonly port: number;
38
+ close(): Promise<void>;
39
+ };
40
+
41
+ const defaultTimeoutMs = 30_000;
42
+ const defaultMaxHeaderBytes = 32 * 1024;
43
+ const defaultMaxConnections = 64;
44
+ const defaultMaxRequestBodyBytes = 16 * 1024 * 1024;
45
+ const defaultMaxTransferredBytes = 1024 * 1024 * 1024;
46
+ const maxConnectAddresses = 16;
47
+
48
+ type HttpUpstream = {
49
+ readonly request: ClientRequest;
50
+ readonly detachDownstream: () => void;
51
+ response: IncomingMessage | null;
52
+ socket: Socket | null;
53
+ settled: boolean;
54
+ };
55
+
56
+ const hopByHopHeaders = new Set([
57
+ "connection",
58
+ "keep-alive",
59
+ "proxy-authenticate",
60
+ "proxy-authorization",
61
+ "proxy-connection",
62
+ "te",
63
+ "trailer",
64
+ "transfer-encoding",
65
+ "upgrade",
66
+ ]);
67
+
68
+ class ProxyRequestTooLarge extends Error {
69
+ constructor() {
70
+ super("proxy request body exceeded its limit");
71
+ this.name = "ProxyRequestTooLarge";
72
+ }
73
+ }
74
+
75
+ function positiveBoundedInteger(value: number | undefined, fallback: number, maximum: number): number {
76
+ if (value === undefined) return fallback;
77
+ if (!Number.isSafeInteger(value) || value < 1) throw new Error("network proxy limits must be positive integers");
78
+ return Math.min(value, maximum);
79
+ }
80
+
81
+ function connectionNamedHeaders(headers: IncomingHttpHeaders): ReadonlySet<string> {
82
+ const names = new Set<string>();
83
+ const connection: unknown = headers["connection"];
84
+ const values: readonly unknown[] = Array.isArray(connection)
85
+ ? connection
86
+ : connection === undefined
87
+ ? []
88
+ : [connection];
89
+ for (const value of values) {
90
+ if (typeof value !== "string") continue;
91
+ for (const name of value.split(",")) {
92
+ const normalized = name.trim().toLowerCase();
93
+ if (normalized !== "") names.add(normalized);
94
+ }
95
+ }
96
+ return names;
97
+ }
98
+
99
+ function forwardedHeaders(headers: IncomingHttpHeaders): IncomingHttpHeaders {
100
+ const named = connectionNamedHeaders(headers);
101
+ const forwarded: IncomingHttpHeaders = {};
102
+ for (const [name, value] of Object.entries(headers)) {
103
+ const normalized = name.toLowerCase();
104
+ if (hopByHopHeaders.has(normalized) || named.has(normalized) || normalized === "host") continue;
105
+ if (value !== undefined) forwarded[normalized] = value;
106
+ }
107
+ return forwarded;
108
+ }
109
+
110
+ function responseHeaders(headers: IncomingHttpHeaders): IncomingHttpHeaders {
111
+ const named = connectionNamedHeaders(headers);
112
+ const forwarded: IncomingHttpHeaders = {};
113
+ for (const [name, value] of Object.entries(headers)) {
114
+ const normalized = name.toLowerCase();
115
+ if (hopByHopHeaders.has(normalized) || named.has(normalized)) continue;
116
+ if (value !== undefined) forwarded[normalized] = value;
117
+ }
118
+ return forwarded;
119
+ }
120
+
121
+ function proxyStatusFor(error: unknown): number {
122
+ if (error instanceof ProxyRequestTooLarge) return 413;
123
+ if (error instanceof FetchFailure && (error.code === "private-network" || error.code === "invalid-url")) return 403;
124
+ if (error instanceof FetchFailure && error.code === "timeout") return 504;
125
+ return 502;
126
+ }
127
+
128
+ function readRequestBody(incoming: IncomingMessage, maxBytes: number): Promise<Buffer> {
129
+ return new Promise((resolve, reject) => {
130
+ const bytes = new BoundedByteBuffer(maxBytes);
131
+ let settled = false;
132
+ const cleanup = (): void => {
133
+ incoming.off("data", onData);
134
+ incoming.off("end", onEnd);
135
+ incoming.off("error", onError);
136
+ incoming.off("aborted", onAborted);
137
+ };
138
+ const fail = (error: Error): void => {
139
+ if (settled) return;
140
+ settled = true;
141
+ cleanup();
142
+ reject(error);
143
+ };
144
+ const onData = (value: unknown): void => {
145
+ const chunk = Buffer.isBuffer(value)
146
+ ? value
147
+ : typeof value === "string"
148
+ ? Buffer.from(value)
149
+ : value instanceof Uint8Array
150
+ ? Buffer.from(value)
151
+ : null;
152
+ if (chunk === null) {
153
+ fail(new Error("proxy request yielded a non-byte chunk"));
154
+ return;
155
+ }
156
+ if (!bytes.append(chunk)) {
157
+ fail(new ProxyRequestTooLarge());
158
+ return;
159
+ }
160
+ };
161
+ const onEnd = (): void => {
162
+ if (settled) return;
163
+ settled = true;
164
+ cleanup();
165
+ const body = bytes.toUint8Array();
166
+ resolve(Buffer.from(body.buffer, body.byteOffset, body.byteLength));
167
+ };
168
+ const onError = (error: Error): void => fail(error);
169
+ const onAborted = (): void => fail(new Error("proxy request was aborted"));
170
+ incoming.on("data", onData);
171
+ incoming.once("end", onEnd);
172
+ incoming.once("error", onError);
173
+ incoming.once("aborted", onAborted);
174
+ });
175
+ }
176
+
177
+ function finishSocket(socket: Duplex, status: number): void {
178
+ if (socket.destroyed) return;
179
+ const reason = status === 403 ? "Forbidden" : status === 504 ? "Gateway Timeout" : "Bad Gateway";
180
+ socket.end(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
181
+ }
182
+
183
+ function finishResponse(response: ServerResponse, status: number): void {
184
+ if (response.headersSent || response.destroyed) {
185
+ response.destroy();
186
+ return;
187
+ }
188
+ response.writeHead(status, {
189
+ "Cache-Control": "no-store",
190
+ Connection: "close",
191
+ "Content-Length": "0",
192
+ });
193
+ response.end();
194
+ }
195
+
196
+ function connectAuthority(authority: string): URL {
197
+ if (
198
+ authority === ""
199
+ || authority.length > 1_024
200
+ || /[\s\\/?#@]/.test(authority)
201
+ ) {
202
+ throw new FetchFailure("invalid-url", "invalid CONNECT authority");
203
+ }
204
+ let target: URL;
205
+ try {
206
+ target = new URL(`https://${authority}/`);
207
+ } catch (error) {
208
+ throw new FetchFailure("invalid-url", "invalid CONNECT authority", { cause: error });
209
+ }
210
+ if (target.hostname === "" || target.username !== "" || target.password !== "") {
211
+ throw new FetchFailure("invalid-url", "invalid CONNECT authority");
212
+ }
213
+ return target;
214
+ }
215
+
216
+ function targetPort(target: URL): number {
217
+ const raw = target.port;
218
+ const fallback = target.protocol === "https:" ? 443 : 80;
219
+ const port = raw === "" ? fallback : Number(raw);
220
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
221
+ throw new FetchFailure("invalid-url", "destination port is invalid");
222
+ }
223
+ return port;
224
+ }
225
+
226
+ function trackSocket(socket: Duplex, sockets: Set<Duplex>, timeoutMs: number): void {
227
+ sockets.add(socket);
228
+ if (socket instanceof Socket) socket.setTimeout(timeoutMs, () => socket.destroy());
229
+ socket.once("close", () => sockets.delete(socket));
230
+ }
231
+
232
+ async function connectPinned(
233
+ addresses: readonly ResolvedNetworkAddress[],
234
+ port: number,
235
+ timeoutMs: number,
236
+ sockets: Set<Duplex>,
237
+ connectAddress: (address: ResolvedNetworkAddress, port: number) => Socket,
238
+ isClosing: () => boolean,
239
+ ): Promise<Socket> {
240
+ const deadline = Date.now() + timeoutMs;
241
+ let lastError: unknown;
242
+ for (const address of addresses.slice(0, maxConnectAddresses)) {
243
+ if (isClosing()) throw new FetchFailure("network", "proxy is closing");
244
+ try {
245
+ const socket = await new Promise<Socket>((resolve, reject) => {
246
+ const remainingMs = deadline - Date.now();
247
+ if (remainingMs <= 0) {
248
+ reject(new FetchFailure("timeout", "proxy connection timed out"));
249
+ return;
250
+ }
251
+ const candidate = connectAddress(address, port);
252
+ trackSocket(candidate, sockets, timeoutMs);
253
+ let settled = false;
254
+ const timeout = setTimeout(() => {
255
+ if (settled) return;
256
+ settled = true;
257
+ candidate.destroy();
258
+ reject(new FetchFailure("timeout", "proxy connection timed out"));
259
+ }, remainingMs);
260
+ candidate.once("connect", () => {
261
+ if (settled) return;
262
+ settled = true;
263
+ clearTimeout(timeout);
264
+ resolve(candidate);
265
+ });
266
+ candidate.once("error", (error) => {
267
+ if (settled) return;
268
+ settled = true;
269
+ clearTimeout(timeout);
270
+ candidate.destroy();
271
+ reject(error);
272
+ });
273
+ candidate.once("close", () => {
274
+ if (settled) return;
275
+ settled = true;
276
+ clearTimeout(timeout);
277
+ reject(new FetchFailure("network", "proxy connection closed before it was established"));
278
+ });
279
+ });
280
+ if (isClosing()) {
281
+ socket.destroy();
282
+ throw new FetchFailure("network", "proxy is closing");
283
+ }
284
+ return socket;
285
+ } catch (error) {
286
+ if (isClosing()) throw new FetchFailure("network", "proxy is closing", { cause: error });
287
+ if (error instanceof FetchFailure && error.code === "timeout") throw error;
288
+ lastError = error;
289
+ }
290
+ }
291
+ if (Date.now() >= deadline) {
292
+ throw new FetchFailure("timeout", "proxy connection timed out", { cause: lastError });
293
+ }
294
+ throw new FetchFailure("network", "could not connect to validated destination", { cause: lastError });
295
+ }
296
+
297
+ function parseAbsoluteHttpTarget(raw: string | undefined): URL {
298
+ if (raw === undefined || raw.length > 16 * 1024) {
299
+ throw new FetchFailure("invalid-url", "invalid proxy request target");
300
+ }
301
+ let target: URL;
302
+ try {
303
+ target = new URL(raw);
304
+ } catch (error) {
305
+ throw new FetchFailure("invalid-url", "proxy requests require an absolute HTTP URL", { cause: error });
306
+ }
307
+ if (target.protocol !== "http:" || target.hash !== "") {
308
+ throw new FetchFailure("invalid-url", "absolute proxy requests must use HTTP");
309
+ }
310
+ return target;
311
+ }
312
+
313
+ /**
314
+ * Start a loopback-only forward proxy that validates DNS and pins one approved address for every
315
+ * HTTP request and TLS CONNECT tunnel. It is intended to be short-lived and owned by one capture.
316
+ */
317
+ export async function startNetworkProxy(options: NetworkProxyOptions): Promise<LocalNetworkProxy> {
318
+ const timeoutMs = positiveBoundedInteger(options.timeoutMs, defaultTimeoutMs, 10 * 60_000);
319
+ const maxHeaderBytes = positiveBoundedInteger(options.maxHeaderBytes, defaultMaxHeaderBytes, 1024 * 1024);
320
+ const maxConnections = positiveBoundedInteger(options.maxConnections, defaultMaxConnections, 1_024);
321
+ const maxRequestBodyBytes = positiveBoundedInteger(
322
+ options.maxRequestBodyBytes,
323
+ defaultMaxRequestBodyBytes,
324
+ 1024 * 1024 * 1024,
325
+ );
326
+ const maxTransferredBytes = positiveBoundedInteger(
327
+ options.maxTransferredBytes,
328
+ defaultMaxTransferredBytes,
329
+ Number.MAX_SAFE_INTEGER,
330
+ );
331
+ const sockets = new Set<Duplex>();
332
+ const httpUpstreams = new Set<HttpUpstream>();
333
+ let activeRequests = 0;
334
+ let transferredBytes = 0;
335
+ let closing = false;
336
+
337
+ const reserveRequest = (): boolean => {
338
+ if (closing || activeRequests >= maxConnections) return false;
339
+ activeRequests += 1;
340
+ return true;
341
+ };
342
+ const releaseRequest = (): void => {
343
+ activeRequests = Math.max(0, activeRequests - 1);
344
+ };
345
+ const accountTransfer = (bytes: number): boolean => {
346
+ transferredBytes += bytes;
347
+ return transferredBytes <= maxTransferredBytes;
348
+ };
349
+ const forgetHttpUpstream = (upstream: HttpUpstream): void => {
350
+ if (upstream.settled) return;
351
+ upstream.settled = true;
352
+ httpUpstreams.delete(upstream);
353
+ upstream.detachDownstream();
354
+ };
355
+ const destroyHttpUpstream = (upstream: HttpUpstream): void => {
356
+ if (upstream.settled) return;
357
+ upstream.settled = true;
358
+ httpUpstreams.delete(upstream);
359
+ upstream.detachDownstream();
360
+ upstream.response?.destroy();
361
+ upstream.request.destroy();
362
+ upstream.socket?.destroy();
363
+ };
364
+ const resolveTarget = (target: URL): Promise<readonly ResolvedNetworkAddress[]> =>
365
+ resolveSafeNetworkTarget(target, {
366
+ allowPrivateNetwork: options.allowPrivateNetwork,
367
+ timeoutMs,
368
+ ...(options.resolveHostname === undefined ? {} : { resolveHostname: options.resolveHostname }),
369
+ });
370
+ const connectAddress = options.connectAddress
371
+ ?? ((address: ResolvedNetworkAddress, port: number): Socket =>
372
+ connectTcp({ host: address.address, port, family: address.family }));
373
+
374
+ const server: HttpServer = createServer({
375
+ maxHeaderSize: maxHeaderBytes,
376
+ headersTimeout: timeoutMs,
377
+ requestTimeout: timeoutMs,
378
+ keepAliveTimeout: Math.min(timeoutMs, 5_000),
379
+ });
380
+ server.maxConnections = maxConnections;
381
+
382
+ server.on("request", (incoming, outgoing) => {
383
+ if (!reserveRequest()) {
384
+ finishResponse(outgoing, 503);
385
+ return;
386
+ }
387
+ let released = false;
388
+ const release = (): void => {
389
+ if (released) return;
390
+ released = true;
391
+ releaseRequest();
392
+ };
393
+ let httpUpstream: HttpUpstream | null = null;
394
+ let downstreamClosed = false;
395
+ let detachDownstream = (): void => undefined;
396
+ const closeHttpUpstream = (): void => {
397
+ downstreamClosed = true;
398
+ if (httpUpstream === null) {
399
+ detachDownstream();
400
+ return;
401
+ }
402
+ destroyHttpUpstream(httpUpstream);
403
+ };
404
+ const closeIncompleteIncoming = (): void => {
405
+ if (incoming.aborted || !incoming.complete) closeHttpUpstream();
406
+ };
407
+ detachDownstream = (): void => {
408
+ incoming.off("aborted", closeHttpUpstream);
409
+ incoming.off("error", closeHttpUpstream);
410
+ incoming.off("close", closeIncompleteIncoming);
411
+ incoming.socket.off("error", closeHttpUpstream);
412
+ incoming.socket.off("close", closeHttpUpstream);
413
+ outgoing.off("error", closeHttpUpstream);
414
+ };
415
+ incoming.once("aborted", closeHttpUpstream);
416
+ incoming.once("error", closeHttpUpstream);
417
+ incoming.once("close", closeIncompleteIncoming);
418
+ incoming.socket.once("error", closeHttpUpstream);
419
+ incoming.socket.once("close", closeHttpUpstream);
420
+ outgoing.once("error", closeHttpUpstream);
421
+ outgoing.once("close", () => {
422
+ release();
423
+ closeHttpUpstream();
424
+ });
425
+ outgoing.once("finish", release);
426
+
427
+ void (async () => {
428
+ let target: URL;
429
+ try {
430
+ target = parseAbsoluteHttpTarget(incoming.url);
431
+ const declared = incoming.headers["content-length"];
432
+ if (typeof declared === "string") {
433
+ const length = Number(declared);
434
+ if (!Number.isSafeInteger(length) || length < 0 || length > maxRequestBodyBytes) {
435
+ finishResponse(outgoing, 413);
436
+ incoming.destroy();
437
+ return;
438
+ }
439
+ }
440
+ const bodyPromise = readRequestBody(incoming, maxRequestBodyBytes);
441
+ const [addresses, body] = await Promise.all([resolveTarget(target), bodyPromise]);
442
+ const address = addresses[0];
443
+ if (address === undefined) throw new FetchFailure("dns", "destination had no validated address");
444
+ if (downstreamClosed || incoming.aborted || outgoing.destroyed || closing) return;
445
+
446
+ const hostname = target.hostname.startsWith("[") && target.hostname.endsWith("]")
447
+ ? target.hostname.slice(1, -1)
448
+ : target.hostname;
449
+ const headers = forwardedHeaders(incoming.headers);
450
+ headers.host = target.host;
451
+ const requestOptions: RequestOptions = {
452
+ protocol: "http:",
453
+ hostname,
454
+ port: targetPort(target),
455
+ method: incoming.method ?? "GET",
456
+ path: `${target.pathname}${target.search}`,
457
+ headers,
458
+ lookup: createPinnedLookup(address),
459
+ family: address.family,
460
+ agent: false,
461
+ maxHeaderSize: maxHeaderBytes,
462
+ };
463
+ const upstream = requestHttp(requestOptions, (response: IncomingMessage) => {
464
+ const tracked = httpUpstream;
465
+ if (tracked === null || tracked.settled) {
466
+ response.destroy();
467
+ return;
468
+ }
469
+ tracked.response = response;
470
+ response.once("close", () => forgetHttpUpstream(tracked));
471
+ if (outgoing.destroyed || closing) {
472
+ destroyHttpUpstream(tracked);
473
+ return;
474
+ }
475
+ outgoing.writeHead(response.statusCode ?? 502, response.statusMessage, responseHeaders(response.headers));
476
+ response.on("data", (chunk: unknown) => {
477
+ const size = typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk instanceof Uint8Array ? chunk.byteLength : 0;
478
+ if (!accountTransfer(size)) {
479
+ response.destroy(new Error("proxy transfer limit exceeded"));
480
+ outgoing.destroy();
481
+ }
482
+ });
483
+ response.once("error", () => {
484
+ destroyHttpUpstream(tracked);
485
+ outgoing.destroy();
486
+ });
487
+ response.pipe(outgoing);
488
+ });
489
+ httpUpstream = {
490
+ request: upstream,
491
+ detachDownstream,
492
+ response: null,
493
+ socket: null,
494
+ settled: false,
495
+ };
496
+ httpUpstreams.add(httpUpstream);
497
+ const tracked = httpUpstream;
498
+ upstream.once("socket", (socket: Socket) => {
499
+ if (tracked.settled || closing) {
500
+ socket.destroy();
501
+ return;
502
+ }
503
+ tracked.socket = socket;
504
+ trackSocket(socket, sockets, timeoutMs);
505
+ });
506
+ upstream.setTimeout(timeoutMs, () => upstream.destroy(new Error("proxy request timed out")));
507
+ upstream.once("error", () => {
508
+ destroyHttpUpstream(tracked);
509
+ finishResponse(outgoing, 502);
510
+ });
511
+ upstream.once("close", () => {
512
+ if (tracked.response === null) forgetHttpUpstream(tracked);
513
+ });
514
+ if (downstreamClosed || outgoing.destroyed || closing) {
515
+ destroyHttpUpstream(tracked);
516
+ return;
517
+ }
518
+ if (!accountTransfer(body.byteLength)) {
519
+ upstream.destroy(new Error("proxy transfer limit exceeded"));
520
+ finishResponse(outgoing, 413);
521
+ return;
522
+ }
523
+ upstream.end(body);
524
+ } catch (error) {
525
+ finishResponse(outgoing, proxyStatusFor(error));
526
+ }
527
+ })();
528
+ });
529
+
530
+ server.on("connect", (request, client, head) => {
531
+ if (!reserveRequest()) {
532
+ finishSocket(client, 503);
533
+ return;
534
+ }
535
+ let released = false;
536
+ const release = (): void => {
537
+ if (released) return;
538
+ released = true;
539
+ releaseRequest();
540
+ };
541
+ client.once("close", release);
542
+ trackSocket(client, sockets, timeoutMs);
543
+
544
+ void (async () => {
545
+ try {
546
+ const target = connectAuthority(request.url ?? "");
547
+ const addresses = await resolveTarget(target);
548
+ if (client.destroyed) return;
549
+ const upstream = await connectPinned(
550
+ addresses,
551
+ targetPort(target),
552
+ timeoutMs,
553
+ sockets,
554
+ connectAddress,
555
+ () => closing,
556
+ );
557
+ if (client.destroyed || closing) {
558
+ upstream.destroy();
559
+ return;
560
+ }
561
+ client.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: hraness-kb\r\n\r\n");
562
+ if (head.byteLength > 0) {
563
+ if (!accountTransfer(head.byteLength)) {
564
+ client.destroy();
565
+ upstream.destroy();
566
+ return;
567
+ }
568
+ upstream.write(head);
569
+ }
570
+ client.on("data", (chunk: Buffer) => {
571
+ if (!accountTransfer(chunk.byteLength)) {
572
+ client.destroy();
573
+ upstream.destroy();
574
+ }
575
+ });
576
+ upstream.on("data", (chunk: Buffer) => {
577
+ if (!accountTransfer(chunk.byteLength)) {
578
+ client.destroy();
579
+ upstream.destroy();
580
+ }
581
+ });
582
+ client.once("error", () => upstream.destroy());
583
+ upstream.once("error", () => client.destroy());
584
+ client.pipe(upstream);
585
+ upstream.pipe(client);
586
+ } catch (error) {
587
+ finishSocket(client, proxyStatusFor(error));
588
+ }
589
+ })();
590
+ });
591
+
592
+ server.on("upgrade", (_request, socket) => finishSocket(socket, 501));
593
+ server.on("clientError", (_error, socket) => finishSocket(socket, 400));
594
+ server.on("connection", (socket) => trackSocket(socket, sockets, timeoutMs));
595
+
596
+ await new Promise<void>((resolve, reject) => {
597
+ const onError = (error: Error): void => reject(error);
598
+ server.once("error", onError);
599
+ server.listen(0, "127.0.0.1", () => {
600
+ server.off("error", onError);
601
+ resolve();
602
+ });
603
+ });
604
+ const address = server.address();
605
+ if (address === null || typeof address === "string") {
606
+ server.close();
607
+ throw new Error("network proxy did not bind a TCP port");
608
+ }
609
+
610
+ let closePromise: Promise<void> | null = null;
611
+ return {
612
+ url: `http://127.0.0.1:${address.port}`,
613
+ port: address.port,
614
+ close: () => {
615
+ if (closePromise !== null) return closePromise;
616
+ closing = true;
617
+ for (const upstream of [...httpUpstreams]) destroyHttpUpstream(upstream);
618
+ for (const socket of sockets) socket.destroy();
619
+ closePromise = new Promise<void>((resolveClose) => {
620
+ server.close(() => resolveClose());
621
+ server.closeAllConnections?.();
622
+ server.closeIdleConnections?.();
623
+ });
624
+ return closePromise;
625
+ },
626
+ };
627
+ }