@arnilo/prism-tools 0.0.0-stage → 0.13.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 (331) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +29 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +584 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,599 @@
1
+ /** Allow-list egress proxy: HTTP forward proxy + CONNECT tunnel, deny-all default, pinned DNS, audit. */
2
+
3
+ import { randomUUID } from "node:crypto";
4
+ import { createServer, request as httpRequest, type IncomingMessage, type Server, type ServerResponse } from "node:http";
5
+ import { connect as netConnect, type Socket } from "node:net";
6
+ import type { Readable, Writable } from "node:stream";
7
+ import { Semaphore } from "../semaphore.js";
8
+ import { type AddressResolver, assertPinned, isPrivateAddress, resolvePinned } from "./dns-pin.js";
9
+ import { type EgressLimitOptions, type ResolvedEgressLimits, resolveEgressLimits } from "./limits.js";
10
+ import type { EgressPolicy } from "./policy.js";
11
+ import { type EgressAuditRecord, EgressError, type EgressErrorCode } from "./types.js";
12
+
13
+ export interface EgressProxyEndpoint {
14
+ readonly host: string;
15
+ readonly port: number;
16
+ }
17
+
18
+ export interface EgressAttestation {
19
+ readonly proxyEndpoint: string;
20
+ readonly denyDirectEgress: true;
21
+ readonly policyFingerprint: string;
22
+ readonly policyVersion: number;
23
+ readonly startedAt: string;
24
+ }
25
+
26
+ export interface EgressProxyStats {
27
+ readonly activeConnections: number;
28
+ readonly totalConnections: number;
29
+ readonly totalBytes: number;
30
+ readonly denied: number;
31
+ }
32
+
33
+ export interface EgressProxy {
34
+ /** Bind the listener. Inert until called; default 127.0.0.1:0. */
35
+ readonly start: (options?: { readonly host?: string; readonly port?: number }) => Promise<EgressProxyEndpoint>;
36
+ readonly endpoint: () => EgressProxyEndpoint;
37
+ /** Attestation for sandbox composition; throws until started. */
38
+ readonly attestation: () => EgressAttestation;
39
+ /** Explicit policy reload; bumps policyVersion. */
40
+ readonly reloadPolicy: (policy: EgressPolicy) => void;
41
+ readonly stats: () => EgressProxyStats;
42
+ readonly close: () => Promise<void>;
43
+ }
44
+
45
+ export interface CreateAllowListEgressProxyOptions {
46
+ readonly policy: EgressPolicy;
47
+ readonly audit?: (record: EgressAuditRecord) => void;
48
+ readonly limits?: EgressLimitOptions;
49
+ /** Test seam: replace DNS resolution. */
50
+ readonly resolve?: AddressResolver;
51
+ }
52
+
53
+ interface Target {
54
+ readonly host: string;
55
+ readonly port: number;
56
+ readonly protocol: "http" | "https";
57
+ readonly path: string;
58
+ readonly hostHeader: string;
59
+ }
60
+
61
+ const REDIRECT_CODES = new Set([301, 302, 303, 307, 308]);
62
+
63
+ function splitHostPort(authority: string): { host: string; port: number | undefined } {
64
+ const trimmed = authority.trim();
65
+ if (trimmed.startsWith("[")) {
66
+ const close = trimmed.indexOf("]");
67
+ if (close < 0) return { host: trimmed, port: undefined };
68
+ const rest = trimmed.slice(close + 1);
69
+ return { host: trimmed.slice(1, close), port: rest.startsWith(":") ? Number(rest.slice(1)) : undefined };
70
+ }
71
+ const colon = trimmed.lastIndexOf(":");
72
+ if (colon < 0) return { host: trimmed, port: undefined };
73
+ const port = Number(trimmed.slice(colon + 1));
74
+ return { host: trimmed.slice(0, colon), port: Number.isInteger(port) && port > 0 ? port : undefined };
75
+ }
76
+
77
+ function parseTarget(req: IncomingMessage): Target | undefined {
78
+ const url = req.url ?? "";
79
+ if (url.startsWith("http://") || url.startsWith("https://")) {
80
+ let parsed: URL;
81
+ try {
82
+ parsed = new URL(url);
83
+ } catch {
84
+ return undefined;
85
+ }
86
+ const protocol = parsed.protocol === "https:" ? "https" : "http";
87
+ if (protocol === "https") return undefined; // https absolute-form requires CONNECT; fail closed
88
+ return {
89
+ host: parsed.hostname,
90
+ port: parsed.port ? Number(parsed.port) : 80,
91
+ protocol,
92
+ path: parsed.pathname + parsed.search,
93
+ hostHeader: parsed.host,
94
+ };
95
+ }
96
+ const hostHeader = req.headers.host;
97
+ if (!hostHeader) return undefined;
98
+ const { host, port } = splitHostPort(hostHeader);
99
+ if (!host) return undefined;
100
+ return { host, port: port ?? 80, protocol: "http", path: url, hostHeader };
101
+ }
102
+
103
+ function parseLocation(location: string, base: Target): Target | undefined {
104
+ let parsed: URL;
105
+ try {
106
+ parsed = new URL(location, `http://${base.hostHeader}`);
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ if (parsed.protocol !== "http:") return undefined; // https redirects require CONNECT; fail closed
111
+ return {
112
+ host: parsed.hostname,
113
+ port: parsed.port ? Number(parsed.port) : 80,
114
+ protocol: "http",
115
+ path: parsed.pathname + parsed.search,
116
+ hostHeader: parsed.host,
117
+ };
118
+ }
119
+
120
+ /** Bounded, backpressured pipe with a byte cap. */
121
+ function pipeBounded(src: Readable, dest: Writable, maxBytes: number, onExceed: () => void, onBytes?: (n: number) => void): void {
122
+ let total = 0;
123
+ src.on("data", (chunk: Buffer) => {
124
+ total += chunk.length;
125
+ onBytes?.(chunk.length);
126
+ if (total > maxBytes) {
127
+ onExceed();
128
+ src.destroy();
129
+ dest.destroy();
130
+ return;
131
+ }
132
+ if (!dest.write(chunk)) {
133
+ src.pause();
134
+ dest.once("drain", () => src.resume());
135
+ }
136
+ });
137
+ src.on("end", () => dest.end());
138
+ src.on("error", () => dest.destroy());
139
+ dest.on("error", () => src.destroy());
140
+ }
141
+
142
+ export function createAllowListEgressProxy(options: CreateAllowListEgressProxyOptions): EgressProxy {
143
+ const limits: ResolvedEgressLimits = resolveEgressLimits(options.limits);
144
+ let policy = options.policy;
145
+ let policyVersion = 0;
146
+ let started = false;
147
+ let startedAt = "";
148
+ let endpoint: EgressProxyEndpoint | undefined;
149
+ let server: Server | undefined;
150
+ let active = 0;
151
+ let total = 0;
152
+ let totalBytes = 0;
153
+ let denied = 0;
154
+ const sockets = new Set<Socket>();
155
+ const semaphore = new Semaphore(limits.concurrentConnections);
156
+
157
+ const audit = (record: Omit<EgressAuditRecord, "id" | "ts">): void => {
158
+ options.audit?.({ id: randomUUID(), ts: new Date().toISOString(), ...record });
159
+ };
160
+
161
+ const failHttp = (res: ServerResponse, status: number, code: EgressErrorCode, message: string): void => {
162
+ if (res.headersSent || res.destroyed) {
163
+ // Mid-stream failure: destroy the socket, not just the response. A partial
164
+ // body behind a forwarded content-length must reach the client as a
165
+ // connection error (ECONNRESET), never as a clean short read — Node's
166
+ // res.destroy() does that, Bun's resolves the client with a 200 instead.
167
+ res.socket?.destroy();
168
+ res.destroy();
169
+ return;
170
+ }
171
+ res.writeHead(status, { "content-type": "text/plain" });
172
+ res.end(`${code}: ${message}`);
173
+ };
174
+
175
+ const handleHttp = (req: IncomingMessage, res: ServerResponse): void => {
176
+ const clientAddress = req.socket.remoteAddress;
177
+ const startedAtMs = Date.now();
178
+ const target = parseTarget(req);
179
+ if (!target) {
180
+ denied += 1;
181
+ audit({
182
+ decision: "deny",
183
+ host: req.headers.host ?? "?",
184
+ port: 0,
185
+ protocol: "http",
186
+ reason: "ERR_PRISM_EGRESS_POLICY: malformed target",
187
+ clientAddress,
188
+ });
189
+ failHttp(res, 400, "ERR_PRISM_EGRESS_POLICY", "malformed request target");
190
+ return;
191
+ }
192
+ const deadline = setTimeout(() => {
193
+ audit({
194
+ decision: "deny",
195
+ host: target.host,
196
+ port: target.port,
197
+ protocol: target.protocol,
198
+ reason: "ERR_PRISM_EGRESS_LIMIT: transfer time exceeded",
199
+ durationMs: Date.now() - startedAtMs,
200
+ clientAddress,
201
+ });
202
+ failHttp(res, 504, "ERR_PRISM_EGRESS_LIMIT", "transfer time exceeded");
203
+ }, limits.transferTimeMs);
204
+
205
+ const run = async (current: Target, hop: number, isFirstHop: boolean): Promise<void> => {
206
+ const rule = policy.match(current.host, current.port, current.protocol);
207
+ if (!rule) {
208
+ denied += 1;
209
+ audit({
210
+ decision: "deny",
211
+ host: current.host,
212
+ port: current.port,
213
+ protocol: current.protocol,
214
+ reason: "ERR_PRISM_EGRESS_DENIED: no matching rule",
215
+ durationMs: Date.now() - startedAtMs,
216
+ clientAddress,
217
+ });
218
+ failHttp(res, 403, "ERR_PRISM_EGRESS_DENIED", `egress denied: ${current.host}:${current.port} ${current.protocol}`);
219
+ return;
220
+ }
221
+ let pinned: readonly string[];
222
+ try {
223
+ pinned = await resolvePinned(current.host, options.resolve);
224
+ } catch (error) {
225
+ denied += 1;
226
+ audit({
227
+ decision: "deny",
228
+ host: current.host,
229
+ port: current.port,
230
+ protocol: current.protocol,
231
+ reason: "ERR_PRISM_EGRESS_DNS",
232
+ durationMs: Date.now() - startedAtMs,
233
+ clientAddress,
234
+ });
235
+ failHttp(res, 502, "ERR_PRISM_EGRESS_DNS", error instanceof Error ? error.message : "DNS failure");
236
+ return;
237
+ }
238
+ const connectable = pinned.filter((ip) => !isPrivateAddress(ip) || rule.allowPrivate === true);
239
+ if (connectable.length === 0) {
240
+ denied += 1;
241
+ audit({
242
+ decision: "deny",
243
+ host: current.host,
244
+ port: current.port,
245
+ protocol: current.protocol,
246
+ reason: "ERR_PRISM_EGRESS_DNS: no connectable pinned address",
247
+ durationMs: Date.now() - startedAtMs,
248
+ clientAddress,
249
+ });
250
+ failHttp(res, 502, "ERR_PRISM_EGRESS_DNS", "no connectable pinned address");
251
+ return;
252
+ }
253
+ const upstream = httpRequest({
254
+ host: connectable[0],
255
+ port: current.port,
256
+ method: req.method,
257
+ path: current.path,
258
+ headers: { ...req.headers, host: current.hostHeader },
259
+ setHost: false,
260
+ });
261
+ if (isFirstHop) {
262
+ pipeBounded(req, upstream, limits.requestBytes, () => {
263
+ audit({
264
+ decision: "deny",
265
+ host: current.host,
266
+ port: current.port,
267
+ protocol: current.protocol,
268
+ reason: "ERR_PRISM_EGRESS_LIMIT: request bytes exceeded",
269
+ durationMs: Date.now() - startedAtMs,
270
+ clientAddress,
271
+ });
272
+ failHttp(res, 413, "ERR_PRISM_EGRESS_LIMIT", "request bytes exceeded");
273
+ });
274
+ } else {
275
+ upstream.end();
276
+ }
277
+ const upRes = await new Promise<IncomingMessage>((resolve, reject) => {
278
+ upstream.once("response", resolve);
279
+ upstream.once("error", reject);
280
+ });
281
+ try {
282
+ assertPinned(current.host, upstream.socket?.remoteAddress, connectable);
283
+ } catch (error) {
284
+ denied += 1;
285
+ audit({
286
+ decision: "deny",
287
+ host: current.host,
288
+ port: current.port,
289
+ protocol: current.protocol,
290
+ reason: "ERR_PRISM_EGRESS_DNS: rebinding detected",
291
+ durationMs: Date.now() - startedAtMs,
292
+ clientAddress,
293
+ });
294
+ failHttp(res, 502, "ERR_PRISM_EGRESS_DNS", error instanceof Error ? error.message : "rebinding detected");
295
+ return;
296
+ }
297
+ if (REDIRECT_CODES.has(upRes.statusCode ?? 0) && upRes.headers.location) {
298
+ upRes.resume();
299
+ if (hop >= limits.redirectHops) {
300
+ denied += 1;
301
+ audit({
302
+ decision: "deny",
303
+ host: current.host,
304
+ port: current.port,
305
+ protocol: current.protocol,
306
+ reason: "ERR_PRISM_EGRESS_LIMIT: redirect hops exceeded",
307
+ durationMs: Date.now() - startedAtMs,
308
+ clientAddress,
309
+ });
310
+ failHttp(res, 502, "ERR_PRISM_EGRESS_LIMIT", "redirect hops exceeded");
311
+ return;
312
+ }
313
+ const next = parseLocation(upRes.headers.location, current);
314
+ if (!next) {
315
+ denied += 1;
316
+ audit({
317
+ decision: "deny",
318
+ host: current.host,
319
+ port: current.port,
320
+ protocol: current.protocol,
321
+ reason: "ERR_PRISM_EGRESS_POLICY: redirect target not http",
322
+ durationMs: Date.now() - startedAtMs,
323
+ clientAddress,
324
+ });
325
+ failHttp(res, 502, "ERR_PRISM_EGRESS_POLICY", "redirect target not http");
326
+ return;
327
+ }
328
+ await run(next, hop + 1, false);
329
+ return;
330
+ }
331
+ res.writeHead(upRes.statusCode ?? 502, upRes.headers);
332
+ pipeBounded(
333
+ upRes,
334
+ res,
335
+ limits.responseBytes,
336
+ () => {
337
+ audit({
338
+ decision: "deny",
339
+ host: current.host,
340
+ port: current.port,
341
+ protocol: current.protocol,
342
+ reason: "ERR_PRISM_EGRESS_LIMIT: response bytes exceeded",
343
+ durationMs: Date.now() - startedAtMs,
344
+ clientAddress,
345
+ });
346
+ failHttp(res, 502, "ERR_PRISM_EGRESS_LIMIT", "response bytes exceeded");
347
+ },
348
+ (n) => {
349
+ totalBytes += n;
350
+ },
351
+ );
352
+ audit({
353
+ decision: "allow",
354
+ host: current.host,
355
+ port: current.port,
356
+ protocol: current.protocol,
357
+ durationMs: Date.now() - startedAtMs,
358
+ clientAddress,
359
+ });
360
+ };
361
+
362
+ void (async () => {
363
+ const release = await semaphore.acquire();
364
+ active += 1;
365
+ total += 1;
366
+ res.once("close", () => {
367
+ clearTimeout(deadline);
368
+ active -= 1;
369
+ release();
370
+ });
371
+ try {
372
+ await run(target, 1, true);
373
+ } catch {
374
+ denied += 1;
375
+ audit({
376
+ decision: "deny",
377
+ host: target.host,
378
+ port: target.port,
379
+ protocol: target.protocol,
380
+ reason: "ERR_PRISM_EGRESS_DNS: upstream failure",
381
+ durationMs: Date.now() - startedAtMs,
382
+ clientAddress,
383
+ });
384
+ failHttp(res, 502, "ERR_PRISM_EGRESS_DNS", "upstream failure");
385
+ }
386
+ })();
387
+ };
388
+
389
+ const handleConnect = (req: IncomingMessage, socket: Socket, head: Buffer): void => {
390
+ const clientAddress = socket.remoteAddress;
391
+ const startedAtMs = Date.now();
392
+ const { host, port } = splitHostPort(req.url ?? "");
393
+ const deadline = setTimeout(() => {
394
+ audit({
395
+ decision: "deny",
396
+ host,
397
+ port: port ?? 0,
398
+ protocol: "https",
399
+ reason: "ERR_PRISM_EGRESS_LIMIT: transfer time exceeded",
400
+ durationMs: Date.now() - startedAtMs,
401
+ clientAddress,
402
+ });
403
+ socket.destroy();
404
+ }, limits.transferTimeMs);
405
+
406
+ void (async () => {
407
+ const release = await semaphore.acquire();
408
+ active += 1;
409
+ total += 1;
410
+ socket.once("close", () => {
411
+ clearTimeout(deadline);
412
+ active -= 1;
413
+ release();
414
+ });
415
+ try {
416
+ if (!host || port === undefined || !Number.isInteger(port) || port < 1 || port > 65_535) {
417
+ denied += 1;
418
+ audit({
419
+ decision: "deny",
420
+ host: host ?? "?",
421
+ port: port ?? 0,
422
+ protocol: "https",
423
+ reason: "ERR_PRISM_EGRESS_POLICY: malformed CONNECT target",
424
+ clientAddress,
425
+ });
426
+ socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
427
+ socket.destroy();
428
+ return;
429
+ }
430
+ const rule = policy.match(host, port, "https");
431
+ if (!rule) {
432
+ denied += 1;
433
+ audit({ decision: "deny", host, port, protocol: "https", reason: "ERR_PRISM_EGRESS_DENIED: no matching rule", clientAddress });
434
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
435
+ socket.destroy();
436
+ return;
437
+ }
438
+ let pinned: readonly string[];
439
+ try {
440
+ pinned = await resolvePinned(host, options.resolve);
441
+ } catch {
442
+ denied += 1;
443
+ audit({ decision: "deny", host, port, protocol: "https", reason: "ERR_PRISM_EGRESS_DNS", clientAddress });
444
+ socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
445
+ socket.destroy();
446
+ return;
447
+ }
448
+ const connectable = pinned.filter((ip) => !isPrivateAddress(ip) || rule.allowPrivate === true);
449
+ if (connectable.length === 0) {
450
+ denied += 1;
451
+ audit({
452
+ decision: "deny",
453
+ host,
454
+ port,
455
+ protocol: "https",
456
+ reason: "ERR_PRISM_EGRESS_DNS: no connectable pinned address",
457
+ clientAddress,
458
+ });
459
+ socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
460
+ socket.destroy();
461
+ return;
462
+ }
463
+ const upstream = netConnect({ host: connectable[0], port });
464
+ sockets.add(upstream);
465
+ upstream.once("close", () => sockets.delete(upstream));
466
+ upstream.once("error", () => {
467
+ audit({
468
+ decision: "deny",
469
+ host,
470
+ port,
471
+ protocol: "https",
472
+ reason: "ERR_PRISM_EGRESS_DNS: upstream connect failed",
473
+ clientAddress,
474
+ });
475
+ socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
476
+ socket.destroy();
477
+ });
478
+ upstream.once("connect", () => {
479
+ try {
480
+ assertPinned(host, upstream.remoteAddress, connectable);
481
+ } catch (_error) {
482
+ denied += 1;
483
+ audit({ decision: "deny", host, port, protocol: "https", reason: "ERR_PRISM_EGRESS_DNS: rebinding detected", clientAddress });
484
+ socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
485
+ socket.destroy();
486
+ upstream.destroy();
487
+ return;
488
+ }
489
+ socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
490
+ if (head.length > 0) upstream.write(head);
491
+ pipeBounded(
492
+ socket,
493
+ upstream,
494
+ limits.requestBytes,
495
+ () => {
496
+ audit({
497
+ decision: "deny",
498
+ host,
499
+ port,
500
+ protocol: "https",
501
+ reason: "ERR_PRISM_EGRESS_LIMIT: request bytes exceeded",
502
+ clientAddress,
503
+ });
504
+ socket.destroy();
505
+ upstream.destroy();
506
+ },
507
+ (n) => {
508
+ totalBytes += n;
509
+ },
510
+ );
511
+ pipeBounded(
512
+ upstream,
513
+ socket,
514
+ limits.responseBytes,
515
+ () => {
516
+ audit({
517
+ decision: "deny",
518
+ host,
519
+ port,
520
+ protocol: "https",
521
+ reason: "ERR_PRISM_EGRESS_LIMIT: response bytes exceeded",
522
+ clientAddress,
523
+ });
524
+ socket.destroy();
525
+ upstream.destroy();
526
+ },
527
+ (n) => {
528
+ totalBytes += n;
529
+ },
530
+ );
531
+ audit({ decision: "allow", host, port, protocol: "https", clientAddress });
532
+ });
533
+ } catch {
534
+ socket.destroy();
535
+ }
536
+ })();
537
+ };
538
+
539
+ const proxy: EgressProxy = {
540
+ async start({ host = "127.0.0.1", port = 0 } = {}) {
541
+ if (started) return endpoint as EgressProxyEndpoint;
542
+ const srv = createServer(handleHttp);
543
+ srv.on("connect", handleConnect);
544
+ srv.on("connection", (socket) => {
545
+ sockets.add(socket);
546
+ socket.once("close", () => sockets.delete(socket));
547
+ });
548
+ await new Promise<void>((resolve, reject) => {
549
+ srv.once("error", reject);
550
+ srv.listen(port, host, () => {
551
+ srv.removeListener("error", reject);
552
+ resolve();
553
+ });
554
+ });
555
+ const addr = srv.address();
556
+ if (!addr || typeof addr === "string") {
557
+ throw new EgressError("ERR_PRISM_EGRESS_POLICY", "proxy failed to bind");
558
+ }
559
+ server = srv;
560
+ endpoint = { host, port: addr.port };
561
+ started = true;
562
+ startedAt = new Date().toISOString();
563
+ return endpoint;
564
+ },
565
+ endpoint() {
566
+ if (!started || !endpoint) throw new EgressError("ERR_PRISM_EGRESS_ATTESTATION", "proxy not started");
567
+ return endpoint;
568
+ },
569
+ attestation() {
570
+ if (!started || !endpoint) {
571
+ throw new EgressError("ERR_PRISM_EGRESS_ATTESTATION", "proxy not started; no attestation available");
572
+ }
573
+ return {
574
+ proxyEndpoint: `http://${endpoint.host}:${endpoint.port}`,
575
+ denyDirectEgress: true,
576
+ policyFingerprint: policy.fingerprint,
577
+ policyVersion,
578
+ startedAt,
579
+ };
580
+ },
581
+ reloadPolicy(next) {
582
+ policy = next;
583
+ policyVersion += 1;
584
+ },
585
+ stats() {
586
+ return { activeConnections: active, totalConnections: total, totalBytes, denied };
587
+ },
588
+ async close() {
589
+ if (!server) return;
590
+ const srv = server;
591
+ server = undefined;
592
+ started = false;
593
+ for (const socket of sockets) socket.destroy();
594
+ sockets.clear();
595
+ await new Promise<void>((resolve) => srv.close(() => resolve()));
596
+ },
597
+ };
598
+ return proxy;
599
+ }
@@ -0,0 +1,30 @@
1
+ /** Shared egress types: typed errors and audit records. */
2
+
3
+ export type EgressErrorCode =
4
+ | "ERR_PRISM_EGRESS_DENIED"
5
+ | "ERR_PRISM_EGRESS_DNS"
6
+ | "ERR_PRISM_EGRESS_LIMIT"
7
+ | "ERR_PRISM_EGRESS_POLICY"
8
+ | "ERR_PRISM_EGRESS_ATTESTATION";
9
+
10
+ export class EgressError extends Error {
11
+ readonly code: EgressErrorCode;
12
+ constructor(code: EgressErrorCode, message: string) {
13
+ super(message);
14
+ this.name = "EgressError";
15
+ this.code = code;
16
+ }
17
+ }
18
+
19
+ export interface EgressAuditRecord {
20
+ readonly id: string;
21
+ readonly ts: string;
22
+ readonly decision: "allow" | "deny";
23
+ readonly host: string;
24
+ readonly port: number;
25
+ readonly protocol: "http" | "https";
26
+ readonly reason?: string;
27
+ readonly bytes?: number;
28
+ readonly durationMs?: number;
29
+ readonly clientAddress?: string;
30
+ }