harnery 0.42.0 → 0.44.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 (343) hide show
  1. package/README.md +3 -3
  2. package/dist/commander.d.ts +16 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +18 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +95 -52
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +38 -11
  9. package/dist/commands/diagnostics.d.ts.map +1 -1
  10. package/dist/commands/diagnostics.js +10 -0
  11. package/dist/commands/doctor.d.ts +12 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +22 -7
  14. package/dist/commands/governor.js +22 -10
  15. package/dist/commands/inbox.d.ts +1 -1
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +55 -2
  18. package/dist/commands/rm.d.ts.map +1 -1
  19. package/dist/commands/rm.js +28 -4
  20. package/dist/commands/servers.d.ts.map +1 -1
  21. package/dist/commands/servers.js +16 -1
  22. package/dist/commands/supervisor.d.ts.map +1 -1
  23. package/dist/commands/supervisor.js +7 -2
  24. package/dist/commands/toc.d.ts +14 -0
  25. package/dist/commands/toc.d.ts.map +1 -1
  26. package/dist/commands/toc.js +7 -4
  27. package/dist/commands/tunnel.d.ts +65 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -1
  29. package/dist/commands/tunnel.js +290 -101
  30. package/dist/commands/web-fetch.d.ts +3 -2
  31. package/dist/commands/web-fetch.d.ts.map +1 -1
  32. package/dist/commands/web-fetch.js +25 -19
  33. package/dist/commands/web.d.ts +7 -1
  34. package/dist/commands/web.d.ts.map +1 -1
  35. package/dist/commands/web.js +9 -3
  36. package/dist/commands/workflow.js +24 -30
  37. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  38. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  39. package/dist/core/agents/live-authority-v3.js +11 -0
  40. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  41. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  42. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  43. package/dist/core/agents/mailbox.d.ts +9 -8
  44. package/dist/core/agents/mailbox.d.ts.map +1 -1
  45. package/dist/core/agents/mailbox.js +24 -17
  46. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  47. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  48. package/dist/core/agents/render/prompt-context.js +19 -17
  49. package/dist/core/agents/render/session-context.d.ts +2 -8
  50. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  51. package/dist/core/agents/render/session-context.js +64 -43
  52. package/dist/core/agents/rules/stop-hook.js +3 -3
  53. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  54. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  55. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  57. package/dist/core/agents/state/names.d.ts +43 -3
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +87 -3
  60. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  61. package/dist/core/agents/state/pidmap.js +5 -0
  62. package/dist/core/agents/state/proc-start.d.ts +19 -0
  63. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  64. package/dist/core/agents/state/proc-start.js +41 -0
  65. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  66. package/dist/core/agents/state/stale-sweep.js +180 -70
  67. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  68. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  69. package/dist/core/agents/state/unchanged-file.js +390 -0
  70. package/dist/core/context/index.d.ts +22 -10
  71. package/dist/core/context/index.d.ts.map +1 -1
  72. package/dist/core/context/index.js +381 -119
  73. package/dist/core/context/observation.d.ts +26 -0
  74. package/dist/core/context/observation.d.ts.map +1 -0
  75. package/dist/core/context/observation.js +11 -0
  76. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  77. package/dist/core/diagnostics/bundle.js +25 -14
  78. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  79. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  80. package/dist/core/diagnostics/path-identity.js +12 -0
  81. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  82. package/dist/core/diagnostics/sanitize.js +4 -3
  83. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  84. package/dist/core/events/v3/authority-outbox.js +29 -0
  85. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  86. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  87. package/dist/core/events/v3/generation-custody.js +50 -0
  88. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  89. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  90. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  91. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  92. package/dist/core/events/v3/producers/recorder.js +77 -64
  93. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  94. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  95. package/dist/core/governor/planning.d.ts.map +1 -1
  96. package/dist/core/governor/planning.js +14 -1
  97. package/dist/core/governor/service-read.d.ts +10 -2
  98. package/dist/core/governor/service-read.d.ts.map +1 -1
  99. package/dist/core/governor/service-read.js +105 -22
  100. package/dist/core/governor/service.d.ts +20 -2
  101. package/dist/core/governor/service.d.ts.map +1 -1
  102. package/dist/core/governor/service.js +72 -47
  103. package/dist/core/hooks/cli.js +120 -23
  104. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  105. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  106. package/dist/core/hooks/fork-lineage.js +62 -0
  107. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  108. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  109. package/dist/core/hooks/resolve/transcript.js +84 -0
  110. package/dist/core/hooks/shell-rm.d.ts +2 -0
  111. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  112. package/dist/core/hooks/shell-rm.js +105 -0
  113. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  114. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  115. package/dist/core/hooks/shell-tokens.js +97 -0
  116. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  117. package/dist/core/hooks/shell-waiter.js +3 -93
  118. package/dist/core/inbox/contract.d.ts +2 -1
  119. package/dist/core/inbox/contract.d.ts.map +1 -1
  120. package/dist/core/inbox/index.d.ts +2 -0
  121. package/dist/core/inbox/index.d.ts.map +1 -1
  122. package/dist/core/inbox/index.js +2 -0
  123. package/dist/core/inbox/peer-messages.d.ts +37 -0
  124. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  125. package/dist/core/inbox/peer-messages.js +72 -0
  126. package/dist/core/inbox/service.d.ts +8 -1
  127. package/dist/core/inbox/service.d.ts.map +1 -1
  128. package/dist/core/inbox/service.js +67 -21
  129. package/dist/core/inbox/targets.d.ts +22 -0
  130. package/dist/core/inbox/targets.d.ts.map +1 -0
  131. package/dist/core/inbox/targets.js +108 -0
  132. package/dist/core/inbox/watcher.js +7 -3
  133. package/dist/core/journal/index.d.ts.map +1 -1
  134. package/dist/core/journal/index.js +17 -2
  135. package/dist/core/resources/contract.d.ts +3 -0
  136. package/dist/core/resources/contract.d.ts.map +1 -1
  137. package/dist/core/resources/service-status.d.ts +14 -1
  138. package/dist/core/resources/service-status.d.ts.map +1 -1
  139. package/dist/core/resources/service-status.js +23 -13
  140. package/dist/core/resources/service.d.ts +9 -1
  141. package/dist/core/resources/service.d.ts.map +1 -1
  142. package/dist/core/resources/service.js +47 -27
  143. package/dist/core/resources/status.d.ts +2 -5
  144. package/dist/core/resources/status.d.ts.map +1 -1
  145. package/dist/core/resources/status.js +36 -7
  146. package/dist/core/semantic/contract.d.ts +5 -5
  147. package/dist/core/servers/index.d.ts +24 -13
  148. package/dist/core/servers/index.d.ts.map +1 -1
  149. package/dist/core/servers/index.js +132 -92
  150. package/dist/core/servers/tunnels.d.ts +3 -5
  151. package/dist/core/servers/tunnels.d.ts.map +1 -1
  152. package/dist/core/servers/tunnels.js +3 -9
  153. package/dist/core/storage/process-log.d.ts +12 -0
  154. package/dist/core/storage/process-log.d.ts.map +1 -1
  155. package/dist/core/storage/process-log.js +10 -2
  156. package/dist/core/supervisor/contract.d.ts +12 -0
  157. package/dist/core/supervisor/contract.d.ts.map +1 -1
  158. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  159. package/dist/core/supervisor/explanations.js +2 -1
  160. package/dist/core/supervisor/findings.d.ts.map +1 -1
  161. package/dist/core/supervisor/findings.js +54 -9
  162. package/dist/core/supervisor/service.d.ts +9 -1
  163. package/dist/core/supervisor/service.d.ts.map +1 -1
  164. package/dist/core/supervisor/service.js +59 -29
  165. package/dist/core/supervisor/services.d.ts.map +1 -1
  166. package/dist/core/supervisor/services.js +22 -1
  167. package/dist/core/supervisor/status.d.ts +17 -2
  168. package/dist/core/supervisor/status.d.ts.map +1 -1
  169. package/dist/core/supervisor/status.js +70 -12
  170. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  171. package/dist/core/supervisor/timeline.js +2 -1
  172. package/dist/core/work/state.js +7 -7
  173. package/dist/core/workflow/engine.d.ts.map +1 -1
  174. package/dist/core/workflow/engine.js +105 -32
  175. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  176. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  177. package/dist/core/workflow/failure-evidence.js +114 -0
  178. package/dist/core/workflow/index.d.ts +1 -1
  179. package/dist/core/workflow/index.d.ts.map +1 -1
  180. package/dist/core/workflow/index.js +1 -1
  181. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  182. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  183. package/dist/core/workflow/live-session-v3.js +126 -5
  184. package/dist/core/workflow/proof.d.ts +5 -5
  185. package/dist/core/workflow/proof.d.ts.map +1 -1
  186. package/dist/core/workflow/proof.js +115 -20
  187. package/dist/core/workflow/run-state.d.ts +3 -1
  188. package/dist/core/workflow/run-state.d.ts.map +1 -1
  189. package/dist/core/workflow/run-state.js +31 -27
  190. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  191. package/dist/core/workflow/spawn-claude.js +19 -0
  192. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  193. package/dist/core/workflow/spawn-codex.js +13 -0
  194. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  195. package/dist/core/workflow/spawn-cursor.js +19 -0
  196. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  197. package/dist/core/workflow/spawn-opencode.js +14 -0
  198. package/dist/core/workflow/types.d.ts +64 -17
  199. package/dist/core/workflow/types.d.ts.map +1 -1
  200. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  201. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  202. package/dist/core/workflow/workspaces/index.js +0 -1
  203. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  204. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  205. package/dist/core/workflow/workspaces/leases.js +30 -16
  206. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  207. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  208. package/dist/core/workflow/workspaces/local-git.js +270 -6
  209. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  210. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  211. package/dist/core/workflow/workspaces/ownership.js +194 -0
  212. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  213. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  214. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  215. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  216. package/dist/core/workflow/workspaces/recovery.js +21 -13
  217. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  218. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  219. package/dist/core/workflow/workspaces/state.js +21 -1
  220. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  221. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  222. package/dist/lib/browser/windows-chrome.js +262 -73
  223. package/dist/lib/docs-links.d.ts.map +1 -1
  224. package/dist/lib/docs-links.js +45 -2
  225. package/dist/lib/guarded-remove.d.ts +7 -0
  226. package/dist/lib/guarded-remove.d.ts.map +1 -1
  227. package/dist/lib/guarded-remove.js +106 -12
  228. package/dist/lib/identities/assume.d.ts.map +1 -1
  229. package/dist/lib/identities/assume.js +4 -0
  230. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  231. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  232. package/dist/lib/tunnel/allowlist.js +204 -0
  233. package/dist/lib/tunnel/current-address.d.ts +41 -0
  234. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  235. package/dist/lib/tunnel/current-address.js +123 -0
  236. package/dist/lib/tunnel/gate.js +13 -6
  237. package/dist/lib/tunnel/processes.d.ts +39 -0
  238. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  239. package/dist/lib/tunnel/processes.js +121 -0
  240. package/dist/lib/tunnel/state.d.ts +28 -3
  241. package/dist/lib/tunnel/state.d.ts.map +1 -1
  242. package/dist/lib/tunnel/state.js +98 -27
  243. package/dist/lib/zip/index.d.ts +41 -0
  244. package/dist/lib/zip/index.d.ts.map +1 -0
  245. package/dist/lib/zip/index.js +398 -0
  246. package/package.json +8 -1
  247. package/src/commander.ts +32 -0
  248. package/src/commands/agents.ts +151 -74
  249. package/src/commands/browse.ts +50 -14
  250. package/src/commands/diagnostics.ts +10 -0
  251. package/src/commands/doctor.ts +26 -7
  252. package/src/commands/governor.ts +23 -10
  253. package/src/commands/inbox.ts +52 -2
  254. package/src/commands/rm.ts +43 -4
  255. package/src/commands/servers.ts +16 -4
  256. package/src/commands/supervisor.ts +7 -2
  257. package/src/commands/toc.ts +14 -6
  258. package/src/commands/tunnel.ts +394 -116
  259. package/src/commands/web-fetch.ts +30 -19
  260. package/src/commands/web.ts +9 -3
  261. package/src/commands/workflow.ts +30 -30
  262. package/src/core/agents/live-authority-v3.ts +17 -0
  263. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  264. package/src/core/agents/mailbox.ts +26 -17
  265. package/src/core/agents/render/prompt-context.ts +35 -20
  266. package/src/core/agents/render/session-context.ts +92 -48
  267. package/src/core/agents/rules/stop-hook.ts +3 -3
  268. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  269. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  270. package/src/core/agents/state/names.ts +117 -6
  271. package/src/core/agents/state/pidmap.ts +13 -0
  272. package/src/core/agents/state/proc-start.ts +57 -0
  273. package/src/core/agents/state/stale-sweep.ts +247 -82
  274. package/src/core/agents/state/unchanged-file.ts +456 -0
  275. package/src/core/context/index.ts +446 -136
  276. package/src/core/context/observation.ts +29 -0
  277. package/src/core/diagnostics/bundle.ts +23 -18
  278. package/src/core/diagnostics/path-identity.ts +15 -0
  279. package/src/core/diagnostics/sanitize.ts +4 -3
  280. package/src/core/events/v3/authority-outbox.ts +56 -0
  281. package/src/core/events/v3/generation-custody.ts +59 -0
  282. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  283. package/src/core/events/v3/producers/recorder.ts +94 -68
  284. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  285. package/src/core/governor/planning.ts +20 -8
  286. package/src/core/governor/service-read.ts +128 -20
  287. package/src/core/governor/service.ts +119 -42
  288. package/src/core/hooks/cli.ts +139 -22
  289. package/src/core/hooks/fork-lineage.ts +82 -0
  290. package/src/core/hooks/resolve/transcript.ts +88 -0
  291. package/src/core/hooks/shell-rm.ts +103 -0
  292. package/src/core/hooks/shell-tokens.ts +96 -0
  293. package/src/core/hooks/shell-waiter.ts +4 -91
  294. package/src/core/inbox/contract.ts +2 -1
  295. package/src/core/inbox/index.ts +2 -0
  296. package/src/core/inbox/peer-messages.ts +94 -0
  297. package/src/core/inbox/service.ts +88 -22
  298. package/src/core/inbox/targets.ts +148 -0
  299. package/src/core/inbox/watcher.ts +8 -9
  300. package/src/core/journal/index.ts +19 -2
  301. package/src/core/resources/contract.ts +4 -0
  302. package/src/core/resources/service-status.ts +35 -12
  303. package/src/core/resources/service.ts +71 -24
  304. package/src/core/resources/status.ts +57 -8
  305. package/src/core/servers/index.ts +177 -91
  306. package/src/core/servers/tunnels.ts +3 -11
  307. package/src/core/storage/process-log.ts +17 -2
  308. package/src/core/supervisor/contract.ts +13 -0
  309. package/src/core/supervisor/explanations.ts +4 -1
  310. package/src/core/supervisor/findings.ts +68 -10
  311. package/src/core/supervisor/service.ts +88 -26
  312. package/src/core/supervisor/services.ts +26 -8
  313. package/src/core/supervisor/status.ts +89 -12
  314. package/src/core/supervisor/timeline.ts +2 -1
  315. package/src/core/work/state.ts +7 -7
  316. package/src/core/workflow/engine.ts +131 -49
  317. package/src/core/workflow/failure-evidence.ts +141 -0
  318. package/src/core/workflow/index.ts +0 -1
  319. package/src/core/workflow/live-session-v3.ts +146 -5
  320. package/src/core/workflow/proof.ts +134 -26
  321. package/src/core/workflow/run-state.ts +31 -29
  322. package/src/core/workflow/spawn-claude.ts +18 -0
  323. package/src/core/workflow/spawn-codex.ts +13 -0
  324. package/src/core/workflow/spawn-cursor.ts +18 -0
  325. package/src/core/workflow/spawn-opencode.ts +14 -0
  326. package/src/core/workflow/types.ts +51 -17
  327. package/src/core/workflow/workspaces/index.ts +0 -1
  328. package/src/core/workflow/workspaces/leases.ts +48 -17
  329. package/src/core/workflow/workspaces/local-git.ts +315 -5
  330. package/src/core/workflow/workspaces/ownership.ts +232 -0
  331. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  332. package/src/core/workflow/workspaces/recovery.ts +25 -13
  333. package/src/core/workflow/workspaces/state.ts +20 -0
  334. package/src/lib/browser/windows-chrome.ts +346 -86
  335. package/src/lib/docs-links.ts +49 -2
  336. package/src/lib/guarded-remove.ts +132 -14
  337. package/src/lib/identities/assume.ts +4 -0
  338. package/src/lib/tunnel/allowlist.ts +208 -0
  339. package/src/lib/tunnel/current-address.ts +158 -0
  340. package/src/lib/tunnel/gate.ts +16 -8
  341. package/src/lib/tunnel/processes.ts +149 -0
  342. package/src/lib/tunnel/state.ts +123 -27
  343. package/src/lib/zip/index.ts +483 -0
@@ -0,0 +1,483 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ fstatSync,
6
+ linkSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readdirSync,
11
+ readSync,
12
+ realpathSync,
13
+ renameSync,
14
+ unlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
18
+ import { inflateRawSync } from "node:zlib";
19
+ import { strFromU8, type Zippable, zipSync } from "fflate";
20
+
21
+ export interface ZipLimits {
22
+ archiveBytes: number;
23
+ entryBytes: number;
24
+ totalBytes: number;
25
+ entries: number;
26
+ }
27
+ export const DEFAULT_ZIP_LIMITS: Readonly<ZipLimits> = Object.freeze({
28
+ archiveBytes: 64 * 1024 * 1024,
29
+ entryBytes: 64 * 1024 * 1024,
30
+ totalBytes: 256 * 1024 * 1024,
31
+ entries: 10_000,
32
+ });
33
+ export interface ZipEntryInfo {
34
+ name: string;
35
+ size: number;
36
+ compressedSize: number;
37
+ directory: boolean;
38
+ crc32: number;
39
+ }
40
+ export type ZipSource = string | Uint8Array;
41
+ export interface ReadOptions {
42
+ limits?: Partial<ZipLimits>;
43
+ }
44
+
45
+ function limitsFor(options: ReadOptions): ZipLimits {
46
+ const limits = { ...DEFAULT_ZIP_LIMITS, ...options.limits };
47
+ for (const [key, value] of Object.entries(limits))
48
+ if (!Number.isSafeInteger(value) || value < 1) throw new Error(`Invalid ZIP limit: ${key}`);
49
+ return limits;
50
+ }
51
+
52
+ function readBoundedFile(path: string, limit: number, noFollow = false): Uint8Array {
53
+ const fd = openSync(path, constants.O_RDONLY | (noFollow ? (constants.O_NOFOLLOW ?? 0) : 0));
54
+ try {
55
+ const stat = fstatSync(fd);
56
+ if (!stat.isFile() || stat.size > limit)
57
+ throw new Error("ZIP input exceeds byte limit or is not a file");
58
+ const bytes = Buffer.alloc(stat.size + 1);
59
+ let count = 0;
60
+ while (count < bytes.length) {
61
+ const read = readSync(fd, bytes, count, bytes.length - count, null);
62
+ if (!read) break;
63
+ count += read;
64
+ }
65
+ if (count !== stat.size) throw new Error("ZIP input changed while reading");
66
+ return bytes.subarray(0, count);
67
+ } finally {
68
+ closeSync(fd);
69
+ }
70
+ }
71
+
72
+ function bytesOf(source: ZipSource, limits: ZipLimits): Uint8Array {
73
+ const bytes = typeof source === "string" ? readBoundedFile(source, limits.archiveBytes) : source;
74
+ if (bytes.byteLength > limits.archiveBytes)
75
+ throw new Error("ZIP archive exceeds compressed byte limit");
76
+ return bytes;
77
+ }
78
+
79
+ interface ParsedEntry extends ZipEntryInfo {
80
+ method: number;
81
+ dataOffset: number;
82
+ headerOffset: number;
83
+ }
84
+
85
+ function centralEntries(bytes: Uint8Array, limits: ZipLimits): ParsedEntry[] {
86
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
87
+ let end = bytes.length - 22;
88
+ for (; end >= Math.max(0, bytes.length - 65_557); end--) {
89
+ if (
90
+ view.getUint32(end, true) === 0x06054b50 &&
91
+ end + 22 + view.getUint16(end + 20, true) === bytes.length
92
+ )
93
+ break;
94
+ }
95
+ if (end < Math.max(0, bytes.length - 65_557)) throw new Error("Missing ZIP end record");
96
+ const count = view.getUint16(end + 10, true);
97
+ if (
98
+ view.getUint16(end + 4, true) !== 0 ||
99
+ view.getUint16(end + 6, true) !== 0 ||
100
+ view.getUint16(end + 8, true) !== count ||
101
+ count === 0xffff
102
+ )
103
+ throw new Error("Split or ZIP64 archives are unsupported");
104
+ if (count > limits.entries) throw new Error("ZIP archive exceeds entry count limit");
105
+ const central = view.getUint32(end + 16, true);
106
+ let offset = central;
107
+ if (offset + view.getUint32(end + 12, true) !== end)
108
+ throw new Error("Invalid ZIP directory bounds");
109
+ const entries: ParsedEntry[] = [];
110
+ const names = new Set<string>();
111
+ let total = 0;
112
+ for (let index = 0; index < count; index++) {
113
+ if (offset + 46 > end || view.getUint32(offset, true) !== 0x02014b50)
114
+ throw new Error("Invalid ZIP directory entry");
115
+ const flags = view.getUint16(offset + 8, true);
116
+ const method = view.getUint16(offset + 10, true);
117
+ if (flags & 1) throw new Error("Encrypted ZIP entries are unsupported");
118
+ if (method !== 0 && method !== 8) throw new Error("Unsupported ZIP compression method");
119
+ const nameLength = view.getUint16(offset + 28, true);
120
+ const next =
121
+ offset +
122
+ 46 +
123
+ nameLength +
124
+ view.getUint16(offset + 30, true) +
125
+ view.getUint16(offset + 32, true);
126
+ if (next > end || view.getUint16(offset + 34, true) !== 0)
127
+ throw new Error("Invalid ZIP directory entry bounds");
128
+ const rawName = bytes.subarray(offset + 46, offset + 46 + nameLength);
129
+ const name = strFromU8(rawName, !(flags & 0x800));
130
+ if (names.has(name)) throw new Error(`Duplicate ZIP entry: ${name}`);
131
+ names.add(name);
132
+ const size = view.getUint32(offset + 24, true);
133
+ const compressedSize = view.getUint32(offset + 20, true);
134
+ const checksum = view.getUint32(offset + 16, true);
135
+ const header = view.getUint32(offset + 42, true);
136
+ if (size === 0xffffffff || compressedSize === 0xffffffff || header === 0xffffffff)
137
+ throw new Error("ZIP64 entries are unsupported");
138
+ total += size;
139
+ if (size > limits.entryBytes || total > limits.totalBytes)
140
+ throw new Error("ZIP archive exceeds entry or total byte limit");
141
+ if (header + 30 > central || view.getUint32(header, true) !== 0x04034b50)
142
+ throw new Error("Invalid ZIP local header");
143
+ const localNameLength = view.getUint16(header + 26, true);
144
+ const dataOffset = header + 30 + localNameLength + view.getUint16(header + 28, true);
145
+ if (
146
+ dataOffset + compressedSize > central ||
147
+ localNameLength !== nameLength ||
148
+ view.getUint16(header + 6, true) !== flags ||
149
+ view.getUint16(header + 8, true) !== method ||
150
+ !rawName.every((byte, i) => bytes[header + 30 + i] === byte)
151
+ )
152
+ throw new Error("ZIP local and central header mismatch");
153
+ if (
154
+ !(flags & 8) &&
155
+ (view.getUint32(header + 14, true) !== checksum ||
156
+ view.getUint32(header + 18, true) !== compressedSize ||
157
+ view.getUint32(header + 22, true) !== size)
158
+ )
159
+ throw new Error("ZIP local and central size/checksum mismatch");
160
+ if (method === 0 && compressedSize !== size) throw new Error("Stored ZIP entry size mismatch");
161
+ if (name.endsWith("/") && size !== 0) throw new Error("ZIP directory has a payload");
162
+ entries.push({
163
+ name,
164
+ size,
165
+ compressedSize,
166
+ directory: name.endsWith("/"),
167
+ crc32: checksum,
168
+ method,
169
+ dataOffset,
170
+ headerOffset: header,
171
+ });
172
+ offset = next;
173
+ }
174
+ if (offset !== end) throw new Error("Invalid ZIP directory length");
175
+ const ordered = [...entries].sort((a, b) => a.headerOffset - b.headerOffset);
176
+ for (let i = 1; i < ordered.length; i++)
177
+ if (ordered[i]!.headerOffset < ordered[i - 1]!.dataOffset + ordered[i - 1]!.compressedSize)
178
+ throw new Error("Overlapping ZIP entry ranges");
179
+ return entries;
180
+ }
181
+
182
+ const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, value) => {
183
+ for (let bit = 0; bit < 8; bit++) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
184
+ return value >>> 0;
185
+ });
186
+ function crc32(bytes: Uint8Array): number {
187
+ let crc = 0xffffffff;
188
+ for (const byte of bytes) crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ byte) & 255]!;
189
+ return (crc ^ 0xffffffff) >>> 0;
190
+ }
191
+
192
+ function inventory(bytes: Uint8Array, limits: ZipLimits): ParsedEntry[] {
193
+ try {
194
+ return centralEntries(bytes, limits);
195
+ } catch (error) {
196
+ throw new Error(
197
+ `Unreadable ZIP archive: ${error instanceof Error ? error.message : String(error)}`,
198
+ );
199
+ }
200
+ }
201
+
202
+ export function listZip(source: ZipSource, options: ReadOptions = {}): ZipEntryInfo[] {
203
+ const limits = limitsFor(options);
204
+ return inventory(bytesOf(source, limits), limits).map(
205
+ ({ method, dataOffset, headerOffset, ...entry }) => entry,
206
+ );
207
+ }
208
+
209
+ export function readZipEntries(
210
+ source: ZipSource,
211
+ keep: (name: string) => boolean = () => true,
212
+ options: ReadOptions = {},
213
+ ): Map<string, Uint8Array> {
214
+ const limits = limitsFor(options);
215
+ const bytes = bytesOf(source, limits);
216
+ const selected = inventory(bytes, limits).filter((entry) => !entry.directory && keep(entry.name));
217
+ const result = new Map<string, Uint8Array>();
218
+ for (const entry of selected) {
219
+ const compressed = bytes.subarray(entry.dataOffset, entry.dataOffset + entry.compressedSize);
220
+ // zlib enforces actual output length during decoding, independent of forged headers.
221
+ const data =
222
+ entry.method === 0
223
+ ? compressed.slice()
224
+ : inflateRawSync(compressed, {
225
+ maxOutputLength: Math.max(1, Math.min(entry.size, limits.entryBytes)),
226
+ });
227
+ if (!(data instanceof Uint8Array) || data.byteLength !== entry.size)
228
+ throw new Error(`ZIP entry size or decode mismatch: ${entry.name}`);
229
+ if (crc32(data) !== entry.crc32) throw new Error(`ZIP entry checksum mismatch: ${entry.name}`);
230
+ result.set(entry.name, data);
231
+ }
232
+ return result;
233
+ }
234
+
235
+ export function readZipEntry(
236
+ source: ZipSource,
237
+ name: string,
238
+ options: ReadOptions = {},
239
+ ): Uint8Array | undefined {
240
+ return readZipEntries(source, (entry) => entry === name, options).get(name);
241
+ }
242
+
243
+ export function readZipText(
244
+ source: ZipSource,
245
+ name: string,
246
+ options: ReadOptions = {},
247
+ ): string | undefined {
248
+ const bytes = readZipEntry(source, name, options);
249
+ return bytes ? new TextDecoder().decode(bytes) : undefined;
250
+ }
251
+
252
+ /** Portable names exclude traversal, device names, streams and ambiguous aliases. */
253
+ export function safeEntryPath(root: string, name: string): string {
254
+ const normalized = name.replaceAll("\\", "/");
255
+ const parts = normalized.replace(/\/$/, "").split("/");
256
+ if (
257
+ !name ||
258
+ normalized.startsWith("/") ||
259
+ isAbsolute(normalized) ||
260
+ parts.some(
261
+ (part) =>
262
+ !part ||
263
+ part === "." ||
264
+ part === ".." ||
265
+ [...part].some((character) => character.charCodeAt(0) < 32) ||
266
+ /[:<>"|?*]/.test(part) ||
267
+ /[. ]$/.test(part) ||
268
+ /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part),
269
+ )
270
+ ) {
271
+ throw new Error(`Unsafe ZIP entry path: ${name}`);
272
+ }
273
+ const base = resolve(root);
274
+ const target = resolve(base, ...parts);
275
+ if (!target.startsWith(base.endsWith(sep) ? base : base + sep))
276
+ throw new Error(`ZIP entry escapes destination: ${name}`);
277
+ return target;
278
+ }
279
+
280
+ function statIfPresent(path: string) {
281
+ try {
282
+ return lstatSync(path);
283
+ } catch (error) {
284
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
285
+ throw error;
286
+ }
287
+ }
288
+
289
+ const outside = (rel: string): boolean =>
290
+ rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
291
+
292
+ /**
293
+ * Canonicalize the directory holding a caller-named path. The caller chose
294
+ * that location, so links among its ancestors (macOS /tmp and /var, a linked
295
+ * home directory) are resolved once here. The named path itself and everything
296
+ * below it stay subject to assertNoLinks, which is where a planted link could
297
+ * redirect a write. Missing directories are appended unresolved and checked
298
+ * again once they exist.
299
+ */
300
+ function anchored(path: string): { anchor: string; path: string } {
301
+ const named = resolve(path);
302
+ const missing = [basename(named)];
303
+ let existing = dirname(named);
304
+ while (!statIfPresent(existing) && dirname(existing) !== existing) {
305
+ missing.unshift(basename(existing));
306
+ existing = dirname(existing);
307
+ }
308
+ const anchor = realpathSync(existing);
309
+ return { anchor, path: join(anchor, ...missing) };
310
+ }
311
+
312
+ /**
313
+ * Inspect every existing component from the path up to, not including, its
314
+ * anchor, including a dangling link at the final path.
315
+ */
316
+ function assertNoLinks(path: string, leaf: "file" | "directory", anchor: string): void {
317
+ let current = resolve(path);
318
+ if (outside(relative(anchor, current)))
319
+ throw new Error(`ZIP path is outside its anchored directory: ${current}`);
320
+ let first = true;
321
+ while (current !== anchor) {
322
+ const stat = statIfPresent(current);
323
+ if (stat) {
324
+ if (stat.isSymbolicLink())
325
+ throw new Error(`Refusing ZIP path through a symbolic link: ${current}`);
326
+ if ((!first || leaf === "directory") && !stat.isDirectory())
327
+ throw new Error(`ZIP directory path is not a directory: ${current}`);
328
+ if (first && leaf === "file" && !stat.isFile())
329
+ throw new Error(`ZIP output is not a regular file: ${current}`);
330
+ }
331
+ current = dirname(current);
332
+ first = false;
333
+ }
334
+ }
335
+
336
+ function writeAtomic(path: string, bytes: Uint8Array, overwrite: boolean, anchor: string): void {
337
+ assertNoLinks(path, "file", anchor);
338
+ if (!overwrite && statIfPresent(path)) throw new Error(`ZIP output already exists: ${path}`);
339
+ mkdirSync(dirname(path), { recursive: true });
340
+ assertNoLinks(dirname(path), "directory", anchor);
341
+ const temp = join(dirname(path), `.zip-${randomUUID()}.tmp`);
342
+ let fd: number | undefined;
343
+ try {
344
+ fd = openSync(
345
+ temp,
346
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0),
347
+ 0o600,
348
+ );
349
+ writeFileSync(fd, bytes);
350
+ closeSync(fd);
351
+ fd = undefined;
352
+ assertNoLinks(path, "file", anchor);
353
+ if (overwrite) renameSync(temp, path);
354
+ else {
355
+ linkSync(temp, path);
356
+ unlinkSync(temp);
357
+ }
358
+ } finally {
359
+ if (fd !== undefined) closeSync(fd);
360
+ if (statIfPresent(temp)) unlinkSync(temp);
361
+ }
362
+ }
363
+
364
+ export interface ExtractOptions extends ReadOptions {
365
+ overwrite?: boolean;
366
+ filter?: (name: string) => boolean;
367
+ }
368
+
369
+ /** Preflight all selected names, collisions and existing filesystem paths before writing. */
370
+ export function extractZip(
371
+ source: ZipSource,
372
+ dest: string,
373
+ options: ExtractOptions = {},
374
+ ): string[] {
375
+ const limits = limitsFor(options);
376
+ const bytes = bytesOf(source, limits);
377
+ const entries = inventory(bytes, limits).filter((entry) =>
378
+ (options.filter ?? (() => true))(entry.name),
379
+ );
380
+ const { anchor, path: root } = anchored(dest);
381
+ assertNoLinks(root, "directory", anchor);
382
+ const targets = new Map<string, boolean>();
383
+ for (const entry of entries) {
384
+ const target = safeEntryPath(root, entry.name);
385
+ const key = target.toLowerCase();
386
+ if (targets.has(key)) throw new Error(`Colliding ZIP entry: ${entry.name}`);
387
+ targets.set(key, entry.directory);
388
+ assertNoLinks(target, entry.directory ? "directory" : "file", anchor);
389
+ if (!entry.directory && !options.overwrite && statIfPresent(target))
390
+ throw new Error(`ZIP output already exists: ${target}`);
391
+ }
392
+ for (const target of targets.keys()) {
393
+ let parent = dirname(target);
394
+ while (parent !== dirname(parent)) {
395
+ if (targets.get(parent) === false) throw new Error(`ZIP file/directory collision: ${parent}`);
396
+ parent = dirname(parent);
397
+ }
398
+ }
399
+ const selected = new Set(entries.filter((entry) => !entry.directory).map((entry) => entry.name));
400
+ const files = readZipEntries(bytes, (name) => selected.has(name), options);
401
+ mkdirSync(root, { recursive: true });
402
+ for (const entry of entries) {
403
+ if (entry.directory) {
404
+ const path = safeEntryPath(root, entry.name);
405
+ assertNoLinks(path, "directory", anchor);
406
+ mkdirSync(path, { recursive: true });
407
+ }
408
+ }
409
+ for (const [name, data] of files)
410
+ writeAtomic(safeEntryPath(root, name), data, options.overwrite ?? false, anchor);
411
+ return [...files.keys()];
412
+ }
413
+
414
+ export interface CreateOptions extends ReadOptions {
415
+ base?: string;
416
+ level?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
417
+ overwrite?: boolean;
418
+ }
419
+
420
+ export function buildZip(
421
+ inputs: readonly string[],
422
+ options: CreateOptions = {},
423
+ ): { bytes: Uint8Array; entries: string[] } {
424
+ const limits = limitsFor(options);
425
+ // Inputs are matched against the base as the caller spelled it; reads go
426
+ // through its anchored form.
427
+ const named = resolve(options.base ?? ".");
428
+ const { anchor, path: base } = anchored(named);
429
+ assertNoLinks(base, "directory", anchor);
430
+ const tree: Zippable = Object.create(null);
431
+ const entries: string[] = [];
432
+ const names = new Set<string>();
433
+ let total = 0;
434
+ const add = (input: string): void => {
435
+ const rel = relative(named, resolve(named, input));
436
+ if (!rel || outside(rel)) throw new Error(`ZIP input is outside its base directory: ${input}`);
437
+ const path = join(base, rel);
438
+ const stat = lstatSync(path);
439
+ if (stat.isSymbolicLink()) throw new Error(`Refusing ZIP input symbolic link: ${input}`);
440
+ assertNoLinks(path, stat.isDirectory() ? "directory" : "file", anchor);
441
+ if (stat.isDirectory()) {
442
+ for (const name of readdirSync(path).sort()) add(join(named, rel, name));
443
+ return;
444
+ }
445
+ if (!stat.isFile()) throw new Error(`ZIP input is not a regular file: ${input}`);
446
+ const name = rel.split(sep).join("/");
447
+ safeEntryPath(base, name);
448
+ if (Object.hasOwn(tree, name)) return;
449
+ if (names.has(name.toLowerCase())) throw new Error(`Colliding ZIP input: ${name}`);
450
+ total += stat.size;
451
+ if (
452
+ stat.size > limits.entryBytes ||
453
+ total > limits.totalBytes ||
454
+ entries.length >= limits.entries
455
+ )
456
+ throw new Error("ZIP inputs exceed size or entry count limit");
457
+ const bytes = readBoundedFile(path, Math.min(stat.size, limits.entryBytes), true);
458
+ if (bytes.byteLength !== stat.size) throw new Error(`ZIP input changed while reading: ${name}`);
459
+ names.add(name.toLowerCase());
460
+ tree[name] = [bytes, { mtime: stat.mtime }];
461
+ entries.push(name);
462
+ };
463
+ for (const input of [...inputs].sort()) add(input);
464
+ if (!entries.length) throw new Error("Nothing to ZIP: inputs contain no files");
465
+ const bytes = zipSync(tree, { level: options.level ?? 6 });
466
+ if (bytes.byteLength > limits.archiveBytes)
467
+ throw new Error("ZIP archive exceeds compressed byte limit");
468
+ return { bytes, entries };
469
+ }
470
+
471
+ export function createZip(
472
+ archive: string,
473
+ inputs: readonly string[],
474
+ options: CreateOptions = {},
475
+ ): string[] {
476
+ const { anchor, path } = anchored(archive);
477
+ assertNoLinks(path, "file", anchor);
478
+ if (!options.overwrite && statIfPresent(path))
479
+ throw new Error(`ZIP output already exists: ${archive}`);
480
+ const result = buildZip(inputs, options);
481
+ writeAtomic(path, result.bytes, options.overwrite ?? false, anchor);
482
+ return result.entries;
483
+ }