brane 0.1.1 → 0.3.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 (348) hide show
  1. package/AGENTS.md +362 -0
  2. package/CHANGELOG.md +33 -0
  3. package/CONTRIBUTING.md +91 -0
  4. package/DESIGN.md +402 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +205 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +116 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +332 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1094 -0
  37. package/dist/commands/token.js +153 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6349 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +633 -0
  58. package/dist/db/tokens.js +583 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/block-glyphs.js +114 -0
  96. package/dist/shared/diagram-css.js +17 -0
  97. package/dist/shared/export-selection.js +101 -0
  98. package/dist/shared/find-in-text.js +110 -0
  99. package/dist/shared/glow-hues.js +225 -0
  100. package/dist/shared/hex-dump.js +62 -0
  101. package/dist/shared/hex-pages.js +52 -0
  102. package/dist/shared/hit-drum.js +79 -0
  103. package/dist/shared/known-nids.js +58 -0
  104. package/dist/shared/list-scroll-keys.js +56 -0
  105. package/dist/shared/markdown-lists.js +72 -0
  106. package/dist/shared/markdown-math.js +485 -0
  107. package/dist/shared/markdown-mentions.js +79 -0
  108. package/dist/shared/markdown-nids.js +41 -0
  109. package/dist/shared/markdown-protocols.js +53 -0
  110. package/dist/shared/markdown-refs.js +123 -0
  111. package/dist/shared/membrane-invasion.js +342 -0
  112. package/dist/shared/membrane-motion.js +83 -0
  113. package/dist/shared/membrane-palette.js +253 -0
  114. package/dist/shared/membrane-terrain.js +212 -0
  115. package/dist/shared/message-selection.js +123 -0
  116. package/dist/shared/mo-glyph.js +57 -0
  117. package/dist/shared/narrative-format.js +237 -0
  118. package/dist/shared/narrative-menu.js +82 -0
  119. package/dist/shared/narrative-sort.js +47 -0
  120. package/dist/shared/narrative-status.js +52 -0
  121. package/dist/shared/no-pan.js +85 -0
  122. package/dist/shared/pane-layout.js +50 -0
  123. package/dist/shared/payload-inline.js +83 -0
  124. package/dist/shared/speaker-color.js +61 -0
  125. package/dist/shared/token-rotation.js +14 -0
  126. package/dist/shared/view-url.js +197 -0
  127. package/dist/shared/wiki-links.js +70 -0
  128. package/dist/shared/wiki-window.js +4 -0
  129. package/dist/tailscale.js +55 -0
  130. package/dist/test/absorb-claude.test.js +892 -0
  131. package/dist/test/agent-scope.test.js +193 -0
  132. package/dist/test/agents.test.js +55 -0
  133. package/dist/test/api-request.test.js +122 -0
  134. package/dist/test/awaiting-glow.test.js +54 -0
  135. package/dist/test/awaiting.test.js +122 -0
  136. package/dist/test/bind-token.test.js +88 -0
  137. package/dist/test/block-glyphs.test.js +68 -0
  138. package/dist/test/cert-manager.test.js +262 -0
  139. package/dist/test/claude-live-session.test.js +110 -0
  140. package/dist/test/codex-tool-log.test.js +568 -0
  141. package/dist/test/config.test.js +71 -0
  142. package/dist/test/copilot-tool-log.test.js +93 -0
  143. package/dist/test/cors-preflight.test.js +49 -0
  144. package/dist/test/cost-linger.test.js +31 -0
  145. package/dist/test/cost-pricing.test.js +72 -0
  146. package/dist/test/cost-reprice.test.js +139 -0
  147. package/dist/test/cost-sessions.test.js +301 -0
  148. package/dist/test/crush-tool-log.test.js +289 -0
  149. package/dist/test/daemon-client.test.js +109 -0
  150. package/dist/test/daemon-socket.test.js +293 -0
  151. package/dist/test/diagram-arrows.test.js +205 -0
  152. package/dist/test/diagram-export.test.js +210 -0
  153. package/dist/test/diagram-labels.test.js +313 -0
  154. package/dist/test/diagram-parse.test.js +191 -0
  155. package/dist/test/diagram-render.test.js +175 -0
  156. package/dist/test/diagram-review.test.js +288 -0
  157. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  158. package/dist/test/embedder.test.js +84 -0
  159. package/dist/test/events.test.js +123 -0
  160. package/dist/test/export-selection.test.js +102 -0
  161. package/dist/test/export.test.js +505 -0
  162. package/dist/test/files-browse.test.js +393 -0
  163. package/dist/test/files-find.test.js +272 -0
  164. package/dist/test/find-in-text.test.js +86 -0
  165. package/dist/test/first-paint-viewport.test.js +59 -0
  166. package/dist/test/fixtures/embedding-golden.js +23 -0
  167. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  168. package/dist/test/glow-hues.test.js +202 -0
  169. package/dist/test/harness-cost.test.js +423 -0
  170. package/dist/test/helpers.js +21 -0
  171. package/dist/test/hex-pages.test.js +68 -0
  172. package/dist/test/highlight.test.js +69 -0
  173. package/dist/test/hit-drum.test.js +71 -0
  174. package/dist/test/hook-config.test.js +262 -0
  175. package/dist/test/json-gzip.test.js +70 -0
  176. package/dist/test/key-routing.test.js +58 -0
  177. package/dist/test/known-nids.test.js +106 -0
  178. package/dist/test/link-preview.test.js +67 -0
  179. package/dist/test/list-scroll-keys.test.js +66 -0
  180. package/dist/test/listen-own-words.test.js +147 -0
  181. package/dist/test/listen-timeout.test.js +33 -0
  182. package/dist/test/markdown-diagnostics.test.js +121 -0
  183. package/dist/test/markdown-lists.test.js +182 -0
  184. package/dist/test/markdown-math.test.js +256 -0
  185. package/dist/test/markdown-nids.test.js +146 -0
  186. package/dist/test/markdown-protocols.test.js +139 -0
  187. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  188. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  189. package/dist/test/mcp-narrative.test.js +193 -0
  190. package/dist/test/mcp.test.js +548 -0
  191. package/dist/test/meld.test.js +85 -0
  192. package/dist/test/membrane-invasion.test.js +482 -0
  193. package/dist/test/membrane-motion.test.js +101 -0
  194. package/dist/test/membrane-palette.test.js +166 -0
  195. package/dist/test/membrane-terrain.test.js +170 -0
  196. package/dist/test/memory-log.test.js +202 -0
  197. package/dist/test/mentions-e2e.test.js +261 -0
  198. package/dist/test/mentions-ledger.test.js +265 -0
  199. package/dist/test/mentions-listen.test.js +265 -0
  200. package/dist/test/mentions.test.js +238 -0
  201. package/dist/test/message-selection.test.js +114 -0
  202. package/dist/test/mo-glyph.test.js +22 -0
  203. package/dist/test/model-resolution.test.js +166 -0
  204. package/dist/test/narrate-validate.test.js +77 -0
  205. package/dist/test/narrate-wait.test.js +160 -0
  206. package/dist/test/narrative-format.test.js +219 -0
  207. package/dist/test/narrative-menu.test.js +137 -0
  208. package/dist/test/narrative-sort.test.js +75 -0
  209. package/dist/test/no-pan.test.js +60 -0
  210. package/dist/test/pane-layout.test.js +153 -0
  211. package/dist/test/paths.test.js +37 -0
  212. package/dist/test/payload-inline.test.js +91 -0
  213. package/dist/test/precache-fonts.test.js +72 -0
  214. package/dist/test/presence-gate.test.js +440 -0
  215. package/dist/test/presence.test.js +160 -0
  216. package/dist/test/read-events.test.js +220 -0
  217. package/dist/test/remote-meld.test.js +115 -0
  218. package/dist/test/revive-cli.test.js +93 -0
  219. package/dist/test/search-floor.test.js +206 -0
  220. package/dist/test/search-fusion.test.js +168 -0
  221. package/dist/test/search.test.js +205 -0
  222. package/dist/test/security.test.js +48 -0
  223. package/dist/test/selection-wiring.test.js +61 -0
  224. package/dist/test/session-affinity.test.js +295 -0
  225. package/dist/test/signal-source.test.js +32 -0
  226. package/dist/test/socket-account.test.js +268 -0
  227. package/dist/test/spawn-toml.test.js +72 -0
  228. package/dist/test/speaker-color.test.js +90 -0
  229. package/dist/test/tailscale-rebind.test.js +79 -0
  230. package/dist/test/terminal-cursor.test.js +61 -0
  231. package/dist/test/terminal-resync.test.js +23 -0
  232. package/dist/test/thin-cli.test.js +382 -0
  233. package/dist/test/token-bootstrap.test.js +272 -0
  234. package/dist/test/token-devices.test.js +587 -0
  235. package/dist/test/token-lapse.test.js +94 -0
  236. package/dist/test/token-ring.test.js +188 -0
  237. package/dist/test/token-rotate-cli.test.js +58 -0
  238. package/dist/test/token-routes.test.js +95 -0
  239. package/dist/test/tool-uses.test.js +1006 -0
  240. package/dist/test/usage-ledger.test.js +126 -0
  241. package/dist/test/view-url.test.js +581 -0
  242. package/dist/test/wiki-links.test.js +57 -0
  243. package/dist/test/wiki-sources.test.js +85 -0
  244. package/dist/test/wiki-window.test.js +31 -0
  245. package/dist/test/wiki.test.js +399 -0
  246. package/dist/test/wordpiece.test.js +68 -0
  247. package/dist/tls.js +148 -0
  248. package/dist/tmux.js +202 -0
  249. package/dist/user-agent.js +46 -0
  250. package/docker-entrypoint.sh +141 -0
  251. package/docs/README.md +3 -0
  252. package/docs/absorb.md +231 -0
  253. package/docs/cost-ledger.md +125 -0
  254. package/docs/daemon-api.md +189 -0
  255. package/docs/diagrams.md +163 -0
  256. package/docs/doctor.md +36 -0
  257. package/docs/export-scope.md +69 -0
  258. package/docs/files-view.md +133 -0
  259. package/docs/link-previews.md +38 -0
  260. package/docs/math.md +32 -0
  261. package/docs/mcp.md +69 -0
  262. package/docs/memory-search.md +75 -0
  263. package/docs/mentions.md +101 -0
  264. package/docs/multi-brane.md +14 -0
  265. package/docs/passkeys.md +165 -0
  266. package/docs/presence.md +202 -0
  267. package/docs/releasing.md +53 -0
  268. package/docs/rendering-warnings.md +22 -0
  269. package/docs/request-layer.md +72 -0
  270. package/docs/selection-store.md +50 -0
  271. package/docs/session-affinity.md +61 -0
  272. package/docs/terminal-mirroring.md +211 -0
  273. package/docs/token-rotation.md +413 -0
  274. package/docs/view-urls.md +114 -0
  275. package/docs/web-app.md +23 -0
  276. package/docs/wiki.md +43 -0
  277. package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -0
  278. package/expo/dist/apple-touch-icon.png +0 -0
  279. package/expo/dist/favicon.svg +3 -0
  280. package/expo/dist/index.html +177 -0
  281. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  282. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  283. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  284. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  285. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  301. package/expo/dist/katex/katex.min.css +1 -0
  302. package/expo/dist/manifest.json +16 -0
  303. package/expo/dist/metadata.json +1 -0
  304. package/expo/dist/og/membrane.v1.png +0 -0
  305. package/expo/dist/pdf.min.mjs +28 -0
  306. package/expo/dist/pdf.worker.min.mjs +28 -0
  307. package/expo/dist/sw.js +154 -0
  308. package/package.json +79 -27
  309. package/protocols/LIVE_loop.md +407 -0
  310. package/protocols/bebrief.md +41 -0
  311. package/protocols/digest.md +231 -0
  312. package/protocols/fly.md +266 -0
  313. package/protocols/idle.md +81 -0
  314. package/protocols/keeper.md +829 -0
  315. package/protocols/markdown.md +80 -0
  316. package/protocols/memory.md +419 -0
  317. package/protocols/mentions.md +93 -0
  318. package/protocols/payloads.md +53 -0
  319. package/protocols/prelude.md +91 -0
  320. package/protocols/ramify.md +105 -0
  321. package/protocols/review.md +356 -0
  322. package/protocols/rrr.md +232 -0
  323. package/protocols/scribe.md +429 -0
  324. package/protocols/summarize.md +203 -0
  325. package/protocols/twin.md +174 -0
  326. package/protocols/wiki.md +438 -0
  327. package/protocols/worker.md +38 -0
  328. package/wiki/shared/append-only-event-log.md +148 -0
  329. package/wiki/shared/index.md +48 -0
  330. package/wiki/shared/owning-your-narratives.md +161 -0
  331. package/wiki/shared/premature-ontologies.md +435 -0
  332. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  333. package/wiki/shared/sequestering-the-narrative.md +143 -0
  334. package/wiki/shared/why-wikis-work.md +216 -0
  335. package/.travis.yml +0 -7
  336. package/lib/brane.cjs.js +0 -12
  337. package/lib/brane.cjs.js.map +0 -1
  338. package/lib/brane.d.ts +0 -11
  339. package/lib/brane.js +0 -6
  340. package/lib/brane.js.map +0 -1
  341. package/lib/tests.d.ts +0 -1
  342. package/lib/tests.js +0 -8
  343. package/lib/tests.js.map +0 -1
  344. package/rollup.config.js +0 -21
  345. package/scripts/test.sh +0 -8
  346. package/src/brane.ts +0 -21
  347. package/src/tests.ts +0 -8
  348. package/tsconfig.json +0 -16
@@ -0,0 +1,182 @@
1
+ // Archives as directories. A path that runs through an archive —
2
+ // `dist/app.zip/src/main.ts` — names a member, and the archive itself lists
3
+ // as a folder. Nothing is ever extracted: members are listed from the index
4
+ // the format keeps (zip) or from one pass over the stream (tar), and read
5
+ // through a `ByteSource` that decodes only as far as a request reaches.
6
+ //
7
+ // Member names are data from the archive's author, so this is where they are
8
+ // made safe to show. A name that is absolute has its leading slashes dropped;
9
+ // one containing `..` or a NUL is hidden and counted (`skipped`), since there
10
+ // is no honest place in the tree to draw it. Since nothing is written to
11
+ // disk, none of this is what stops zip-slip — there is no slip to stop — it
12
+ // only keeps the tree the operator sees from lying about where a member sits.
13
+ import { stat } from "node:fs/promises";
14
+ import { fileSource, streamSource } from "./bytes.js";
15
+ import { HAS_ZSTD, openDecoded } from "./codecs.js";
16
+ import { listZip } from "./zip.js";
17
+ import { scanTar } from "./tar.js";
18
+ /** Zip under other names: Java and Android packages, Python wheels, e-books,
19
+ * Office and OpenDocument files, browser and editor extensions. Each is a zip
20
+ * whose contents are worth reading, and each showed as binary before. */
21
+ const ZIP_EXTS = [
22
+ ".zip", ".jar", ".war", ".ear", ".aar", ".apk", ".aab", ".ipa", ".whl",
23
+ ".epub", ".docx", ".xlsx", ".pptx", ".odt", ".ods", ".odp", ".odg",
24
+ ".nupkg", ".vsix", ".xpi", ".kmz", ".sketch", ".xmind",
25
+ ];
26
+ const COMPRESSED_EXTS = [
27
+ { ext: ".tar.gz", codec: "gzip", tar: true },
28
+ { ext: ".tgz", codec: "gzip", tar: true },
29
+ { ext: ".taz", codec: "gzip", tar: true },
30
+ { ext: ".tar.br", codec: "brotli", tar: true },
31
+ { ext: ".tbr", codec: "brotli", tar: true },
32
+ ...(HAS_ZSTD ? [
33
+ { ext: ".tar.zst", codec: "zstd", tar: true },
34
+ { ext: ".tzst", codec: "zstd", tar: true },
35
+ { ext: ".zst", codec: "zstd", tar: false },
36
+ ] : []),
37
+ { ext: ".gz", codec: "gzip", tar: false },
38
+ { ext: ".br", codec: "brotli", tar: false },
39
+ ];
40
+ /** What kind of archive a file name says it is, or null. The name only
41
+ * nominates: a file that doesn't parse as what it claims falls back to being
42
+ * an ordinary (usually binary) file. */
43
+ export function archiveFormatFor(name) {
44
+ const lower = name.toLowerCase();
45
+ const base = lower.slice(lower.lastIndexOf("/") + 1);
46
+ if (ZIP_EXTS.some((e) => base.endsWith(e) && base.length > e.length)) {
47
+ return { kind: "zip", label: "zip" };
48
+ }
49
+ if (base.endsWith(".tar") && base.length > 4)
50
+ return { kind: "tar", label: "tar", codec: null };
51
+ for (const c of COMPRESSED_EXTS) {
52
+ if (!base.endsWith(c.ext) || base.length <= c.ext.length)
53
+ continue;
54
+ const label = c.ext.slice(1);
55
+ if (c.tar)
56
+ return { kind: "tar", label, codec: c.codec };
57
+ const original = name.slice(name.lastIndexOf("/") + 1);
58
+ return { kind: "single", label, codec: c.codec, inner: original.slice(0, original.length - c.ext.length) };
59
+ }
60
+ return null;
61
+ }
62
+ /** A name as the tree will show it, or null if it can't honestly be placed.
63
+ * A name that is only the root (`./`, which `tar -C dir .` writes first) is
64
+ * "" — nothing to place, but nothing wrong either. */
65
+ export function normalizeMemberName(name) {
66
+ if (name.includes("\0"))
67
+ return null;
68
+ const parts = [];
69
+ for (const part of name.replace(/\\/g, "/").split("/")) {
70
+ if (part === "" || part === ".")
71
+ continue;
72
+ if (part === "..")
73
+ return null;
74
+ parts.push(part);
75
+ }
76
+ return parts.join("/");
77
+ }
78
+ export const ARCHIVE_LIMITS = {
79
+ maxEntries: 200_000,
80
+ /** Decoded bytes a compressed tar's listing may run through. */
81
+ maxScanBytes: 1024 * 1024 * 1024,
82
+ };
83
+ export function buildIndex(format, members, partial) {
84
+ const nodes = new Map();
85
+ const children = new Map();
86
+ const root = { name: "", kind: "dir", size: null, mtimeMs: 0 };
87
+ nodes.set("", root);
88
+ children.set("", new Map());
89
+ let skipped = 0;
90
+ const ensureDir = (path, mtimeMs) => {
91
+ const existing = nodes.get(path);
92
+ if (existing)
93
+ return existing.kind === "dir" ? existing : null;
94
+ const slash = path.lastIndexOf("/");
95
+ const parentPath = slash < 0 ? "" : path.slice(0, slash);
96
+ const parent = ensureDir(parentPath, mtimeMs);
97
+ if (!parent)
98
+ return null;
99
+ const node = { name: path.slice(slash + 1), kind: "dir", size: null, mtimeMs };
100
+ nodes.set(path, node);
101
+ children.set(path, new Map());
102
+ children.get(parentPath).set(node.name, node);
103
+ return node;
104
+ };
105
+ for (const m of members) {
106
+ const path = normalizeMemberName(m.name);
107
+ if (path === null || (path === "" && m.kind !== "dir")) {
108
+ skipped++;
109
+ continue;
110
+ }
111
+ if (m.kind === "dir") {
112
+ const dir = ensureDir(path, m.mtimeMs);
113
+ if (!dir) {
114
+ skipped++;
115
+ continue;
116
+ }
117
+ if (m.mtimeMs)
118
+ dir.mtimeMs = m.mtimeMs;
119
+ continue;
120
+ }
121
+ const slash = path.lastIndexOf("/");
122
+ const parentPath = slash < 0 ? "" : path.slice(0, slash);
123
+ const existing = nodes.get(path);
124
+ // A file where the tree already has a directory (`a` beside `a/b`) has
125
+ // nowhere to go. A second file of the same name replaces the first, as
126
+ // extracting it would.
127
+ if (existing?.kind === "dir") {
128
+ skipped++;
129
+ continue;
130
+ }
131
+ const parent = ensureDir(parentPath, m.mtimeMs);
132
+ if (!parent) {
133
+ skipped++;
134
+ continue;
135
+ }
136
+ const node = { name: path.slice(slash + 1), kind: m.kind, size: m.size, mtimeMs: m.mtimeMs, member: m };
137
+ nodes.set(path, node);
138
+ children.get(parentPath).set(node.name, node);
139
+ }
140
+ return { format, nodes, children, skipped, partial };
141
+ }
142
+ /** Lists the archive at `path`. Throws when it isn't what its name claims. */
143
+ export async function readArchiveIndex(path, size, format) {
144
+ if (format.kind === "zip") {
145
+ const { members, partial } = await listZip(path, size, ARCHIVE_LIMITS.maxEntries);
146
+ return buildIndex(format, members, partial);
147
+ }
148
+ if (format.kind === "tar") {
149
+ const codec = format.codec;
150
+ const { members, partial } = await scanTar(openDecoded(path, codec), (offset, bodySize, mtime) => codec
151
+ ? streamSource(() => openDecoded(path, codec), bodySize, mtime, offset)
152
+ : fileSource(path, bodySize, mtime, offset), ARCHIVE_LIMITS);
153
+ return buildIndex(format, members, partial);
154
+ }
155
+ throw new Error("not an archive");
156
+ }
157
+ // A listing of a compressed tar costs a pass over the whole thing, and the
158
+ // Files view asks for one per directory it opens, so indexes are kept while
159
+ // the file is unchanged. Keyed on (path, size, mtime): an edit or a rebuild
160
+ // is a new key, never a stale listing.
161
+ const CACHE_SIZE = 16;
162
+ const cache = new Map();
163
+ export async function archiveIndex(path, format) {
164
+ const st = await stat(path);
165
+ const key = `${path}\0${st.size}\0${st.mtimeMs}`;
166
+ let hit = cache.get(key);
167
+ if (hit) {
168
+ cache.delete(key);
169
+ cache.set(key, hit);
170
+ return hit;
171
+ }
172
+ hit = readArchiveIndex(path, st.size, format);
173
+ cache.set(key, hit);
174
+ // A failure is not cached: the next request tries again (the file may have
175
+ // been mid-write).
176
+ hit.catch(() => { if (cache.get(key) === hit)
177
+ cache.delete(key); });
178
+ while (cache.size > CACHE_SIZE)
179
+ cache.delete(cache.keys().next().value);
180
+ return hit;
181
+ }
182
+ //# sourceMappingURL=archive.js.map
@@ -0,0 +1,327 @@
1
+ // GET /api/files, as a function of (fold, path, options) → answer, so the
2
+ // whole route can be tested without HTTP. The server does only the lookup of
3
+ // the narrative's fold and the writing of what comes back.
4
+ //
5
+ // Every file — on disk, inside an archive, behind a `.gz` — is served through
6
+ // one `respondWithSource`, which is why a format added in `archive.ts` gains
7
+ // text, highlighting, images, the hex view and download all at once.
8
+ import { readdir, stat } from "node:fs/promises";
9
+ import { extname, relative, resolve } from "node:path";
10
+ import { detectLanguage, highlight } from "../highlight.js";
11
+ import { BINARY_SNIFF_BYTES, HEX_PAGE_BYTES, looksBinary } from "../shared/hex-dump.js";
12
+ import { ReachError, fileSource, streamSource } from "./bytes.js";
13
+ import { openDecoded } from "./codecs.js";
14
+ import { archiveFormatFor, archiveIndex, normalizeMemberName } from "./archive.js";
15
+ import { MemberError } from "./zip.js";
16
+ /** Text is sent inline up to this size; past it, a notice and a download. */
17
+ export const MAX_INLINE = 2 * 1024 * 1024;
18
+ const err = (status, error) => ({ status, json: { error } });
19
+ /** Parse `offset=`: a non-negative safe integer, or undefined when absent. */
20
+ export function parseOffsetParam(v) {
21
+ if (v === null)
22
+ return null;
23
+ if (!/^\d+$/.test(v))
24
+ return undefined;
25
+ const n = Number(v);
26
+ return Number.isSafeInteger(n) ? n : undefined;
27
+ }
28
+ /** The request's path confined to the fold, or why it can't be. */
29
+ function confine(foldRoot, relParam) {
30
+ if (relParam.startsWith("/") || relParam.startsWith("\\"))
31
+ return { status: 400, error: "Absolute paths not allowed" };
32
+ const cleanRel = relParam.split("#")[0].split("?")[0];
33
+ const target = resolve(foldRoot, cleanRel);
34
+ if (target !== foldRoot && !target.startsWith(foldRoot + "/"))
35
+ return { status: 400, error: "Path escapes fold" };
36
+ // Echo a NORMALIZED relative path: resolve() already collapsed any
37
+ // `.`/`..`/`//` segments, so the breadcrumb never renders `fold/./blah`.
38
+ return { target, normRel: relative(foldRoot, target) };
39
+ }
40
+ export async function browseFold(foldRoot, relParam, opts) {
41
+ const where = confine(foldRoot, relParam);
42
+ if ("error" in where)
43
+ return err(where.status, where.error);
44
+ const { target, normRel } = where;
45
+ let st;
46
+ try {
47
+ st = await stat(target);
48
+ }
49
+ catch (e) {
50
+ const code = e.code;
51
+ if (code !== "ENOENT" && code !== "ENOTDIR")
52
+ throw e;
53
+ return browseInsideArchive(foldRoot, normRel, opts);
54
+ }
55
+ if (st.isDirectory()) {
56
+ return { status: 200, json: { type: "directory", path: normRel, entries: await listDirectory(target) } };
57
+ }
58
+ const format = archiveFormatFor(normRel);
59
+ if (format?.kind === "single" && !opts.raw) {
60
+ // A `.gz` opens as what it compresses. `raw` still means the file itself.
61
+ const source = streamSource(() => openDecoded(target, format.codec), null, st.mtimeMs);
62
+ return respondWithSource(source, normRel, format.inner, opts);
63
+ }
64
+ if (format && format.kind !== "single" && !opts.raw && (opts.offset ?? null) === null) {
65
+ try {
66
+ const index = await archiveIndex(target, format);
67
+ return archiveDirectory(index, "", normRel, archiveInfo(normRel, format, index));
68
+ }
69
+ catch {
70
+ // Not what its name says: show it as the file it is.
71
+ }
72
+ }
73
+ return respondWithSource(fileSource(target, st.size, st.mtimeMs), normRel, normRel, opts);
74
+ }
75
+ async function listDirectory(dir) {
76
+ const names = await readdir(dir);
77
+ const entries = await Promise.all(names
78
+ .filter((n) => !n.startsWith(".")) // hide dotfiles by default
79
+ .map(async (name) => {
80
+ try {
81
+ const child = await stat(resolve(dir, name));
82
+ const isDir = child.isDirectory();
83
+ const fmt = isDir ? null : archiveFormatFor(name);
84
+ return {
85
+ name,
86
+ is_dir: isDir,
87
+ size: isDir ? null : child.size,
88
+ mtime: child.mtimeMs,
89
+ ...(fmt && fmt.kind !== "single" ? { archive: true } : {}),
90
+ };
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }));
96
+ return sortEntries(entries.filter((e) => e !== null));
97
+ }
98
+ function sortEntries(entries) {
99
+ return entries.sort((a, b) => {
100
+ if (a.is_dir !== b.is_dir)
101
+ return a.is_dir ? -1 : 1;
102
+ return a.name.localeCompare(b.name);
103
+ });
104
+ }
105
+ function archiveInfo(root, format, index) {
106
+ return {
107
+ root,
108
+ format: format.label,
109
+ ...(index.skipped ? { skipped: index.skipped } : {}),
110
+ ...(index.partial ? { partial: true } : {}),
111
+ };
112
+ }
113
+ function archiveDirectory(index, inner, normRel, info) {
114
+ const kids = index.children.get(inner);
115
+ if (!kids)
116
+ return err(404, "Not found");
117
+ const entries = [];
118
+ for (const node of kids.values()) {
119
+ // Dotfiles are hidden as they are in the fold — which also keeps macOS's
120
+ // `._name` AppleDouble twins out of every tarball made on a Mac.
121
+ if (node.name.startsWith("."))
122
+ continue;
123
+ const isDir = node.kind === "dir";
124
+ entries.push({
125
+ name: node.name,
126
+ is_dir: isDir,
127
+ size: isDir ? null : node.size,
128
+ mtime: node.mtimeMs,
129
+ ...(node.member?.link ? { link: node.member.link } : {}),
130
+ });
131
+ }
132
+ return { status: 200, json: { type: "directory", path: normRel, entries: sortEntries(entries), archive: info } };
133
+ }
134
+ /** `normRel` doesn't exist on disk. If a prefix of it is an archive, the rest
135
+ * names a member. Archives do not nest: a member that is itself a zip opens
136
+ * as its bytes. */
137
+ async function browseInsideArchive(foldRoot, normRel, opts) {
138
+ const found = await locateMember(foldRoot, normRel);
139
+ if (!found)
140
+ return err(404, "Not found");
141
+ const { index, node, inner, info } = found;
142
+ return browseMember(index, node, inner, normRel, info, opts);
143
+ }
144
+ /** The archive member `normRel` names, when a prefix of it is an archive. */
145
+ async function locateMember(foldRoot, normRel) {
146
+ const segs = normRel.split("/");
147
+ for (let i = segs.length - 1; i >= 1; i--) {
148
+ const prefix = segs.slice(0, i).join("/");
149
+ const format = archiveFormatFor(prefix);
150
+ let st;
151
+ try {
152
+ st = await stat(resolve(foldRoot, prefix));
153
+ }
154
+ catch {
155
+ continue;
156
+ }
157
+ if (!st.isFile() || !format || format.kind === "single")
158
+ return null;
159
+ let index;
160
+ try {
161
+ index = await archiveIndex(resolve(foldRoot, prefix), format);
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ const inner = segs.slice(i).join("/");
167
+ const node = index.nodes.get(inner);
168
+ return node ? { index, node, inner, info: archiveInfo(prefix, format, index) } : null;
169
+ }
170
+ return null;
171
+ }
172
+ /** A tar hard link names an earlier member of the same archive; that one
173
+ * lookup is as far as it goes. */
174
+ function hardlinkTarget(index, node) {
175
+ const to = node.member?.link;
176
+ const resolved = to === undefined ? undefined : index.nodes.get(normalizeMemberName(to) ?? "\0");
177
+ return resolved?.kind === "file" ? resolved : undefined;
178
+ }
179
+ /** The bytes the hex view shows for `relParam` — a file, an archive member,
180
+ * a `.gz` body — for a search to read. A search reads what the view reads,
181
+ * or its hits would name offsets the view has never heard of. */
182
+ export async function openFoldSource(foldRoot, relParam) {
183
+ const where = confine(foldRoot, relParam);
184
+ if ("error" in where)
185
+ return where;
186
+ const { target, normRel } = where;
187
+ let st;
188
+ try {
189
+ st = await stat(target);
190
+ }
191
+ catch (e) {
192
+ const code = e.code;
193
+ if (code !== "ENOENT" && code !== "ENOTDIR")
194
+ throw e;
195
+ const found = await locateMember(foldRoot, normRel);
196
+ if (!found)
197
+ return { status: 404, error: "Not found" };
198
+ const node = found.node.kind === "hardlink" ? hardlinkTarget(found.index, found.node) : found.node;
199
+ if (node?.kind !== "file" || !node.member?.open)
200
+ return { status: 400, error: "Not a file with contents" };
201
+ try {
202
+ return node.member.open();
203
+ }
204
+ catch (e) {
205
+ if (e instanceof MemberError)
206
+ return { status: 400, error: e.message };
207
+ throw e;
208
+ }
209
+ }
210
+ if (!st.isFile())
211
+ return { status: 400, error: "Not a file" };
212
+ const format = archiveFormatFor(normRel);
213
+ if (format?.kind === "single")
214
+ return streamSource(() => openDecoded(target, format.codec), null, st.mtimeMs);
215
+ return fileSource(target, st.size, st.mtimeMs);
216
+ }
217
+ async function browseMember(index, node, inner, normRel, info, opts) {
218
+ if (node.kind === "dir")
219
+ return archiveDirectory(index, inner, normRel, info);
220
+ const plain = (extra) => ({
221
+ status: 200,
222
+ json: { type: "file", path: normRel, size: node.size, mtime: node.mtimeMs, content: "", archive: info, ...extra },
223
+ });
224
+ let target = node;
225
+ if (node.kind === "symlink") {
226
+ // Shown, never followed: a link's target is the archive author's claim
227
+ // about a filesystem that doesn't exist here.
228
+ let link = node.member?.link;
229
+ if (link === undefined && node.member?.readLink) {
230
+ link = await node.member.readLink().catch(() => undefined);
231
+ }
232
+ return plain({ size: 0, link: link ?? "" });
233
+ }
234
+ if (node.kind === "hardlink") {
235
+ const resolved = hardlinkTarget(index, node);
236
+ if (!resolved)
237
+ return plain({ size: 0, link: node.member?.link ?? "" });
238
+ target = resolved;
239
+ }
240
+ if (target.kind !== "file" || !target.member?.open) {
241
+ return plain({ notice: "A special file (device, FIFO or similar), which has no contents to show" });
242
+ }
243
+ let source;
244
+ try {
245
+ source = target.member.open();
246
+ }
247
+ catch (e) {
248
+ if (e instanceof MemberError)
249
+ return plain({ notice: e.message });
250
+ throw e;
251
+ }
252
+ const res = await respondWithSource(source, normRel, inner, opts);
253
+ if ("json" in res && res.status === 200 && res.json && res.json.type === "file") {
254
+ res.json.archive = info;
255
+ }
256
+ return res;
257
+ }
258
+ /** The one place a file's bytes become a response. `name` decides the MIME
259
+ * type and the highlighter; it differs from `normRel` for a `.gz` body. */
260
+ export async function respondWithSource(source, normRel, name, opts) {
261
+ const mime = opts.mimeTypes[extname(name).toLowerCase()];
262
+ const base = { type: "file", path: normRel, size: source.size, mtime: source.mtimeMs };
263
+ if (opts.raw) {
264
+ return { status: 200, raw: { stream: source.stream(), contentType: mime ?? "application/octet-stream" } };
265
+ }
266
+ const offset = opts.offset ?? null;
267
+ if (offset !== null)
268
+ return hexPage(source, base, offset);
269
+ // Images, PDFs and fonts render from a raw URL; the JSON only says so.
270
+ const rendersFromRaw = mime?.startsWith("image/") || mime === "application/pdf" || mime === "font/woff" || mime === "font/woff2";
271
+ if (rendersFromRaw) {
272
+ return {
273
+ status: 200,
274
+ json: { ...base, binary: true, truncated: source.size !== null && source.size > MAX_INLINE, content: "" },
275
+ };
276
+ }
277
+ const head = await source.read(0, BINARY_SNIFF_BYTES);
278
+ if (looksBinary(head))
279
+ return hexPage(source, base, 0);
280
+ if (source.size !== null && source.size > MAX_INLINE) {
281
+ return { status: 200, json: { ...base, truncated: true, content: "" } };
282
+ }
283
+ // One byte past the limit says whether a body of unknown size is too big.
284
+ const body = source.size !== null && source.size <= BINARY_SNIFF_BYTES ? head : await source.read(0, MAX_INLINE + 1);
285
+ if (body.length > MAX_INLINE) {
286
+ return { status: 200, json: { ...base, truncated: true, content: "" } };
287
+ }
288
+ const data = body.toString("utf8");
289
+ // detectLanguage returns null for markdown and unknown types, so markdown
290
+ // (rendered client-side by MarkdownContent) and plain text both skip the
291
+ // highlighter without a special case here.
292
+ const lang = detectLanguage(name);
293
+ const hi = lang
294
+ ? await highlight(data, lang).catch((e) => {
295
+ console.warn(`[files] highlight failed for ${normRel} (${lang}):`, e);
296
+ return null;
297
+ })
298
+ : null;
299
+ return { status: 200, json: { ...base, size: source.size ?? body.length, content: data, highlight: hi } };
300
+ }
301
+ async function hexPage(source, base, offset) {
302
+ let bytes;
303
+ let notice;
304
+ try {
305
+ // One byte past the page says whether another follows.
306
+ bytes = await source.read(offset, HEX_PAGE_BYTES + 1);
307
+ }
308
+ catch (e) {
309
+ if (!(e instanceof ReachError))
310
+ throw e;
311
+ bytes = Buffer.alloc(0);
312
+ notice = e.message;
313
+ }
314
+ const eof = bytes.length <= HEX_PAGE_BYTES;
315
+ const page = eof ? bytes : bytes.subarray(0, HEX_PAGE_BYTES);
316
+ return {
317
+ status: 200,
318
+ json: {
319
+ ...base,
320
+ binary: true,
321
+ content: "",
322
+ hex: { offset, data: page.toString("base64"), eof },
323
+ ...(notice ? { notice } : {}),
324
+ },
325
+ };
326
+ }
327
+ //# sourceMappingURL=browse.js.map
@@ -0,0 +1,162 @@
1
+ // A ByteSource is anything the Files view can open: a file in the fold, a
2
+ // member of an archive, the decompressed body of a `.gz`. Every renderer —
3
+ // text, highlighting, images, the hex view, a raw download — reads through
4
+ // this one interface, so a format added here is browsable everywhere at once.
5
+ import { open } from "node:fs/promises";
6
+ import { createReadStream } from "node:fs";
7
+ import { Readable } from "node:stream";
8
+ /** Bytes a search reads from disk at a time. */
9
+ const SCAN_CHUNK_BYTES = 1024 * 1024;
10
+ /** A byte range of a file on disk, read by position. */
11
+ export function fileSource(path, size, mtimeMs, start = 0) {
12
+ return {
13
+ size,
14
+ mtimeMs,
15
+ async read(offset, length) {
16
+ const n = Math.max(0, Math.min(length, size - offset));
17
+ if (n === 0)
18
+ return Buffer.alloc(0);
19
+ return readAt(path, start + offset, n);
20
+ },
21
+ stream() {
22
+ if (size === 0)
23
+ return Readable.from([]);
24
+ return createReadStream(path, { start, end: start + size - 1 });
25
+ },
26
+ chunks(from) {
27
+ if (from >= size)
28
+ return Readable.from([]);
29
+ return createReadStream(path, { start: start + from, end: start + size - 1, highWaterMark: SCAN_CHUNK_BYTES });
30
+ },
31
+ };
32
+ }
33
+ /** `length` bytes at `position` of a file, fewer only at its end. */
34
+ export async function readAt(path, position, length) {
35
+ const fh = await open(path, "r");
36
+ try {
37
+ const buf = Buffer.alloc(length);
38
+ let got = 0;
39
+ while (got < length) {
40
+ const { bytesRead } = await fh.read(buf, got, length - got, position + got);
41
+ if (bytesRead === 0)
42
+ break;
43
+ got += bytesRead;
44
+ }
45
+ return got === length ? buf : buf.subarray(0, got);
46
+ }
47
+ finally {
48
+ await fh.close();
49
+ }
50
+ }
51
+ /** How far into a decompressed stream a read may reach. Reaching byte N of a
52
+ * compressed body means inflating every byte before it, so this bounds the
53
+ * CPU one request can spend: a page 10 GB into a zip bomb is refused rather
54
+ * than computed. */
55
+ export const MAX_STREAM_REACH = 1024 * 1024 * 1024;
56
+ export class ReachError extends Error {
57
+ constructor(limit) {
58
+ super(`That is more than ${Math.round(limit / 1024 / 1024)} MB into a compressed stream, further than brane will decompress`);
59
+ }
60
+ }
61
+ /** A body that can only be read front to back, at `start` within what `open`
62
+ * yields: each read decompresses from the beginning and tears the pipeline
63
+ * down once it has what it asked for. Nothing past the requested range is
64
+ * ever produced, and that is the whole defence against a decompression
65
+ * bomb — output is bounded by the request, never by what the archive claims. */
66
+ export function streamSource(openStream, size, mtimeMs, start = 0, reach = MAX_STREAM_REACH) {
67
+ const bound = (offset, length) => {
68
+ if (size !== null)
69
+ length = Math.max(0, Math.min(length, size - offset));
70
+ const end = start + offset + length;
71
+ if (end > reach) {
72
+ if (start + offset >= reach)
73
+ throw new ReachError(reach);
74
+ length = reach - start - offset;
75
+ }
76
+ return length;
77
+ };
78
+ return {
79
+ size,
80
+ mtimeMs,
81
+ async read(offset, length) {
82
+ const n = bound(offset, length);
83
+ if (n === 0)
84
+ return Buffer.alloc(0);
85
+ const parts = [];
86
+ for await (const chunk of sliceStream(openStream, start + offset, n))
87
+ parts.push(chunk);
88
+ return Buffer.concat(parts);
89
+ },
90
+ stream() {
91
+ // A download stops at the reach limit too: a bomb member streams a
92
+ // gigabyte and ends, rather than inflating until the client gives up.
93
+ const n = size ?? reach - start;
94
+ return Readable.from(sliceStream(openStream, start, Math.min(n, reach - start)));
95
+ },
96
+ async *chunks(from) {
97
+ const want = size === null ? Infinity : Math.max(0, size - from);
98
+ const room = reach - start - from;
99
+ if (want === 0)
100
+ return;
101
+ if (room <= 0)
102
+ throw new ReachError(reach);
103
+ // One byte past the reach limit says whether the body goes on past it.
104
+ const take = Math.min(want, room + 1);
105
+ let got = 0;
106
+ for await (const chunk of sliceStream(openStream, start + from, take)) {
107
+ const keep = Math.min(chunk.length, room - got);
108
+ if (keep > 0)
109
+ yield keep === chunk.length ? chunk : chunk.subarray(0, keep);
110
+ got += chunk.length;
111
+ if (got > room)
112
+ throw new ReachError(reach);
113
+ }
114
+ },
115
+ };
116
+ }
117
+ /** Bytes [start, start + length) of a freshly opened stream, as chunks. The
118
+ * stream is destroyed when the range is done or the consumer stops early. */
119
+ export async function* sliceStream(openStream, start, length) {
120
+ const stream = await openStream();
121
+ let pos = 0;
122
+ let left = length;
123
+ try {
124
+ for await (const chunk of stream) {
125
+ const end = pos + chunk.length;
126
+ if (end > start && left > 0) {
127
+ const from = Math.max(0, start - pos);
128
+ const take = Math.min(chunk.length - from, left);
129
+ yield chunk.subarray(from, from + take);
130
+ left -= take;
131
+ }
132
+ pos = end;
133
+ if (left <= 0)
134
+ break;
135
+ }
136
+ }
137
+ finally {
138
+ stream.destroy();
139
+ }
140
+ }
141
+ /** A source whose real reader needs an async step first (a zip member's local
142
+ * header). `size` is the archive's claim, shown but never relied on. */
143
+ export function deferredSource(size, mtimeMs, make) {
144
+ let inner = null;
145
+ const get = () => (inner ??= make());
146
+ return {
147
+ size,
148
+ mtimeMs,
149
+ async read(offset, length) {
150
+ return (await get()).read(offset, length);
151
+ },
152
+ stream() {
153
+ return Readable.from((async function* () {
154
+ yield* (await get()).stream();
155
+ })());
156
+ },
157
+ async *chunks(from) {
158
+ yield* (await get()).chunks(from);
159
+ },
160
+ };
161
+ }
162
+ //# sourceMappingURL=bytes.js.map