@nimbus-sh/worker 0.1.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 (408) hide show
  1. package/README.md +219 -0
  2. package/dist/_shared/bytes.d.ts +18 -0
  3. package/dist/_shared/bytes.d.ts.map +1 -0
  4. package/dist/_shared/bytes.js +17 -0
  5. package/dist/_shared/cache-stats.d.ts +146 -0
  6. package/dist/_shared/cache-stats.d.ts.map +1 -0
  7. package/dist/_shared/cache-stats.js +162 -0
  8. package/dist/_shared/exports-resolver.d.ts +76 -0
  9. package/dist/_shared/exports-resolver.d.ts.map +1 -0
  10. package/dist/_shared/exports-resolver.js +284 -0
  11. package/dist/_shared/install-phase.d.ts +47 -0
  12. package/dist/_shared/install-phase.d.ts.map +1 -0
  13. package/dist/_shared/install-phase.js +1 -0
  14. package/dist/_shared/real-node-imports.d.ts +38 -0
  15. package/dist/_shared/real-node-imports.d.ts.map +1 -0
  16. package/dist/_shared/real-node-imports.js +46 -0
  17. package/dist/_shared/retry.d.ts +93 -0
  18. package/dist/_shared/retry.d.ts.map +1 -0
  19. package/dist/_shared/retry.js +161 -0
  20. package/dist/_shared/session-id.d.ts +39 -0
  21. package/dist/_shared/session-id.d.ts.map +1 -0
  22. package/dist/_shared/session-id.js +127 -0
  23. package/dist/_shared/session-router.d.ts +104 -0
  24. package/dist/_shared/session-router.d.ts.map +1 -0
  25. package/dist/_shared/session-router.js +165 -0
  26. package/dist/_shared/w7-frame.d.ts +93 -0
  27. package/dist/_shared/w7-frame.d.ts.map +1 -0
  28. package/dist/_shared/w7-frame.js +402 -0
  29. package/dist/auth/index.d.ts +16 -0
  30. package/dist/auth/index.d.ts.map +1 -0
  31. package/dist/auth/index.js +15 -0
  32. package/dist/auth/middleware.d.ts +85 -0
  33. package/dist/auth/middleware.d.ts.map +1 -0
  34. package/dist/auth/middleware.js +150 -0
  35. package/dist/auth/token.d.ts +96 -0
  36. package/dist/auth/token.d.ts.map +1 -0
  37. package/dist/auth/token.js +226 -0
  38. package/dist/auth/types.d.ts +141 -0
  39. package/dist/auth/types.d.ts.map +1 -0
  40. package/dist/auth/types.js +114 -0
  41. package/dist/bindings/d1.d.ts +102 -0
  42. package/dist/bindings/d1.d.ts.map +1 -0
  43. package/dist/bindings/d1.js +549 -0
  44. package/dist/bindings/kv.d.ts +82 -0
  45. package/dist/bindings/kv.d.ts.map +1 -0
  46. package/dist/bindings/kv.js +290 -0
  47. package/dist/bindings/r2.d.ts +137 -0
  48. package/dist/bindings/r2.d.ts.map +1 -0
  49. package/dist/bindings/r2.js +379 -0
  50. package/dist/cirrus-npm-cjs.generated.d.ts +17 -0
  51. package/dist/cirrus-npm-cjs.generated.d.ts.map +1 -0
  52. package/dist/cirrus-npm-cjs.generated.js +41 -0
  53. package/dist/cirrus-plugin-react.generated.d.ts +34 -0
  54. package/dist/cirrus-plugin-react.generated.d.ts.map +1 -0
  55. package/dist/cirrus-plugin-react.generated.js +33 -0
  56. package/dist/constants.d.ts +34 -0
  57. package/dist/constants.d.ts.map +1 -0
  58. package/dist/constants.js +116 -0
  59. package/dist/esbuild-wasm-bundle.generated.d.ts +41 -0
  60. package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -0
  61. package/dist/esbuild-wasm-bundle.generated.js +40 -0
  62. package/dist/facets/cirrus-real.d.ts +147 -0
  63. package/dist/facets/cirrus-real.d.ts.map +1 -0
  64. package/dist/facets/cirrus-real.js +981 -0
  65. package/dist/facets/inner-do-registry.d.ts +41 -0
  66. package/dist/facets/inner-do-registry.d.ts.map +1 -0
  67. package/dist/facets/inner-do-registry.js +51 -0
  68. package/dist/facets/manager.d.ts +314 -0
  69. package/dist/facets/manager.d.ts.map +1 -0
  70. package/dist/facets/manager.js +2681 -0
  71. package/dist/facets/process.d.ts +318 -0
  72. package/dist/facets/process.d.ts.map +1 -0
  73. package/dist/facets/process.js +567 -0
  74. package/dist/facets/real-vite-fs-shim.d.ts +151 -0
  75. package/dist/facets/real-vite-fs-shim.d.ts.map +1 -0
  76. package/dist/facets/real-vite-fs-shim.js +1279 -0
  77. package/dist/facets/real-vite-hmr.d.ts +142 -0
  78. package/dist/facets/real-vite-hmr.d.ts.map +1 -0
  79. package/dist/facets/real-vite-hmr.js +476 -0
  80. package/dist/facets/vite-dev-server.d.ts +293 -0
  81. package/dist/facets/vite-dev-server.d.ts.map +1 -0
  82. package/dist/facets/vite-dev-server.js +2580 -0
  83. package/dist/facets/wasm-swap-registry.d.ts +261 -0
  84. package/dist/facets/wasm-swap-registry.d.ts.map +1 -0
  85. package/dist/facets/wasm-swap-registry.js +551 -0
  86. package/dist/facets/ws-terminal.d.ts +107 -0
  87. package/dist/facets/ws-terminal.d.ts.map +1 -0
  88. package/dist/facets/ws-terminal.js +207 -0
  89. package/dist/git/commands.d.ts +13 -0
  90. package/dist/git/commands.d.ts.map +1 -0
  91. package/dist/git/commands.js +633 -0
  92. package/dist/git/network-facet.d.ts +61 -0
  93. package/dist/git/network-facet.d.ts.map +1 -0
  94. package/dist/git/network-facet.js +700 -0
  95. package/dist/git-bundle.generated.d.ts +16 -0
  96. package/dist/git-bundle.generated.d.ts.map +1 -0
  97. package/dist/git-bundle.generated.js +15 -0
  98. package/dist/index.d.ts +41 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +73 -0
  101. package/dist/loaders/child-process/spawn-facet.d.ts +90 -0
  102. package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -0
  103. package/dist/loaders/child-process/spawn-facet.js +103 -0
  104. package/dist/loaders/child-process/spawn-pool.d.ts +110 -0
  105. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -0
  106. package/dist/loaders/child-process/spawn-pool.js +161 -0
  107. package/dist/loaders/fanout-pool.d.ts +195 -0
  108. package/dist/loaders/fanout-pool.d.ts.map +1 -0
  109. package/dist/loaders/fanout-pool.js +326 -0
  110. package/dist/loaders/generated-workers.d.ts +26 -0
  111. package/dist/loaders/generated-workers.d.ts.map +1 -0
  112. package/dist/loaders/generated-workers.js +25 -0
  113. package/dist/loaders/index.d.ts +15 -0
  114. package/dist/loaders/index.d.ts.map +1 -0
  115. package/dist/loaders/index.js +13 -0
  116. package/dist/loaders/loader-pool.d.ts +273 -0
  117. package/dist/loaders/loader-pool.d.ts.map +1 -0
  118. package/dist/loaders/loader-pool.js +624 -0
  119. package/dist/loaders/npm-resolve-preamble.d.ts +34 -0
  120. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -0
  121. package/dist/loaders/npm-resolve-preamble.js +237 -0
  122. package/dist/loaders/pre-bundle-preamble.d.ts +41 -0
  123. package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -0
  124. package/dist/loaders/pre-bundle-preamble.js +114 -0
  125. package/dist/loaders/vendor/codegen.d.ts +24 -0
  126. package/dist/loaders/vendor/codegen.d.ts.map +1 -0
  127. package/dist/loaders/vendor/codegen.js +44 -0
  128. package/dist/loaders/vendor/errors.d.ts +24 -0
  129. package/dist/loaders/vendor/errors.d.ts.map +1 -0
  130. package/dist/loaders/vendor/errors.js +46 -0
  131. package/dist/loaders/vendor/index.d.ts +16 -0
  132. package/dist/loaders/vendor/index.d.ts.map +1 -0
  133. package/dist/loaders/vendor/index.js +11 -0
  134. package/dist/loaders/vendor/pool.d.ts +88 -0
  135. package/dist/loaders/vendor/pool.d.ts.map +1 -0
  136. package/dist/loaders/vendor/pool.js +353 -0
  137. package/dist/loaders/vendor/primitives.d.ts +11 -0
  138. package/dist/loaders/vendor/primitives.d.ts.map +1 -0
  139. package/dist/loaders/vendor/primitives.js +33 -0
  140. package/dist/loaders/vendor/serialize.d.ts +3 -0
  141. package/dist/loaders/vendor/serialize.d.ts.map +1 -0
  142. package/dist/loaders/vendor/serialize.js +25 -0
  143. package/dist/loaders/vendor/types.d.ts +60 -0
  144. package/dist/loaders/vendor/types.d.ts.map +1 -0
  145. package/dist/loaders/vendor/types.js +4 -0
  146. package/dist/npm/cache.d.ts +136 -0
  147. package/dist/npm/cache.d.ts.map +1 -0
  148. package/dist/npm/cache.js +429 -0
  149. package/dist/npm/install-batch-facet.d.ts +129 -0
  150. package/dist/npm/install-batch-facet.d.ts.map +1 -0
  151. package/dist/npm/install-batch-facet.js +598 -0
  152. package/dist/npm/install-facet.d.ts +29 -0
  153. package/dist/npm/install-facet.d.ts.map +1 -0
  154. package/dist/npm/install-facet.js +9 -0
  155. package/dist/npm/installer.d.ts +239 -0
  156. package/dist/npm/installer.d.ts.map +1 -0
  157. package/dist/npm/installer.js +2022 -0
  158. package/dist/npm/npx-install.d.ts +74 -0
  159. package/dist/npm/npx-install.d.ts.map +1 -0
  160. package/dist/npm/npx-install.js +302 -0
  161. package/dist/npm/pre-bundle-facet.d.ts +168 -0
  162. package/dist/npm/pre-bundle-facet.d.ts.map +1 -0
  163. package/dist/npm/pre-bundle-facet.js +653 -0
  164. package/dist/npm/r2-cache.d.ts +286 -0
  165. package/dist/npm/r2-cache.d.ts.map +1 -0
  166. package/dist/npm/r2-cache.js +506 -0
  167. package/dist/npm/resolve-facet.d.ts +202 -0
  168. package/dist/npm/resolve-facet.d.ts.map +1 -0
  169. package/dist/npm/resolve-facet.js +833 -0
  170. package/dist/npm/resolve-one-facet.d.ts +204 -0
  171. package/dist/npm/resolve-one-facet.d.ts.map +1 -0
  172. package/dist/npm/resolve-one-facet.js +437 -0
  173. package/dist/npm/resolver.d.ts +109 -0
  174. package/dist/npm/resolver.d.ts.map +1 -0
  175. package/dist/npm/resolver.js +899 -0
  176. package/dist/npm/tarball-stream.d.ts +85 -0
  177. package/dist/npm/tarball-stream.d.ts.map +1 -0
  178. package/dist/npm/tarball-stream.js +205 -0
  179. package/dist/npm/tarball.d.ts +51 -0
  180. package/dist/npm/tarball.d.ts.map +1 -0
  181. package/dist/npm/tarball.js +185 -0
  182. package/dist/observability/diag-counters.d.ts +292 -0
  183. package/dist/observability/diag-counters.d.ts.map +1 -0
  184. package/dist/observability/diag-counters.js +302 -0
  185. package/dist/observability/heap-estimate.d.ts +166 -0
  186. package/dist/observability/heap-estimate.d.ts.map +1 -0
  187. package/dist/observability/heap-estimate.js +192 -0
  188. package/dist/observability/heavy-alloc-coord.d.ts +95 -0
  189. package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
  190. package/dist/observability/heavy-alloc-coord.js +149 -0
  191. package/dist/observability/oom-classify.d.ts +56 -0
  192. package/dist/observability/oom-classify.d.ts.map +1 -0
  193. package/dist/observability/oom-classify.js +118 -0
  194. package/dist/observability/oom-discriminator.d.ts +192 -0
  195. package/dist/observability/oom-discriminator.d.ts.map +1 -0
  196. package/dist/observability/oom-discriminator.js +233 -0
  197. package/dist/real-vite-bundle.generated.d.ts +46 -0
  198. package/dist/real-vite-bundle.generated.d.ts.map +1 -0
  199. package/dist/real-vite-bundle.generated.js +53 -0
  200. package/dist/replica/routing.d.ts +171 -0
  201. package/dist/replica/routing.d.ts.map +1 -0
  202. package/dist/replica/routing.js +288 -0
  203. package/dist/replica/suspension.d.ts +36 -0
  204. package/dist/replica/suspension.d.ts.map +1 -0
  205. package/dist/replica/suspension.js +49 -0
  206. package/dist/router/index.d.ts +156 -0
  207. package/dist/router/index.d.ts.map +1 -0
  208. package/dist/router/index.js +220 -0
  209. package/dist/runtime/assets-loader.d.ts +71 -0
  210. package/dist/runtime/assets-loader.d.ts.map +1 -0
  211. package/dist/runtime/assets-loader.js +128 -0
  212. package/dist/runtime/barrel-detect.d.ts +46 -0
  213. package/dist/runtime/barrel-detect.d.ts.map +1 -0
  214. package/dist/runtime/barrel-detect.js +73 -0
  215. package/dist/runtime/barrel-synthesizer.d.ts +181 -0
  216. package/dist/runtime/barrel-synthesizer.d.ts.map +1 -0
  217. package/dist/runtime/barrel-synthesizer.js +541 -0
  218. package/dist/runtime/bun-repl.d.ts +46 -0
  219. package/dist/runtime/bun-repl.d.ts.map +1 -0
  220. package/dist/runtime/bun-repl.js +326 -0
  221. package/dist/runtime/bun-runner.d.ts +66 -0
  222. package/dist/runtime/bun-runner.d.ts.map +1 -0
  223. package/dist/runtime/bun-runner.js +219 -0
  224. package/dist/runtime/clang-runner.d.ts +38 -0
  225. package/dist/runtime/clang-runner.d.ts.map +1 -0
  226. package/dist/runtime/clang-runner.js +1033 -0
  227. package/dist/runtime/comment-strip.d.ts +37 -0
  228. package/dist/runtime/comment-strip.d.ts.map +1 -0
  229. package/dist/runtime/comment-strip.js +74 -0
  230. package/dist/runtime/esbuild-service.d.ts +201 -0
  231. package/dist/runtime/esbuild-service.d.ts.map +1 -0
  232. package/dist/runtime/esbuild-service.js +1444 -0
  233. package/dist/runtime/esbuild-wasm-bytes.d.ts +87 -0
  234. package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -0
  235. package/dist/runtime/esbuild-wasm-bytes.js +131 -0
  236. package/dist/runtime/framework-detect.d.ts +71 -0
  237. package/dist/runtime/framework-detect.d.ts.map +1 -0
  238. package/dist/runtime/framework-detect.js +178 -0
  239. package/dist/runtime/long-running-handle.d.ts +133 -0
  240. package/dist/runtime/long-running-handle.d.ts.map +1 -0
  241. package/dist/runtime/long-running-handle.js +192 -0
  242. package/dist/runtime/node-repl.d.ts +36 -0
  243. package/dist/runtime/node-repl.d.ts.map +1 -0
  244. package/dist/runtime/node-repl.js +293 -0
  245. package/dist/runtime/node-runner.d.ts +111 -0
  246. package/dist/runtime/node-runner.d.ts.map +1 -0
  247. package/dist/runtime/node-runner.js +194 -0
  248. package/dist/runtime/node-shims.d.ts +24 -0
  249. package/dist/runtime/node-shims.d.ts.map +1 -0
  250. package/dist/runtime/node-shims.js +3019 -0
  251. package/dist/runtime/package-manager.d.ts +118 -0
  252. package/dist/runtime/package-manager.d.ts.map +1 -0
  253. package/dist/runtime/package-manager.js +420 -0
  254. package/dist/runtime/port-registry.d.ts +80 -0
  255. package/dist/runtime/port-registry.d.ts.map +1 -0
  256. package/dist/runtime/port-registry.js +166 -0
  257. package/dist/runtime/process-logs-api.d.ts +102 -0
  258. package/dist/runtime/process-logs-api.d.ts.map +1 -0
  259. package/dist/runtime/process-logs-api.js +253 -0
  260. package/dist/runtime/process-logs.d.ts +311 -0
  261. package/dist/runtime/process-logs.d.ts.map +1 -0
  262. package/dist/runtime/process-logs.js +656 -0
  263. package/dist/runtime/process-table.d.ts +65 -0
  264. package/dist/runtime/process-table.d.ts.map +1 -0
  265. package/dist/runtime/process-table.js +107 -0
  266. package/dist/runtime/project-detect.d.ts +21 -0
  267. package/dist/runtime/project-detect.d.ts.map +1 -0
  268. package/dist/runtime/project-detect.js +46 -0
  269. package/dist/runtime/python-repl.d.ts +66 -0
  270. package/dist/runtime/python-repl.d.ts.map +1 -0
  271. package/dist/runtime/python-repl.js +563 -0
  272. package/dist/runtime/python-runner.d.ts +50 -0
  273. package/dist/runtime/python-runner.d.ts.map +1 -0
  274. package/dist/runtime/python-runner.js +870 -0
  275. package/dist/runtime/repl-session.d.ts +160 -0
  276. package/dist/runtime/repl-session.d.ts.map +1 -0
  277. package/dist/runtime/repl-session.js +393 -0
  278. package/dist/runtime/require-resolver.d.ts +52 -0
  279. package/dist/runtime/require-resolver.d.ts.map +1 -0
  280. package/dist/runtime/require-resolver.js +690 -0
  281. package/dist/runtime/router-basename.d.ts +36 -0
  282. package/dist/runtime/router-basename.d.ts.map +1 -0
  283. package/dist/runtime/router-basename.js +310 -0
  284. package/dist/runtime/ruby-repl.d.ts +43 -0
  285. package/dist/runtime/ruby-repl.d.ts.map +1 -0
  286. package/dist/runtime/ruby-repl.js +229 -0
  287. package/dist/runtime/ruby-runner.d.ts +62 -0
  288. package/dist/runtime/ruby-runner.d.ts.map +1 -0
  289. package/dist/runtime/ruby-runner.js +737 -0
  290. package/dist/runtime/runtime-catalog.d.ts +99 -0
  291. package/dist/runtime/runtime-catalog.d.ts.map +1 -0
  292. package/dist/runtime/runtime-catalog.js +183 -0
  293. package/dist/runtime/runtime-registry.d.ts +136 -0
  294. package/dist/runtime/runtime-registry.d.ts.map +1 -0
  295. package/dist/runtime/runtime-registry.js +318 -0
  296. package/dist/runtime/streams.d.ts +13 -0
  297. package/dist/runtime/streams.d.ts.map +1 -0
  298. package/dist/runtime/streams.js +402 -0
  299. package/dist/runtime/wasi-instance.d.ts +190 -0
  300. package/dist/runtime/wasi-instance.d.ts.map +1 -0
  301. package/dist/runtime/wasi-instance.js +2044 -0
  302. package/dist/runtime/wasm-runner.d.ts +117 -0
  303. package/dist/runtime/wasm-runner.d.ts.map +1 -0
  304. package/dist/runtime/wasm-runner.js +705 -0
  305. package/dist/session/bindings.d.ts +168 -0
  306. package/dist/session/bindings.d.ts.map +1 -0
  307. package/dist/session/bindings.js +524 -0
  308. package/dist/session/ctx-exports.d.ts +16 -0
  309. package/dist/session/ctx-exports.d.ts.map +1 -0
  310. package/dist/session/ctx-exports.js +22 -0
  311. package/dist/session/diag.d.ts +62 -0
  312. package/dist/session/diag.d.ts.map +1 -0
  313. package/dist/session/diag.js +118 -0
  314. package/dist/session/fs-watch.d.ts +132 -0
  315. package/dist/session/fs-watch.d.ts.map +1 -0
  316. package/dist/session/fs-watch.js +255 -0
  317. package/dist/session/helpers.d.ts +150 -0
  318. package/dist/session/helpers.d.ts.map +1 -0
  319. package/dist/session/helpers.js +472 -0
  320. package/dist/session/hibernation.d.ts +145 -0
  321. package/dist/session/hibernation.d.ts.map +1 -0
  322. package/dist/session/hibernation.js +385 -0
  323. package/dist/session/init-phases.d.ts +87 -0
  324. package/dist/session/init-phases.d.ts.map +1 -0
  325. package/dist/session/init-phases.js +137 -0
  326. package/dist/session/init.d.ts +46 -0
  327. package/dist/session/init.d.ts.map +1 -0
  328. package/dist/session/init.js +3579 -0
  329. package/dist/session/keys.d.ts +56 -0
  330. package/dist/session/keys.d.ts.map +1 -0
  331. package/dist/session/keys.js +55 -0
  332. package/dist/session/nimbus-session.d.ts +434 -0
  333. package/dist/session/nimbus-session.d.ts.map +1 -0
  334. package/dist/session/nimbus-session.js +1084 -0
  335. package/dist/session/programmatic.d.ts +105 -0
  336. package/dist/session/programmatic.d.ts.map +1 -0
  337. package/dist/session/programmatic.js +326 -0
  338. package/dist/session/replica-routes.d.ts +68 -0
  339. package/dist/session/replica-routes.d.ts.map +1 -0
  340. package/dist/session/replica-routes.js +77 -0
  341. package/dist/session/routes.d.ts +29 -0
  342. package/dist/session/routes.d.ts.map +1 -0
  343. package/dist/session/routes.js +1331 -0
  344. package/dist/session/rpc.d.ts +323 -0
  345. package/dist/session/rpc.d.ts.map +1 -0
  346. package/dist/session/rpc.js +808 -0
  347. package/dist/session/state-store.d.ts +210 -0
  348. package/dist/session/state-store.d.ts.map +1 -0
  349. package/dist/session/state-store.js +459 -0
  350. package/dist/session/supervisor-rpc.d.ts +269 -0
  351. package/dist/session/supervisor-rpc.d.ts.map +1 -0
  352. package/dist/session/supervisor-rpc.js +437 -0
  353. package/dist/session/ws-hibernation-config.d.ts +64 -0
  354. package/dist/session/ws-hibernation-config.d.ts.map +1 -0
  355. package/dist/session/ws-hibernation-config.js +89 -0
  356. package/dist/session/ws.d.ts +95 -0
  357. package/dist/session/ws.d.ts.map +1 -0
  358. package/dist/session/ws.js +395 -0
  359. package/dist/shell/features.d.ts +410 -0
  360. package/dist/shell/features.d.ts.map +1 -0
  361. package/dist/shell/features.js +1885 -0
  362. package/dist/shell/unix-commands.d.ts +15 -0
  363. package/dist/shell/unix-commands.d.ts.map +1 -0
  364. package/dist/shell/unix-commands.js +3703 -0
  365. package/dist/stubs/lifo-ui.d.ts +10 -0
  366. package/dist/stubs/lifo-ui.d.ts.map +1 -0
  367. package/dist/stubs/lifo-ui.js +1 -0
  368. package/dist/tailwind-play.generated.d.ts +19 -0
  369. package/dist/tailwind-play.generated.d.ts.map +1 -0
  370. package/dist/tailwind-play.generated.js +18 -0
  371. package/dist/vfs/dev-provider.d.ts +64 -0
  372. package/dist/vfs/dev-provider.d.ts.map +1 -0
  373. package/dist/vfs/dev-provider.js +191 -0
  374. package/dist/vfs/events.d.ts +51 -0
  375. package/dist/vfs/events.d.ts.map +1 -0
  376. package/dist/vfs/events.js +127 -0
  377. package/dist/vfs/path.d.ts +30 -0
  378. package/dist/vfs/path.d.ts.map +1 -0
  379. package/dist/vfs/path.js +41 -0
  380. package/dist/vfs/seed-project.d.ts +62 -0
  381. package/dist/vfs/seed-project.d.ts.map +1 -0
  382. package/dist/vfs/seed-project.js +1045 -0
  383. package/dist/vfs/sqlite-vfs.d.ts +435 -0
  384. package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
  385. package/dist/vfs/sqlite-vfs.js +1670 -0
  386. package/dist/vfs/symlink-registry.d.ts +64 -0
  387. package/dist/vfs/symlink-registry.d.ts.map +1 -0
  388. package/dist/vfs/symlink-registry.js +137 -0
  389. package/dist/wrangler/nimbus-wrangler.d.ts +157 -0
  390. package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -0
  391. package/dist/wrangler/nimbus-wrangler.js +931 -0
  392. package/package.json +77 -0
  393. package/public/_assets/cirrus-plugin-react.bundle.js +70740 -0
  394. package/public/_assets/esbuild-0.24.2.wasm +0 -0
  395. package/public/_assets/real-vite-bundle.js +101478 -0
  396. package/public/_assets/rollup.wasm +0 -0
  397. package/public/favicon.svg +8 -0
  398. package/public/index.html +565 -0
  399. package/public/s/index.html +2379 -0
  400. package/scripts/bundle-esbuild-wasm.mjs +244 -0
  401. package/scripts/bundle-facet-workers.mjs +137 -0
  402. package/scripts/bundle-git.mjs +118 -0
  403. package/scripts/bundle-npm-cjs.mjs +363 -0
  404. package/scripts/bundle-plugin-react.mjs +480 -0
  405. package/scripts/bundle-real-vite.mjs +959 -0
  406. package/scripts/bundle-runtime.mjs +600 -0
  407. package/scripts/bundle-tailwind-play.mjs +130 -0
  408. package/scripts/patch-lifo-ui.mjs +131 -0
@@ -0,0 +1,2044 @@
1
+ /**
2
+ * wasi-instance.ts — WASI snapshot_preview1 shim for Nimbus (Wave-1 + Wave-2 + Stream-B).
3
+ *
4
+ * Wave-1 surface (16 fns, kept verbatim):
5
+ * args_get / args_sizes_get / environ_get / environ_sizes_get
6
+ * fd_close / fd_write / fd_read / fd_seek / fd_tell
7
+ * fd_fdstat_get / fd_fdstat_set_flags
8
+ * proc_exit
9
+ * clock_time_get / clock_res_get
10
+ * random_get
11
+ * sched_yield
12
+ *
13
+ * Wave-2 additions (Wave-2; verbatim from wasi-w2 plan):
14
+ * path_open
15
+ * path_create_directory / path_remove_directory
16
+ * path_unlink_file
17
+ * path_rename (uses Nimbus W-3 atomic rename)
18
+ * path_filestat_get / path_filestat_set_times (no-op times — UPGRADED in Stream-B B2)
19
+ * fd_readdir (cookie-paginated)
20
+ * fd_filestat_get / fd_filestat_set_size
21
+ * fd_pread / fd_pwrite
22
+ * fd_prestat_get / fd_prestat_dir_name (real preopens, not EBADF)
23
+ *
24
+ * Stream-B additions (B1-B6, no-op stubs upgraded + new fns):
25
+ * B1: per-file mtime/atime/ctime tracking + in-memory symlink table
26
+ * (additive — files Map shape unchanged; parallel times/symlinks Maps)
27
+ * B2: fd_filestat_set_times / path_filestat_set_times — REAL impls that
28
+ * write mtime/atime; honor ATIM_NOW / MTIM_NOW flags via clock_realtime
29
+ * B3: path_symlink / path_readlink / path_link — REAL impls. Symlink
30
+ * resolution via __wasiResolvePathFull(baseFd, path, followFlag)
31
+ * with POSIX-style 40-deep loop detection (returns ELOOP).
32
+ * B4: fd_allocate — REAL impl (extends bytes to offset+len; zero-fill)
33
+ * B5: proc_raise(sig) — fixed: throws __WasiExit(128 + sig) per POSIX
34
+ * shell convention (SIGABRT=6 → 134, SIGTERM=15 → 143); was constant 128.
35
+ * B6: fd_fdstat_set_rights — REAL impl (tracks per-fd rights mask;
36
+ * fd_fdstat_get returns the tracked mask instead of wide-open).
37
+ *
38
+ * Stream-B P3 additions (sockets via cloudflare:sockets + JSPI):
39
+ * B7: sock_send / sock_recv / sock_shutdown — REAL impls via
40
+ * WebAssembly.Suspending wrapping. path_open('/dev/tcp/<host>/<port>')
41
+ * synthetic-path triggers cloudflare:sockets connect().
42
+ *
43
+ * Stream-B P4 additions (poll_oneoff FULL via JSPI):
44
+ * B8: poll_oneoff — REAL impl. Handles ALL 3 subscription types in a
45
+ * single Promise.race wrapped via WebAssembly.Suspending:
46
+ * CLOCK (REALTIME + MONOTONIC, relative + absolute deadlines via
47
+ * SUBSCRIPTION_CLOCK_ABSTIME flag) → setTimeout to deadline.
48
+ * FD_READ/FD_WRITE on file/dir/stdio/symlink → always-ready
49
+ * (POSIX: regular files never block).
50
+ * FD_READ on socket → real await on
51
+ * socket.readable.getReader().read(); data is stashed on
52
+ * entry.readBuf so subsequent sock_recv sees it pre-loaded.
53
+ * FD_WRITE on socket → always-ready (CF Workers writable
54
+ * streams have unbounded queue from wasm-side perspective).
55
+ * Concurrent-ready drain: after first-promise resolution, probes
56
+ * each remaining promise against a microtask sentinel; collects
57
+ * all currently-resolved events into the output.
58
+ *
59
+ * Stream-B P4 / B9: wasi-threads honest assessment (hard limit):
60
+ * wasi_thread_start is NOT exposed in this shim's import table.
61
+ * Workers facet isolates have no shared-linear-memory primitive
62
+ * across instances, so pthread semantics cannot be implemented
63
+ * correctly. User code that links pthreads gets a wasm-ld
64
+ * "undefined symbol: thread_spawn" error at LINK time — a clear
65
+ * diagnostic instead of a runtime memory-corruption bug.
66
+ * Full rationale + workarounds-considered-and-rejected:
67
+ * docs/wasi-threads-infeasibility.md.
68
+ *
69
+ * Architecture (Wave-2 strategy)
70
+ * ──────────────────────────────
71
+ *
72
+ * Strategy: bulk-snapshot + flush. The supervisor snapshots the user's
73
+ * session VFS subtree into a JSON-serializable {files, dirs} shape, ships
74
+ * it as the loader-pool `context` field. The facet's preamble installs
75
+ * `__wasiInitFS(snapshot)` which builds an in-memory virtual FS keyed by
76
+ * canonical path strings. WASI fd≥3 ops act on that VFS. After `_start`
77
+ * returns, `__wasiSnapshotFS()` extracts the mutated state which the
78
+ * supervisor flushes back into SqliteFS.
79
+ *
80
+ * This avoids a per-call SUPERVISOR RPC for every WASI fn (which would
81
+ * be 5+ RPCs per cat(1) invocation; per-byte overhead). The cost is one
82
+ * snapshot at submit-time + one flush at return. For programs touching
83
+ * a small set of files (clang, sed, awk, etc.) this is overwhelmingly
84
+ * the right trade-off.
85
+ *
86
+ * Errno values (subset)
87
+ * ─────────────────────
88
+ * ESUCCESS = 0 EBADF = 8 ENOENT = 44 EEXIST = 20
89
+ * EISDIR = 31 ENOTDIR = 54 EINVAL = 28 ENOSYS = 52
90
+ * ELOOP = 32 ENOTEMPTY = 55 ENOTCAPABLE = 76
91
+ *
92
+ * Clock IDs
93
+ * ─────────
94
+ * CLOCK_REALTIME = 0 / MONOTONIC = 1 / PROCESS_CPUTIME = 2 / THREAD = 3
95
+ *
96
+ * fstflags (filestat_set_times)
97
+ * ─────────────────────────────
98
+ * __WASI_FSTFLAGS_ATIM = 1
99
+ * __WASI_FSTFLAGS_ATIM_NOW = 2
100
+ * __WASI_FSTFLAGS_MTIM = 4
101
+ * __WASI_FSTFLAGS_MTIM_NOW = 8
102
+ */
103
+ /**
104
+ * Source string injected as the loader-pool `preamble`. The facet's
105
+ * module init evaluates this verbatim so the WASI helpers (`__wasiInitFS`,
106
+ * `__wasiMakeImports`, `__wasiRunStart`, `__wasiSnapshotFS`) are in scope
107
+ * when the user fn runs. Self-contained — no closure captures, no imports.
108
+ */
109
+ export const WASI_INSTANCE_PREAMBLE_SRC = `
110
+ // ── BEGIN: wasi-instance preamble (Wave-1 + Wave-2) ─────────────────────
111
+ // Hand-written WASI snapshot_preview1 shim for Nimbus.
112
+ // Source mirror: src/runtime/wasi-instance.ts. Keep in sync by hand.
113
+
114
+ // errno constants
115
+ const __WASI_ESUCCESS = 0;
116
+ const __WASI_EAGAIN = 6;
117
+ const __WASI_EBADF = 8;
118
+ const __WASI_ECONNREFUSED = 14;
119
+ const __WASI_EEXIST = 20;
120
+ const __WASI_EHOSTUNREACH = 23;
121
+ const __WASI_EINVAL = 28;
122
+ const __WASI_EIO = 29;
123
+ const __WASI_EISDIR = 31;
124
+ const __WASI_ELOOP = 32;
125
+ const __WASI_ENOENT = 44;
126
+ const __WASI_ENOSYS = 52;
127
+ const __WASI_ENOTCONN = 53;
128
+ const __WASI_ENOTDIR = 54;
129
+ const __WASI_ENOTEMPTY = 55;
130
+ const __WASI_ENOTSOCK = 57;
131
+ const __WASI_EPIPE = 64;
132
+ const __WASI_ENOTCAPABLE = 76;
133
+ // clock ids
134
+ const __WASI_CLOCK_REALTIME = 0;
135
+ const __WASI_CLOCK_MONOTONIC = 1;
136
+ const __WASI_CLOCK_PROCESS_CPUTIME_ID = 2;
137
+ const __WASI_CLOCK_THREAD_CPUTIME_ID = 3;
138
+ // oflags
139
+ const __WASI_O_CREAT = 1;
140
+ const __WASI_O_DIRECTORY = 2;
141
+ const __WASI_O_EXCL = 4;
142
+ const __WASI_O_TRUNC = 8;
143
+ // lookupflags (passed to path_open, path_filestat_get, etc.)
144
+ const __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW = 1;
145
+ // fstflags (filestat_set_times)
146
+ const __WASI_FSTFLAGS_ATIM = 1;
147
+ const __WASI_FSTFLAGS_ATIM_NOW = 2;
148
+ const __WASI_FSTFLAGS_MTIM = 4;
149
+ const __WASI_FSTFLAGS_MTIM_NOW = 8;
150
+ // filetypes
151
+ const __WASI_FT_UNKNOWN = 0;
152
+ const __WASI_FT_BLOCK_DEVICE = 1;
153
+ const __WASI_FT_CHARACTER_DEVICE = 2;
154
+ const __WASI_FT_DIRECTORY = 3;
155
+ const __WASI_FT_REGULAR_FILE = 4;
156
+ const __WASI_FT_SOCKET_DGRAM = 5;
157
+ const __WASI_FT_SOCKET_STREAM = 6;
158
+ const __WASI_FT_SYMBOLIC_LINK = 7;
159
+ // preopen types
160
+ const __WASI_PREOPENTYPE_DIR = 0;
161
+ // Symlink resolution loop limit (POSIX SYMLOOP_MAX is typically 8-40).
162
+ const __WASI_SYMLOOP_MAX = 40;
163
+ // Default per-fd rights mask (wide-open).
164
+ const __WASI_RIGHTS_ALL = 0xFFFFFFFFFFFFFFFFn;
165
+ // Stream-B B7: sock_shutdown SD flags.
166
+ const __WASI_SDFLAGS_RD = 1;
167
+ const __WASI_SDFLAGS_WR = 2;
168
+ // Stream-B B8: poll_oneoff subscription / event types.
169
+ const __WASI_EVENTTYPE_CLOCK = 0;
170
+ const __WASI_EVENTTYPE_FD_READ = 1;
171
+ const __WASI_EVENTTYPE_FD_WRITE = 2;
172
+ const __WASI_SUBCLOCKFLAGS_ABSTIME = 1; // SUBSCRIPTION_CLOCK_ABSTIME
173
+ // Stream-B B7: synthetic-path prefix recognised by path_open as a request
174
+ // to open a TCP socket. Mirrors bash's /dev/tcp/<host>/<port> convention
175
+ // (https://www.gnu.org/software/bash/manual/html_node/Redirections.html).
176
+ const __WASI_TCP_PATH_PREFIX = '/dev/tcp/';
177
+
178
+ // Stream-B B7: resolved at preamble module-init via dynamic import.
179
+ // CF docs: "TCP sockets cannot be created in global scope and shared
180
+ // across requests" — so we only IMPORT the module here (cheap symbol
181
+ // resolution); the actual connect() call lives inside path_open which
182
+ // runs at facet-handler time (i.e., within WorkerEntrypoint.execute()).
183
+ // See /workspace/.seal-internal/2026-05-11-stream-b/p3-spike.md §2.
184
+ let __cfSocketConnect = null;
185
+ // Top-level await is supported in workerd ES modules at module-init.
186
+ // If import fails (no cloudflare:sockets binding, or running under a
187
+ // runtime that doesn't expose it), __cfSocketConnect stays null and
188
+ // path_open('/dev/tcp/...') returns ENOSYS with a clear diagnostic.
189
+ try {
190
+ const __mod = await import('cloudflare:sockets');
191
+ __cfSocketConnect = __mod.connect;
192
+ } catch (__e) {
193
+ // fail-soft: socket support disabled this call.
194
+ }
195
+
196
+ class __WasiExit { constructor(code) { this.code = code | 0; } }
197
+
198
+ // ─── Virtual filesystem state ───────────────────────────────────────────
199
+ //
200
+ // __wasiInitFS({ root, preopens, files, dirs, times?, symlinks? }) —
201
+ // install a per-call FS:
202
+ // root string — canonical session root, e.g. 'home/user/wasi-w2'.
203
+ // preopens Array<{ wasiPath, vfsPath }> — fd>=3 preopens (in order).
204
+ // files Record<vfsPath, base64 string> — initial file contents.
205
+ // dirs Array<vfsPath> — initial directory list.
206
+ // times Record<vfsPath, {mtime, atime, ctime}> | undefined
207
+ // — per-path nanosecond timestamps (Stream-B B1). Optional;
208
+ // paths without entries default to "now" at init time.
209
+ // Values are decimal strings to survive JSON.serialize
210
+ // (BigInt is not JSON-safe); converted to BigInt internally.
211
+ // symlinks Record<vfsPath, targetPath> | undefined
212
+ // — Stream-B B3 in-memory symlink table. Optional; defaults
213
+ // to empty. Target paths are canonicalized at insertion.
214
+ //
215
+ // After _start returns, __wasiSnapshotFS() extracts:
216
+ // {
217
+ // filesWritten: Record<vfsPath, base64 string>, // new + modified
218
+ // filesDeleted: string[], // unlinked
219
+ // dirsCreated: string[], // mkdir'd
220
+ // dirsDeleted: string[], // rmdir'd
221
+ // timesChanged: Record<vfsPath, {mtime, atime, ctime}> // B1 — new or modified mtime/atime
222
+ // symlinksCreated: Record<vfsPath, targetPath> // B3 — new symlinks
223
+ // symlinksDeleted: string[] // B3 — removed symlinks
224
+ // }
225
+ //
226
+ // Backward-compat: all new fields are additive. Callers that don't pass
227
+ // 'times'/'symlinks' to __wasiInitFS see identical behavior to pre-B1.
228
+ // Supervisors that don't read 'timesChanged'/'symlinks*' from the
229
+ // snapshot see identical behavior to pre-B1.
230
+
231
+ let __wasiFS = null; // populated by __wasiInitFS
232
+ let __wasiPreopens = []; // [{ wasiPath, vfsPath, fd }, ...]
233
+
234
+ // All paths are stored in canonical form: no leading '/', no '..', no
235
+ // double slashes. The wasm program sees '/foo/bar.txt' (with leading
236
+ // slash) but the VFS keys are 'home/user/.../foo/bar.txt'.
237
+ function __wasiCanonicalize(p) {
238
+ if (!p) return '';
239
+ const parts = p.split('/');
240
+ const out = [];
241
+ for (const seg of parts) {
242
+ if (seg === '' || seg === '.') continue;
243
+ if (seg === '..') { if (out.length > 0) out.pop(); continue; }
244
+ out.push(seg);
245
+ }
246
+ return out.join('/');
247
+ }
248
+
249
+ // Wall-clock at init, used as the default mtime/atime/ctime for any path
250
+ // that arrives without an explicit times entry. Captured once per call so
251
+ // all "default-init" timestamps within a call share a value (mtime ==
252
+ // atime == ctime), matching what a real cold-load would produce.
253
+ function __wasiNowNs() {
254
+ // Date.now() is ms since epoch; multiply by 1e6 → ns.
255
+ return BigInt(Date.now()) * 1000000n;
256
+ }
257
+ function __wasiInitFS(opts) {
258
+ const files = new Map(); // canonicalVfsPath → Uint8Array
259
+ const dirs = new Set(); // canonicalVfsPath
260
+ // Stream-B B1: parallel timestamp + symlink maps.
261
+ const times = new Map(); // canonicalVfsPath → {mtime: BigInt, atime: BigInt, ctime: BigInt}
262
+ const symlinks = new Map(); // canonicalVfsPath → targetPath (canonical)
263
+ // mirror originals for diff at flush time
264
+ const origFiles = new Map();
265
+ const origDirs = new Set();
266
+ const origTimes = new Map();
267
+ const origSymlinks = new Map();
268
+ const nowNs = __wasiNowNs();
269
+ for (const [path, b64] of Object.entries(opts.files || {})) {
270
+ const canon = __wasiCanonicalize(path);
271
+ const bin = atob(b64);
272
+ const u8 = new Uint8Array(bin.length);
273
+ for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
274
+ files.set(canon, u8);
275
+ origFiles.set(canon, u8.slice()); // copy so subsequent mutations detect change
276
+ // Default timestamps (overwritten below if opts.times has this path).
277
+ const t = { mtime: nowNs, atime: nowNs, ctime: nowNs };
278
+ times.set(canon, t);
279
+ origTimes.set(canon, { mtime: t.mtime, atime: t.atime, ctime: t.ctime });
280
+ }
281
+ for (const path of opts.dirs || []) {
282
+ const canon = __wasiCanonicalize(path);
283
+ dirs.add(canon);
284
+ origDirs.add(canon);
285
+ if (!times.has(canon)) {
286
+ const t = { mtime: nowNs, atime: nowNs, ctime: nowNs };
287
+ times.set(canon, t);
288
+ origTimes.set(canon, { mtime: t.mtime, atime: t.atime, ctime: t.ctime });
289
+ }
290
+ }
291
+ // Stream-B B1: load explicit per-path timestamps if supervisor provided them.
292
+ // Values arrive as decimal strings (BigInt → JSON.stringify safe) or numbers.
293
+ for (const [path, t] of Object.entries(opts.times || {})) {
294
+ const canon = __wasiCanonicalize(path);
295
+ const cur = times.get(canon);
296
+ if (!cur) continue; // path not yet in files/dirs; skip
297
+ const mtime = (t && t.mtime !== undefined) ? BigInt(t.mtime) : cur.mtime;
298
+ const atime = (t && t.atime !== undefined) ? BigInt(t.atime) : cur.atime;
299
+ const ctime = (t && t.ctime !== undefined) ? BigInt(t.ctime) : cur.ctime;
300
+ times.set(canon, { mtime, atime, ctime });
301
+ origTimes.set(canon, { mtime, atime, ctime });
302
+ }
303
+ // Stream-B B3: load explicit symlinks if supervisor provided them.
304
+ for (const [path, target] of Object.entries(opts.symlinks || {})) {
305
+ const canon = __wasiCanonicalize(path);
306
+ symlinks.set(canon, String(target));
307
+ origSymlinks.set(canon, String(target));
308
+ }
309
+ __wasiFS = {
310
+ root: __wasiCanonicalize(opts.root || ''),
311
+ files, dirs, times, symlinks,
312
+ origFiles, origDirs, origTimes, origSymlinks,
313
+ };
314
+ // Reset fd table baseline; install preopens as fd 3, 4, 5, ...
315
+ __wasiPreopens = [];
316
+ fdTable.clear();
317
+ fdTable.set(0, { kind: 'stdin' });
318
+ fdTable.set(1, { kind: 'stdout' });
319
+ fdTable.set(2, { kind: 'stderr' });
320
+ nextFd = 3;
321
+ for (const po of (opts.preopens || [])) {
322
+ const fd = nextFd++;
323
+ const vfsPath = __wasiCanonicalize(po.vfsPath);
324
+ fdTable.set(fd, { kind: 'preopen', wasiPath: po.wasiPath, vfsPath });
325
+ __wasiPreopens.push({ fd, wasiPath: po.wasiPath, vfsPath });
326
+ if (!dirs.has(vfsPath)) dirs.add(vfsPath);
327
+ if (!times.has(vfsPath)) {
328
+ const t = { mtime: nowNs, atime: nowNs, ctime: nowNs };
329
+ times.set(vfsPath, t);
330
+ origTimes.set(vfsPath, { mtime: t.mtime, atime: t.atime, ctime: t.ctime });
331
+ }
332
+ }
333
+ }
334
+
335
+ // Stream-B B7 helper: open a TCP socket via cloudflare:sockets when
336
+ // path_open is invoked on a /dev/tcp/<host>/<port> synthetic path.
337
+ // Returns a WASI errno. Allocates a new fd of kind:'socket' on success
338
+ // and writes it to fdOutPtr.
339
+ function __wasiOpenTcpSocket(pathArg, fdflags, fdOutPtr, writeU32LE) {
340
+ if (typeof __cfSocketConnect !== 'function') {
341
+ // cloudflare:sockets unavailable in this runtime. Report ENOSYS so
342
+ // the user program sees a clear errno rather than a hang.
343
+ return __WASI_ENOSYS;
344
+ }
345
+ // pathArg shape: "/dev/tcp/<host>/<port>".
346
+ const tail = pathArg.substring(__WASI_TCP_PATH_PREFIX.length);
347
+ const slashIdx = tail.lastIndexOf('/');
348
+ if (slashIdx <= 0 || slashIdx === tail.length - 1) return __WASI_EINVAL;
349
+ const host = tail.substring(0, slashIdx);
350
+ const portStr = tail.substring(slashIdx + 1);
351
+ const port = parseInt(portStr, 10);
352
+ if (!host || !(port > 0 && port < 65536)) return __WASI_EINVAL;
353
+ let socket;
354
+ try {
355
+ // Per CF docs: connect() is sync (returns Socket immediately); the
356
+ // socket.opened promise resolves when the TCP handshake completes.
357
+ // We do NOT await opened here — sock_send/sock_recv will await it
358
+ // implicitly via the writable/readable streams (or via socket.opened
359
+ // before the first read/write).
360
+ //
361
+ // Prod-verify-fix: pass allowHalfOpen=true. Default is false,
362
+ // which makes the writable side close automatically on EOF AND
363
+ // (per empirical prod behaviour) makes a manual writer.close()
364
+ // affect readable-side delivery. POSIX shutdown(SHUT_WR) requires
365
+ // half-close semantics: the user can stop sending while still
366
+ // receiving the peer's response. tcpbin.com:4242 (and most
367
+ // request/response protocols) rely on this — client signals EOF
368
+ // via half-close, server completes its echo, client reads it.
369
+ // See https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#socketoptions
370
+ socket = __cfSocketConnect({ hostname: host, port }, { allowHalfOpen: true });
371
+ } catch (e) {
372
+ // Synchronous errors from connect() (e.g. invalid address). Surface
373
+ // as ECONNREFUSED since the user-observable behavior is the same.
374
+ return __WASI_ECONNREFUSED;
375
+ }
376
+ const fd = nextFd++;
377
+ fdTable.set(fd, {
378
+ kind: 'socket',
379
+ socket,
380
+ reader: null, // lazy: getReader() on first sock_recv
381
+ writer: null, // lazy: getWriter() on first sock_send
382
+ readBuf: new Uint8Array(0),
383
+ readBufOffset: 0,
384
+ eof: false,
385
+ closed: false,
386
+ halfClosedWr: false,
387
+ fdflags: fdflags | 0,
388
+ });
389
+ writeU32LE(fdOutPtr, fd);
390
+ return __WASI_ESUCCESS;
391
+ }
392
+
393
+ // Stream-B B1+B2 helpers: update tracked timestamps for a path. Idempotent.
394
+ // Caller passes nanosecond BigInt(s); pass null for fields to keep unchanged.
395
+ function __wasiTouchTimes(canonPath, mtimeNs, atimeNs, ctimeNs) {
396
+ if (!__wasiFS) return;
397
+ const cur = __wasiFS.times.get(canonPath);
398
+ if (cur) {
399
+ if (mtimeNs !== null) cur.mtime = mtimeNs;
400
+ if (atimeNs !== null) cur.atime = atimeNs;
401
+ if (ctimeNs !== null) cur.ctime = ctimeNs;
402
+ } else {
403
+ const now = __wasiNowNs();
404
+ __wasiFS.times.set(canonPath, {
405
+ mtime: mtimeNs !== null ? mtimeNs : now,
406
+ atime: atimeNs !== null ? atimeNs : now,
407
+ ctime: ctimeNs !== null ? ctimeNs : now,
408
+ });
409
+ }
410
+ }
411
+ // Convenience: bump mtime + ctime to "now" on a write/create.
412
+ function __wasiBumpMtime(canonPath) {
413
+ const now = __wasiNowNs();
414
+ __wasiTouchTimes(canonPath, now, null, now);
415
+ }
416
+
417
+ function __wasiSnapshotFS() {
418
+ if (!__wasiFS) return null;
419
+ const filesWritten = {};
420
+ const filesDeleted = [];
421
+ const dirsCreated = [];
422
+ const dirsDeleted = [];
423
+ const timesChanged = {}; // Stream-B B1: mtime/atime/ctime diffs
424
+ const symlinksCreated = {}; // Stream-B B3: new symlinks
425
+ const symlinksDeleted = []; // Stream-B B3: removed symlinks
426
+ // files: anything in __wasiFS.files not in orig, or whose bytes differ.
427
+ for (const [path, bytes] of __wasiFS.files) {
428
+ const orig = __wasiFS.origFiles.get(path);
429
+ let same = !!orig && orig.length === bytes.length;
430
+ if (same) {
431
+ for (let i = 0; i < bytes.length; i++) {
432
+ if (orig[i] !== bytes[i]) { same = false; break; }
433
+ }
434
+ }
435
+ if (!same) {
436
+ // base64 encode
437
+ let s = '';
438
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
439
+ filesWritten[path] = btoa(s);
440
+ }
441
+ }
442
+ // files: anything in orig not in current
443
+ for (const path of __wasiFS.origFiles.keys()) {
444
+ if (!__wasiFS.files.has(path)) filesDeleted.push(path);
445
+ }
446
+ // dirs
447
+ for (const path of __wasiFS.dirs) {
448
+ if (!__wasiFS.origDirs.has(path)) dirsCreated.push(path);
449
+ }
450
+ for (const path of __wasiFS.origDirs) {
451
+ if (!__wasiFS.dirs.has(path)) dirsDeleted.push(path);
452
+ }
453
+ // Stream-B B1: times diff. BigInt → decimal string (JSON.stringify-safe).
454
+ for (const [path, t] of __wasiFS.times) {
455
+ const orig = __wasiFS.origTimes.get(path);
456
+ if (!orig || orig.mtime !== t.mtime || orig.atime !== t.atime || orig.ctime !== t.ctime) {
457
+ timesChanged[path] = {
458
+ mtime: t.mtime.toString(),
459
+ atime: t.atime.toString(),
460
+ ctime: t.ctime.toString(),
461
+ };
462
+ }
463
+ }
464
+ // Stream-B B3: symlink diff.
465
+ for (const [path, target] of __wasiFS.symlinks) {
466
+ if (__wasiFS.origSymlinks.get(path) !== target) {
467
+ symlinksCreated[path] = target;
468
+ }
469
+ }
470
+ for (const path of __wasiFS.origSymlinks.keys()) {
471
+ if (!__wasiFS.symlinks.has(path)) symlinksDeleted.push(path);
472
+ }
473
+ return {
474
+ filesWritten, filesDeleted, dirsCreated, dirsDeleted,
475
+ timesChanged, symlinksCreated, symlinksDeleted,
476
+ };
477
+ }
478
+
479
+ // ─── fd table ──────────────────────────────────────────────────────────
480
+ //
481
+ // Entry shapes:
482
+ // { kind: 'stdin' | 'stdout' | 'stderr' }
483
+ // { kind: 'preopen', wasiPath, vfsPath, rights? }
484
+ // { kind: 'file', vfsPath, offset, oflags, fdflags, rights? }
485
+ // { kind: 'dir', vfsPath, readdirEntries: null | Array, cookie, rights? }
486
+ // { kind: 'symlink', vfsPath, target, rights? } // Stream-B B3 (only when O_NOFOLLOW)
487
+ //
488
+ // Stream-B B6: 'rights' is an optional BigInt mask. When set, fd_fdstat_get
489
+ // returns it (instead of the wide-open default). fd_fdstat_set_rights
490
+ // writes to it. The mask is advisory in this shim (we don't enforce per-fn
491
+ // rights checks in v1 — single-tenant facet, no untrusted callers).
492
+ const fdTable = new Map();
493
+ let nextFd = 3;
494
+
495
+ // Resolve a WASI path against a preopen fd; returns the canonical VFS path
496
+ // WITHOUT following symlinks. Stream-B kept this name compatible with all
497
+ // existing call sites (~10 of them inside the imports object).
498
+ //
499
+ // wasi-path-fix: a single preopen { wasiPath: '/', vfsPath: 'home/user' }
500
+ // exposes the user's home as the wasm-side root. wasi-libc strips '/'
501
+ // from absolute paths and hands the rest to path_open. For "/tmp/x" the
502
+ // stripped path "tmp/x" + vfsPath "home/user" yields "home/user/tmp/x"
503
+ // (correct). For "/home/user/x" the stripped path "home/user/x" +
504
+ // vfsPath "home/user" yields "home/user/home/user/x" — the file lands
505
+ // at the wrong location. The user mental model is that "/home/user/x"
506
+ // IS the user's file at home/user/x in the VFS.
507
+ //
508
+ // Fix: in __wasiResolvePath, when baseFd is a preopen AND the supplied
509
+ // pathStr already starts with the preopen's vfsPath (followed by '/' or
510
+ // end-of-string), strip the vfsPath prefix BEFORE prepending it. This
511
+ // makes "/home/user/x" → wasi-libc strip → "home/user/x" → resolver
512
+ // strip vfsPath → "x" → final canonical "home/user/x" ✓. Paths that
513
+ // don't share the vfsPath prefix (like "tmp/x") follow the legacy path.
514
+ //
515
+ // Skip the strip when vfsPath is empty (no chroot mismatch possible).
516
+ // Be careful with segment boundaries: vfsPath "home/user" must NOT
517
+ // strip from path "home/userfoo/bar" (it's a different directory).
518
+ function __wasiResolvePath(baseFd, pathStr) {
519
+ const entry = fdTable.get(baseFd);
520
+ if (!entry) return null;
521
+ let baseVfs;
522
+ if (entry.kind === 'preopen' || entry.kind === 'dir') baseVfs = entry.vfsPath;
523
+ else return null;
524
+ // Strip leading './' or '/' segments (POSIX legacy from wasi-libc).
525
+ let trimmed = pathStr.replace(/^\\.?\\/+/, '').replace(/^\\/+/, '');
526
+ // wasi-path-fix: chroot-collision strip. Only applies to preopens (not
527
+ // 'dir' fds — those are opened via path_open with already-resolved
528
+ // paths). Only when baseVfs is non-empty AND trimmed starts with
529
+ // baseVfs at a segment boundary.
530
+ if (entry.kind === 'preopen' && baseVfs.length > 0) {
531
+ if (trimmed === baseVfs) {
532
+ trimmed = '';
533
+ } else if (trimmed.length > baseVfs.length &&
534
+ trimmed.charCodeAt(baseVfs.length) === 47 /* '/' */ &&
535
+ trimmed.substring(0, baseVfs.length) === baseVfs) {
536
+ trimmed = trimmed.substring(baseVfs.length + 1);
537
+ }
538
+ }
539
+ return __wasiCanonicalize(baseVfs + '/' + trimmed);
540
+ }
541
+
542
+ // Stream-B B3: resolve a path WITH optional symlink-follow + POSIX loop
543
+ // detection. Returns {path: canonical, isSymlink: bool, err?: errno}.
544
+ // - If followFlag is true (default — matches LOOKUPFLAGS_SYMLINK_FOLLOW),
545
+ // walks the symlink chain up to __WASI_SYMLOOP_MAX depth. ELOOP if exceeded.
546
+ // - If followFlag is false, returns the bare resolved path; if the path
547
+ // itself is in __wasiFS.symlinks, isSymlink=true so callers know to
548
+ // open it as a symlink fd rather than a regular file.
549
+ // Errors return {path: '', isSymlink: false, err: __WASI_E...}.
550
+ function __wasiResolvePathFull(baseFd, pathStr, followFlag) {
551
+ let p = __wasiResolvePath(baseFd, pathStr);
552
+ if (p === null) return { path: '', isSymlink: false, err: __WASI_EBADF };
553
+ if (!__wasiFS) return { path: p, isSymlink: false };
554
+ if (!followFlag) {
555
+ return { path: p, isSymlink: __wasiFS.symlinks.has(p), err: __WASI_ESUCCESS };
556
+ }
557
+ // Follow symlinks with bounded loop counter.
558
+ let depth = 0;
559
+ while (__wasiFS.symlinks.has(p)) {
560
+ if (depth++ >= __WASI_SYMLOOP_MAX) {
561
+ return { path: p, isSymlink: true, err: __WASI_ELOOP };
562
+ }
563
+ const target = __wasiFS.symlinks.get(p);
564
+ // Target may be relative or absolute. Absolute targets (leading '/')
565
+ // resolve against the same preopen root as the original path; relative
566
+ // targets resolve against the directory containing the symlink.
567
+ let next;
568
+ if (target.startsWith('/')) {
569
+ // Resolve against the preopen of baseFd.
570
+ const e = fdTable.get(baseFd);
571
+ const baseVfs = (e && (e.kind === 'preopen' || e.kind === 'dir')) ? e.vfsPath : '';
572
+ next = __wasiCanonicalize(baseVfs + '/' + target);
573
+ } else {
574
+ const lastSlash = p.lastIndexOf('/');
575
+ const parent = lastSlash >= 0 ? p.substring(0, lastSlash) : '';
576
+ next = __wasiCanonicalize(parent + '/' + target);
577
+ }
578
+ p = next;
579
+ }
580
+ return { path: p, isSymlink: false, err: __WASI_ESUCCESS };
581
+ }
582
+
583
+ // ─── makeImports ────────────────────────────────────────────────────────
584
+
585
+ function __wasiMakeImports(opts) {
586
+ // opts: { argv, env, getMemory, stdoutWrite?, stderrWrite? }
587
+ const argv = opts.argv || [];
588
+ const envArr = [];
589
+ if (opts.env) for (const k of Object.keys(opts.env)) envArr.push(k + '=' + opts.env[k]);
590
+ const utf8enc = new TextEncoder();
591
+ const utf8dec = new TextDecoder();
592
+
593
+ function view() { return new DataView(opts.getMemory().buffer); }
594
+ function u8() { return new Uint8Array(opts.getMemory().buffer); }
595
+ function writeU32LE(off, v) { view().setUint32(off, v >>> 0, true); }
596
+ function writeU64LE(off, v) {
597
+ const dv = view();
598
+ if (typeof v === 'bigint') { dv.setBigUint64(off, v, true); return; }
599
+ const lo = (v >>> 0);
600
+ const hi = Math.floor(v / 4294967296) >>> 0;
601
+ dv.setUint32(off, lo, true);
602
+ dv.setUint32(off + 4, hi, true);
603
+ }
604
+ function readPath(ptr, len) {
605
+ const bytes = u8().subarray(ptr, ptr + len);
606
+ return utf8dec.decode(bytes);
607
+ }
608
+
609
+ let stdoutBuf = '';
610
+ let stderrBuf = '';
611
+ function appendStream(kind, bytes) {
612
+ const s = utf8dec.decode(bytes);
613
+ if (kind === 'stdout') {
614
+ stdoutBuf += s;
615
+ if (opts.stdoutWrite) opts.stdoutWrite(s);
616
+ } else {
617
+ stderrBuf += s;
618
+ if (opts.stderrWrite) opts.stderrWrite(s);
619
+ }
620
+ }
621
+
622
+ // ── Helpers operating on the VFS state ───────────────────────────────
623
+ function getFile(vfsPath) {
624
+ if (!__wasiFS) return null;
625
+ return __wasiFS.files.get(vfsPath) || null;
626
+ }
627
+ function hasDir(vfsPath) {
628
+ if (!__wasiFS) return false;
629
+ if (__wasiFS.dirs.has(vfsPath)) return true;
630
+ // Root preopens implicitly exist as dirs.
631
+ return false;
632
+ }
633
+ function setFile(vfsPath, bytes) {
634
+ __wasiFS.files.set(vfsPath, bytes);
635
+ // Stream-B B1: bump mtime+ctime on every write. atime stays as-is
636
+ // (read paths bump atime explicitly via touchAccess()).
637
+ __wasiBumpMtime(vfsPath);
638
+ }
639
+ function unsetFile(vfsPath) {
640
+ __wasiFS.files.delete(vfsPath);
641
+ __wasiFS.times.delete(vfsPath);
642
+ }
643
+ function touchAccess(vfsPath) {
644
+ // Stream-B B1: bump atime on a read. mtime/ctime unchanged.
645
+ if (!__wasiFS) return;
646
+ const cur = __wasiFS.times.get(vfsPath);
647
+ if (cur) cur.atime = __wasiNowNs();
648
+ }
649
+ function ensureParentDirs(vfsPath) {
650
+ // Ensure all ancestor dirs of vfsPath exist (they should, but mkdir -p
651
+ // semantics are handled in path_create_directory by callers).
652
+ const parts = vfsPath.split('/');
653
+ if (parts.length <= 1) return;
654
+ let p = '';
655
+ for (let i = 0; i < parts.length - 1; i++) {
656
+ p = p ? (p + '/' + parts[i]) : parts[i];
657
+ __wasiFS.dirs.add(p);
658
+ // Stream-B B1: ensure ancestor dirs have a times entry.
659
+ if (!__wasiFS.times.has(p)) {
660
+ const now = __wasiNowNs();
661
+ __wasiFS.times.set(p, { mtime: now, atime: now, ctime: now });
662
+ }
663
+ }
664
+ }
665
+ function readdirChildren(vfsPath) {
666
+ const out = [];
667
+ const prefix = vfsPath === '' ? '' : vfsPath + '/';
668
+ // Files
669
+ for (const path of __wasiFS.files.keys()) {
670
+ if (path.startsWith(prefix)) {
671
+ const rest = path.substring(prefix.length);
672
+ if (rest && rest.indexOf('/') === -1) {
673
+ out.push({ name: rest, type: __WASI_FT_REGULAR_FILE });
674
+ }
675
+ }
676
+ }
677
+ // Dirs
678
+ for (const path of __wasiFS.dirs) {
679
+ if (path !== vfsPath && path.startsWith(prefix)) {
680
+ const rest = path.substring(prefix.length);
681
+ if (rest && rest.indexOf('/') === -1) {
682
+ out.push({ name: rest, type: __WASI_FT_DIRECTORY });
683
+ }
684
+ }
685
+ }
686
+ return out;
687
+ }
688
+
689
+ const imports = {
690
+ // ── args / env ──
691
+ args_get(argvPtr, argvBufPtr) {
692
+ let buf = argvBufPtr;
693
+ const memU8 = u8();
694
+ const dv = view();
695
+ for (let i = 0; i < argv.length; i++) {
696
+ dv.setUint32(argvPtr + i * 4, buf, true);
697
+ const bytes = utf8enc.encode(argv[i] + '\\0');
698
+ memU8.set(bytes, buf);
699
+ buf += bytes.length;
700
+ }
701
+ return __WASI_ESUCCESS;
702
+ },
703
+ args_sizes_get(argcPtr, sizePtr) {
704
+ let total = 0;
705
+ for (let i = 0; i < argv.length; i++) total += utf8enc.encode(argv[i]).length + 1;
706
+ writeU32LE(argcPtr, argv.length);
707
+ writeU32LE(sizePtr, total);
708
+ return __WASI_ESUCCESS;
709
+ },
710
+ environ_get(environPtr, envBufPtr) {
711
+ let buf = envBufPtr;
712
+ const memU8 = u8();
713
+ const dv = view();
714
+ for (let i = 0; i < envArr.length; i++) {
715
+ dv.setUint32(environPtr + i * 4, buf, true);
716
+ const bytes = utf8enc.encode(envArr[i] + '\\0');
717
+ memU8.set(bytes, buf);
718
+ buf += bytes.length;
719
+ }
720
+ return __WASI_ESUCCESS;
721
+ },
722
+ environ_sizes_get(envcPtr, sizePtr) {
723
+ let total = 0;
724
+ for (let i = 0; i < envArr.length; i++) total += utf8enc.encode(envArr[i]).length + 1;
725
+ writeU32LE(envcPtr, envArr.length);
726
+ writeU32LE(sizePtr, total);
727
+ return __WASI_ESUCCESS;
728
+ },
729
+
730
+ // ── fd basic ──
731
+ fd_close(fd) {
732
+ if (fd === 0 || fd === 1 || fd === 2) return __WASI_ESUCCESS;
733
+ if (!fdTable.has(fd)) return __WASI_EBADF;
734
+ // Don't delete preopens — they're meant to persist for the program lifetime.
735
+ const entry = fdTable.get(fd);
736
+ if (entry.kind === 'preopen') return __WASI_ESUCCESS;
737
+ // Stream-B B7: best-effort close of socket streams. The actual
738
+ // socket.close() is fire-and-forget (sync return per WASI) — if
739
+ // the program wants to await closure it should sock_shutdown
740
+ // first. We DO drop the fd-table entry so subsequent ops on this
741
+ // fd see EBADF.
742
+ if (entry.kind === 'socket' && !entry.closed) {
743
+ try { entry.socket.close(); } catch {}
744
+ entry.closed = true;
745
+ }
746
+ fdTable.delete(fd);
747
+ return __WASI_ESUCCESS;
748
+ },
749
+
750
+ fd_read(fd, iovsPtr, iovsLen, nreadPtr) {
751
+ if (fd === 0) { writeU32LE(nreadPtr, 0); return __WASI_ESUCCESS; }
752
+ const entry = fdTable.get(fd);
753
+ if (!entry || entry.kind !== 'file') return __WASI_EBADF;
754
+ const file = getFile(entry.vfsPath);
755
+ if (!file) return __WASI_ENOENT;
756
+ const dv = view();
757
+ const memU8 = u8();
758
+ let total = 0;
759
+ for (let i = 0; i < iovsLen; i++) {
760
+ const iov = iovsPtr + i * 8;
761
+ const bufPtr = dv.getUint32(iov, true);
762
+ const bufLen = dv.getUint32(iov + 4, true);
763
+ const remain = file.length - entry.offset;
764
+ if (remain <= 0) break;
765
+ const n = Math.min(bufLen, remain);
766
+ memU8.set(file.subarray(entry.offset, entry.offset + n), bufPtr);
767
+ entry.offset += n;
768
+ total += n;
769
+ if (n < bufLen) break;
770
+ }
771
+ writeU32LE(nreadPtr, total);
772
+ return __WASI_ESUCCESS;
773
+ },
774
+
775
+ fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {
776
+ const dv = view();
777
+ const memU8 = u8();
778
+ // Gather all iov bytes
779
+ let total = 0;
780
+ const parts = [];
781
+ for (let i = 0; i < iovsLen; i++) {
782
+ const iov = iovsPtr + i * 8;
783
+ const bufPtr = dv.getUint32(iov, true);
784
+ const bufLen = dv.getUint32(iov + 4, true);
785
+ if (bufLen > 0) parts.push(memU8.slice(bufPtr, bufPtr + bufLen));
786
+ total += bufLen;
787
+ }
788
+ let combined;
789
+ if (parts.length === 0) combined = new Uint8Array(0);
790
+ else if (parts.length === 1) combined = parts[0];
791
+ else {
792
+ combined = new Uint8Array(total);
793
+ let off = 0;
794
+ for (const p of parts) { combined.set(p, off); off += p.length; }
795
+ }
796
+ if (fd === 1 || fd === 2) {
797
+ appendStream(fd === 1 ? 'stdout' : 'stderr', combined);
798
+ writeU32LE(nwrittenPtr, total);
799
+ return __WASI_ESUCCESS;
800
+ }
801
+ const entry = fdTable.get(fd);
802
+ if (!entry || entry.kind !== 'file') return __WASI_EBADF;
803
+ const file = getFile(entry.vfsPath) || new Uint8Array(0);
804
+ // Splice combined into file at entry.offset
805
+ const newLen = Math.max(file.length, entry.offset + total);
806
+ const next = new Uint8Array(newLen);
807
+ next.set(file, 0);
808
+ next.set(combined, entry.offset);
809
+ setFile(entry.vfsPath, next);
810
+ entry.offset += total;
811
+ writeU32LE(nwrittenPtr, total);
812
+ return __WASI_ESUCCESS;
813
+ },
814
+
815
+ fd_seek(fd, offsetArg, whence, newOffsetPtr) {
816
+ // WASI/V8 passes i64 args as BigInt. Wave-1 mistakenly assumed
817
+ // (lo, hi) i32 pairs; that worked only because hello-world never
818
+ // exercised seek. Wave-2: accept BigInt directly.
819
+ if (fd === 0 || fd === 1 || fd === 2) {
820
+ writeU64LE(newOffsetPtr, 0n);
821
+ return __WASI_ESUCCESS;
822
+ }
823
+ const entry = fdTable.get(fd);
824
+ if (!entry || entry.kind !== 'file') return __WASI_EBADF;
825
+ const delta = typeof offsetArg === 'bigint' ? offsetArg : BigInt(offsetArg | 0);
826
+ const cur = BigInt(entry.offset);
827
+ const file = getFile(entry.vfsPath);
828
+ const fileLen = file ? BigInt(file.length) : 0n;
829
+ let next;
830
+ if (whence === 0) next = delta;
831
+ else if (whence === 1) next = cur + delta;
832
+ else if (whence === 2) next = fileLen + delta;
833
+ else return __WASI_EINVAL;
834
+ if (next < 0n) return __WASI_EINVAL;
835
+ entry.offset = Number(next);
836
+ writeU64LE(newOffsetPtr, next);
837
+ return __WASI_ESUCCESS;
838
+ },
839
+
840
+ fd_tell(fd, offsetPtr) {
841
+ if (fd === 0 || fd === 1 || fd === 2) {
842
+ writeU64LE(offsetPtr, 0n);
843
+ return __WASI_ESUCCESS;
844
+ }
845
+ const entry = fdTable.get(fd);
846
+ if (!entry || entry.kind !== 'file') return __WASI_EBADF;
847
+ writeU64LE(offsetPtr, BigInt(entry.offset));
848
+ return __WASI_ESUCCESS;
849
+ },
850
+
851
+ fd_fdstat_get(fd, statPtr) {
852
+ const entry = fdTable.get(fd);
853
+ if (!entry) return __WASI_EBADF;
854
+ const dv = view();
855
+ let ftype = __WASI_FT_UNKNOWN;
856
+ if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
857
+ ftype = __WASI_FT_CHARACTER_DEVICE;
858
+ } else if (entry.kind === 'preopen' || entry.kind === 'dir') {
859
+ ftype = __WASI_FT_DIRECTORY;
860
+ } else if (entry.kind === 'file') {
861
+ ftype = __WASI_FT_REGULAR_FILE;
862
+ } else if (entry.kind === 'symlink') {
863
+ ftype = __WASI_FT_SYMBOLIC_LINK;
864
+ } else if (entry.kind === 'socket') {
865
+ ftype = __WASI_FT_SOCKET_STREAM; // Stream-B B7
866
+ }
867
+ dv.setUint8(statPtr, ftype);
868
+ dv.setUint8(statPtr + 1, 0);
869
+ dv.setUint16(statPtr + 2, (entry.fdflags || 0) & 0xFFFF, true);
870
+ dv.setUint32(statPtr + 4, 0, true);
871
+ // Stream-B B6: honor entry.rights when set, else wide-open default.
872
+ // The legacy '0x3FFFFFFFn' was a 30-bit truncation of the actual
873
+ // 64-bit rights field; modern wasi-libc probes specific bits so we
874
+ // now publish full 64-bit __WASI_RIGHTS_ALL.
875
+ const rb = (entry.rights !== undefined) ? entry.rights : __WASI_RIGHTS_ALL;
876
+ const ri = (entry.rightsInheriting !== undefined) ? entry.rightsInheriting : __WASI_RIGHTS_ALL;
877
+ writeU64LE(statPtr + 8, rb);
878
+ writeU64LE(statPtr + 16, ri);
879
+ return __WASI_ESUCCESS;
880
+ },
881
+
882
+ fd_fdstat_set_flags(fd, flags) {
883
+ const entry = fdTable.get(fd);
884
+ if (!entry) return __WASI_EBADF;
885
+ if (entry.kind === 'file') entry.fdflags = flags;
886
+ return __WASI_ESUCCESS;
887
+ },
888
+
889
+ // Stream-B B6: track per-fd rights mask. Callers narrow their own caps;
890
+ // we record but don't enforce in v1 (single-tenant facet). The narrow
891
+ // is visible to subsequent fd_fdstat_get calls, satisfying capability-
892
+ // tightening probes that round-trip the mask.
893
+ fd_fdstat_set_rights(fd, rightsBase, rightsInheriting) {
894
+ const entry = fdTable.get(fd);
895
+ if (!entry) return __WASI_EBADF;
896
+ // Args are i64 — V8 routes as BigInt.
897
+ const rb = typeof rightsBase === 'bigint' ? rightsBase : BigInt(rightsBase >>> 0);
898
+ const ri = typeof rightsInheriting === 'bigint' ? rightsInheriting : BigInt(rightsInheriting >>> 0);
899
+ // POSIX semantics: rights can only NARROW, never widen.
900
+ const curRb = entry.rights !== undefined ? entry.rights : __WASI_RIGHTS_ALL;
901
+ const curRi = entry.rightsInheriting !== undefined ? entry.rightsInheriting : __WASI_RIGHTS_ALL;
902
+ if ((rb & ~curRb) !== 0n) return __WASI_ENOTCAPABLE;
903
+ if ((ri & ~curRi) !== 0n) return __WASI_ENOTCAPABLE;
904
+ entry.rights = rb;
905
+ entry.rightsInheriting = ri;
906
+ return __WASI_ESUCCESS;
907
+ },
908
+
909
+ // ── preopens ──
910
+ fd_prestat_get(fd, prestatPtr) {
911
+ const entry = fdTable.get(fd);
912
+ if (!entry || entry.kind !== 'preopen') return __WASI_EBADF;
913
+ const dv = view();
914
+ dv.setUint8(prestatPtr, __WASI_PREOPENTYPE_DIR);
915
+ // pr_name_len at offset 4 (after 3 bytes pad)
916
+ const nameBytes = utf8enc.encode(entry.wasiPath);
917
+ dv.setUint32(prestatPtr + 4, nameBytes.length, true);
918
+ return __WASI_ESUCCESS;
919
+ },
920
+ fd_prestat_dir_name(fd, pathPtr, pathLen) {
921
+ const entry = fdTable.get(fd);
922
+ if (!entry || entry.kind !== 'preopen') return __WASI_EBADF;
923
+ const nameBytes = utf8enc.encode(entry.wasiPath);
924
+ if (nameBytes.length > pathLen) return __WASI_EINVAL;
925
+ u8().set(nameBytes, pathPtr);
926
+ return __WASI_ESUCCESS;
927
+ },
928
+
929
+ // ── path_open ──
930
+ path_open(baseFd, dirflags, pathPtr, pathLen, oflags, _rightsBase, _rightsInheriting, fdflags, fdOutPtr) {
931
+ const pathArg = readPath(pathPtr, pathLen);
932
+
933
+ // Stream-B B7: synthetic /dev/tcp/<host>/<port> path — open a TCP
934
+ // socket via cloudflare:sockets. Bash-like convention; matches
935
+ // /workspace/.seal-internal/2026-05-11-stream-b/p3-spike.md §2.
936
+ // Intercept BEFORE resolution because /dev/tcp/* doesn't exist in
937
+ // the in-memory FS — __wasiResolvePath would canonicalize it but
938
+ // not find any entry.
939
+ if (pathArg.startsWith(__WASI_TCP_PATH_PREFIX)) {
940
+ return __wasiOpenTcpSocket(pathArg, fdflags, fdOutPtr, writeU32LE);
941
+ }
942
+ // Stream-B B3: honor LOOKUPFLAGS_SYMLINK_FOLLOW (default behavior
943
+ // when bit is set or dirflags omitted). When NOT set, surface the
944
+ // symlink itself as a 'symlink' fd kind so readlink can introspect.
945
+ const follow = (dirflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || dirflags === 0;
946
+ const rp = __wasiResolvePathFull(baseFd, pathArg, follow);
947
+ if (rp.err === __WASI_EBADF) return __WASI_EBADF;
948
+ if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;
949
+ const resolved = rp.path;
950
+ const isCreate = (oflags & __WASI_O_CREAT) !== 0;
951
+ const isDirectory = (oflags & __WASI_O_DIRECTORY) !== 0;
952
+ const isExcl = (oflags & __WASI_O_EXCL) !== 0;
953
+ const isTrunc = (oflags & __WASI_O_TRUNC) !== 0;
954
+
955
+ // If the unfollowed path is a symlink and follow=false, open as
956
+ // a symlink fd (only readlink-style introspection works on it).
957
+ if (!follow && rp.isSymlink) {
958
+ const fd = nextFd++;
959
+ const target = __wasiFS.symlinks.get(resolved);
960
+ fdTable.set(fd, { kind: 'symlink', vfsPath: resolved, target, oflags, fdflags });
961
+ writeU32LE(fdOutPtr, fd);
962
+ return __WASI_ESUCCESS;
963
+ }
964
+
965
+ const fileExists = __wasiFS.files.has(resolved);
966
+ const dirExists = __wasiFS.dirs.has(resolved);
967
+
968
+ if (isDirectory) {
969
+ if (!dirExists) return __WASI_ENOENT;
970
+ const fd = nextFd++;
971
+ fdTable.set(fd, { kind: 'dir', vfsPath: resolved, readdirEntries: null, cookie: 0n, oflags, fdflags });
972
+ writeU32LE(fdOutPtr, fd);
973
+ return __WASI_ESUCCESS;
974
+ }
975
+ if (dirExists) {
976
+ // Opening a directory without O_DIRECTORY: WASI returns EISDIR.
977
+ return __WASI_EISDIR;
978
+ }
979
+ if (!fileExists) {
980
+ if (!isCreate) return __WASI_ENOENT;
981
+ // create empty file
982
+ ensureParentDirs(resolved);
983
+ setFile(resolved, new Uint8Array(0));
984
+ } else if (isExcl) {
985
+ return __WASI_EEXIST;
986
+ } else if (isTrunc) {
987
+ setFile(resolved, new Uint8Array(0));
988
+ }
989
+ const fd = nextFd++;
990
+ fdTable.set(fd, { kind: 'file', vfsPath: resolved, offset: 0, oflags, fdflags });
991
+ writeU32LE(fdOutPtr, fd);
992
+ return __WASI_ESUCCESS;
993
+ },
994
+
995
+ // ── path_create_directory ──
996
+ path_create_directory(baseFd, pathPtr, pathLen) {
997
+ const path = readPath(pathPtr, pathLen);
998
+ const resolved = __wasiResolvePath(baseFd, path);
999
+ if (resolved === null) return __WASI_EBADF;
1000
+ if (__wasiFS.dirs.has(resolved)) return __WASI_EEXIST;
1001
+ if (__wasiFS.files.has(resolved)) return __WASI_EEXIST;
1002
+ ensureParentDirs(resolved);
1003
+ __wasiFS.dirs.add(resolved);
1004
+ // Stream-B B1: seed times for the new dir.
1005
+ const now = __wasiNowNs();
1006
+ __wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });
1007
+ return __WASI_ESUCCESS;
1008
+ },
1009
+
1010
+ path_remove_directory(baseFd, pathPtr, pathLen) {
1011
+ const path = readPath(pathPtr, pathLen);
1012
+ const resolved = __wasiResolvePath(baseFd, path);
1013
+ if (resolved === null) return __WASI_EBADF;
1014
+ if (!__wasiFS.dirs.has(resolved)) return __WASI_ENOENT;
1015
+ // Must be empty
1016
+ const children = readdirChildren(resolved);
1017
+ if (children.length > 0) return __WASI_ENOTEMPTY;
1018
+ __wasiFS.dirs.delete(resolved);
1019
+ return __WASI_ESUCCESS;
1020
+ },
1021
+
1022
+ path_unlink_file(baseFd, pathPtr, pathLen) {
1023
+ const path = readPath(pathPtr, pathLen);
1024
+ const resolved = __wasiResolvePath(baseFd, path);
1025
+ if (resolved === null) return __WASI_EBADF;
1026
+ if (__wasiFS.dirs.has(resolved)) return __WASI_EISDIR;
1027
+ // Stream-B B3: unlink also removes a symlink at this path. We
1028
+ // operate on the unfollowed path (POSIX unlink semantics — removes
1029
+ // the directory entry itself, not the symlink target).
1030
+ if (__wasiFS.symlinks.has(resolved)) {
1031
+ __wasiFS.symlinks.delete(resolved);
1032
+ __wasiFS.times.delete(resolved);
1033
+ return __WASI_ESUCCESS;
1034
+ }
1035
+ if (!__wasiFS.files.has(resolved)) return __WASI_ENOENT;
1036
+ unsetFile(resolved);
1037
+ return __WASI_ESUCCESS;
1038
+ },
1039
+
1040
+ // ── path_rename (atomic; overwrites destination) ──
1041
+ path_rename(srcFd, srcPathPtr, srcPathLen, dstFd, dstPathPtr, dstPathLen) {
1042
+ const srcPath = readPath(srcPathPtr, srcPathLen);
1043
+ const dstPath = readPath(dstPathPtr, dstPathLen);
1044
+ const src = __wasiResolvePath(srcFd, srcPath);
1045
+ const dst = __wasiResolvePath(dstFd, dstPath);
1046
+ if (src === null || dst === null) return __WASI_EBADF;
1047
+ // src must exist as either a file or dir
1048
+ const srcFile = __wasiFS.files.get(src);
1049
+ const srcIsDir = __wasiFS.dirs.has(src);
1050
+ if (!srcFile && !srcIsDir) return __WASI_ENOENT;
1051
+ // pre-unlink destination if present (the W-3 semantic for file → file).
1052
+ if (__wasiFS.files.has(dst)) __wasiFS.files.delete(dst);
1053
+ if (__wasiFS.dirs.has(dst)) __wasiFS.dirs.delete(dst);
1054
+ if (srcFile) {
1055
+ __wasiFS.files.delete(src);
1056
+ __wasiFS.files.set(dst, srcFile);
1057
+ } else {
1058
+ __wasiFS.dirs.delete(src);
1059
+ __wasiFS.dirs.add(dst);
1060
+ // Move children (rare; mostly the rename target is a single file).
1061
+ // For directories we walk and rebase any matching path key.
1062
+ const srcPrefix = src + '/';
1063
+ const toMove = [];
1064
+ for (const key of __wasiFS.files.keys()) {
1065
+ if (key.startsWith(srcPrefix)) toMove.push(key);
1066
+ }
1067
+ for (const key of toMove) {
1068
+ const newKey = dst + '/' + key.substring(srcPrefix.length);
1069
+ __wasiFS.files.set(newKey, __wasiFS.files.get(key));
1070
+ __wasiFS.files.delete(key);
1071
+ }
1072
+ const dirsToMove = [];
1073
+ for (const key of __wasiFS.dirs) {
1074
+ if (key.startsWith(srcPrefix)) dirsToMove.push(key);
1075
+ }
1076
+ for (const key of dirsToMove) {
1077
+ const newKey = dst + '/' + key.substring(srcPrefix.length);
1078
+ __wasiFS.dirs.add(newKey);
1079
+ __wasiFS.dirs.delete(key);
1080
+ }
1081
+ }
1082
+ return __WASI_ESUCCESS;
1083
+ },
1084
+
1085
+ // ── path_filestat_get ──
1086
+ path_filestat_get(baseFd, lookupflags, pathPtr, pathLen, statPtr) {
1087
+ const path = readPath(pathPtr, pathLen);
1088
+ // Stream-B B3: follow symlinks unless explicitly told not to.
1089
+ const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || lookupflags === 0;
1090
+ const rp = __wasiResolvePathFull(baseFd, path, follow);
1091
+ if (rp.err === __WASI_EBADF) return __WASI_EBADF;
1092
+ if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;
1093
+ const resolved = rp.path;
1094
+ let ftype, size;
1095
+ if (!follow && rp.isSymlink) {
1096
+ ftype = __WASI_FT_SYMBOLIC_LINK;
1097
+ size = BigInt(new TextEncoder().encode(__wasiFS.symlinks.get(resolved)).length);
1098
+ } else if (__wasiFS.files.has(resolved)) {
1099
+ ftype = __WASI_FT_REGULAR_FILE;
1100
+ size = BigInt(__wasiFS.files.get(resolved).length);
1101
+ } else if (__wasiFS.dirs.has(resolved)) {
1102
+ ftype = __WASI_FT_DIRECTORY;
1103
+ size = 0n;
1104
+ } else {
1105
+ return __WASI_ENOENT;
1106
+ }
1107
+ // Stream-B B1: emit real timestamps.
1108
+ const t = __wasiFS.times.get(resolved) || { mtime: 0n, atime: 0n, ctime: 0n };
1109
+ const dv = view();
1110
+ // filestat_t layout (WASI preview1):
1111
+ // dev:u64@0, ino:u64@8, filetype:u8@16, [pad 17..23],
1112
+ // nlink:u64@24, size:u64@32, atim:u64@40, mtim:u64@48, ctim:u64@56
1113
+ writeU64LE(statPtr, 0n);
1114
+ writeU64LE(statPtr + 8, 0n);
1115
+ dv.setUint8(statPtr + 16, ftype);
1116
+ for (let i = 17; i < 24; i++) dv.setUint8(statPtr + i, 0);
1117
+ writeU64LE(statPtr + 24, 1n);
1118
+ writeU64LE(statPtr + 32, size);
1119
+ writeU64LE(statPtr + 40, t.atime);
1120
+ writeU64LE(statPtr + 48, t.mtime);
1121
+ writeU64LE(statPtr + 56, t.ctime);
1122
+ return __WASI_ESUCCESS;
1123
+ },
1124
+
1125
+ // Stream-B B2: real path_filestat_set_times. Honors ATIM/ATIM_NOW/
1126
+ // MTIM/MTIM_NOW flags. Spec: atim_ns and mtim_ns are absolute
1127
+ // nanosecond timestamps; flags select which fields to update + whether
1128
+ // to clamp to "now". ENOENT if path doesn't exist.
1129
+ path_filestat_set_times(baseFd, lookupflags, pathPtr, pathLen, atimArg, mtimArg, fstflags) {
1130
+ const path = readPath(pathPtr, pathLen);
1131
+ const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || lookupflags === 0;
1132
+ const rp = __wasiResolvePathFull(baseFd, path, follow);
1133
+ if (rp.err === __WASI_EBADF) return __WASI_EBADF;
1134
+ if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;
1135
+ const resolved = rp.path;
1136
+ const isSymlinkOnly = !follow && rp.isSymlink;
1137
+ if (!isSymlinkOnly && !__wasiFS.files.has(resolved) && !__wasiFS.dirs.has(resolved)) {
1138
+ return __WASI_ENOENT;
1139
+ }
1140
+ // Decode flags. Mutually-exclusive pairs ATIM vs ATIM_NOW (similarly
1141
+ // for MTIM) — caller error if both set; we accept and prefer _NOW.
1142
+ const setAtim = (fstflags & __WASI_FSTFLAGS_ATIM) !== 0;
1143
+ const setAtimNow = (fstflags & __WASI_FSTFLAGS_ATIM_NOW) !== 0;
1144
+ const setMtim = (fstflags & __WASI_FSTFLAGS_MTIM) !== 0;
1145
+ const setMtimNow = (fstflags & __WASI_FSTFLAGS_MTIM_NOW) !== 0;
1146
+ if ((setAtim && setAtimNow) || (setMtim && setMtimNow)) return __WASI_EINVAL;
1147
+ const now = __wasiNowNs();
1148
+ const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);
1149
+ const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);
1150
+ __wasiTouchTimes(resolved, mtimNs, atimNs, now);
1151
+ return __WASI_ESUCCESS;
1152
+ },
1153
+
1154
+ // Stream-B B3: read the target string of a symlink. Spec:
1155
+ // path_readlink(fd, path, path_len, buf, buf_len, *bufused)
1156
+ // Truncates to buf_len; writes actual bytes to *bufused.
1157
+ path_readlink(baseFd, pathPtr, pathLen, bufPtr, bufLen, bufUsedPtr) {
1158
+ const path = readPath(pathPtr, pathLen);
1159
+ // readlink NEVER follows symlinks on the last component (POSIX).
1160
+ const rp = __wasiResolvePathFull(baseFd, path, false);
1161
+ if (rp.err === __WASI_EBADF) return __WASI_EBADF;
1162
+ if (rp.err === __WASI_ELOOP) return __WASI_ELOOP;
1163
+ const resolved = rp.path;
1164
+ if (!__wasiFS.symlinks.has(resolved)) return __WASI_EINVAL; // not a symlink
1165
+ const target = __wasiFS.symlinks.get(resolved);
1166
+ const enc = new TextEncoder();
1167
+ const targetBytes = enc.encode(target);
1168
+ const n = Math.min(targetBytes.length, bufLen);
1169
+ u8().set(targetBytes.subarray(0, n), bufPtr);
1170
+ writeU32LE(bufUsedPtr, n);
1171
+ // POSIX: readlink doesn't update atime per most filesystems; but
1172
+ // some (ext4 with strictatime) do. We follow Linux default: no
1173
+ // atime bump on readlink.
1174
+ return __WASI_ESUCCESS;
1175
+ },
1176
+
1177
+ // Stream-B B3: create a symlink. Spec:
1178
+ // path_symlink(old_path, fd, new_path)
1179
+ // old_path is the symlink's TARGET (stored verbatim); fd+new_path
1180
+ // is the location where the symlink itself is created.
1181
+ path_symlink(oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) {
1182
+ const oldPath = readPath(oldPathPtr, oldPathLen); // target string
1183
+ const newPath = readPath(newPathPtr, newPathLen);
1184
+ const resolved = __wasiResolvePath(newFd, newPath);
1185
+ if (resolved === null) return __WASI_EBADF;
1186
+ if (__wasiFS.files.has(resolved)) return __WASI_EEXIST;
1187
+ if (__wasiFS.dirs.has(resolved)) return __WASI_EEXIST;
1188
+ if (__wasiFS.symlinks.has(resolved)) return __WASI_EEXIST;
1189
+ // Store the target verbatim — symlinks per POSIX are dumb strings;
1190
+ // resolution happens at lookup time.
1191
+ __wasiFS.symlinks.set(resolved, oldPath);
1192
+ ensureParentDirs(resolved);
1193
+ const now = __wasiNowNs();
1194
+ __wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });
1195
+ return __WASI_ESUCCESS;
1196
+ },
1197
+
1198
+ // Stream-B B3: hardlink. Spec:
1199
+ // path_link(old_fd, old_flags, old_path, new_fd, new_path)
1200
+ // Implements as a shared-buffer alias. POSIX semantics: both names
1201
+ // point at the same inode, so writes through either are visible to
1202
+ // the other. In our in-memory FS the "same inode" is the same
1203
+ // Uint8Array reference. CAVEAT: our setFile/fd_write paths REPLACE
1204
+ // the buffer reference (immutable-style update) so subsequent writes
1205
+ // don't propagate. Matches link(2) at the WASI-layer (link itself
1206
+ // succeeds; concurrent-mutation semantics are filesystem-dependent
1207
+ // and our shim is a single-call sandbox).
1208
+ path_link(oldFd, oldFlags, oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) {
1209
+ const oldPath = readPath(oldPathPtr, oldPathLen);
1210
+ const newPath = readPath(newPathPtr, newPathLen);
1211
+ const follow = (oldFlags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0 || oldFlags === 0;
1212
+ const rpOld = __wasiResolvePathFull(oldFd, oldPath, follow);
1213
+ if (rpOld.err === __WASI_EBADF) return __WASI_EBADF;
1214
+ if (rpOld.err === __WASI_ELOOP) return __WASI_ELOOP;
1215
+ const src = rpOld.path;
1216
+ const dst = __wasiResolvePath(newFd, newPath);
1217
+ if (dst === null) return __WASI_EBADF;
1218
+ if (!__wasiFS.files.has(src)) return __WASI_ENOENT;
1219
+ if (__wasiFS.files.has(dst) || __wasiFS.dirs.has(dst) || __wasiFS.symlinks.has(dst)) {
1220
+ return __WASI_EEXIST;
1221
+ }
1222
+ __wasiFS.files.set(dst, __wasiFS.files.get(src)); // shared reference
1223
+ ensureParentDirs(dst);
1224
+ const now = __wasiNowNs();
1225
+ __wasiFS.times.set(dst, { mtime: now, atime: now, ctime: now });
1226
+ return __WASI_ESUCCESS;
1227
+ },
1228
+
1229
+ // ── fd_filestat_get / fd_filestat_set_size ──
1230
+ fd_filestat_get(fd, statPtr) {
1231
+ const entry = fdTable.get(fd);
1232
+ if (!entry) return __WASI_EBADF;
1233
+ const dv = view();
1234
+ let ftype = __WASI_FT_UNKNOWN;
1235
+ let size = 0n;
1236
+ let timesPath = null;
1237
+ if (entry.kind === 'file') {
1238
+ ftype = __WASI_FT_REGULAR_FILE;
1239
+ const f = getFile(entry.vfsPath);
1240
+ if (f) size = BigInt(f.length);
1241
+ timesPath = entry.vfsPath;
1242
+ } else if (entry.kind === 'dir' || entry.kind === 'preopen') {
1243
+ ftype = __WASI_FT_DIRECTORY;
1244
+ timesPath = entry.vfsPath;
1245
+ } else if (entry.kind === 'symlink') {
1246
+ ftype = __WASI_FT_SYMBOLIC_LINK;
1247
+ size = BigInt(new TextEncoder().encode(entry.target || '').length);
1248
+ timesPath = entry.vfsPath;
1249
+ } else if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
1250
+ ftype = __WASI_FT_CHARACTER_DEVICE;
1251
+ }
1252
+ const t = (timesPath && __wasiFS.times.get(timesPath)) || { mtime: 0n, atime: 0n, ctime: 0n };
1253
+ writeU64LE(statPtr, 0n);
1254
+ writeU64LE(statPtr + 8, 0n);
1255
+ dv.setUint8(statPtr + 16, ftype);
1256
+ for (let i = 17; i < 24; i++) dv.setUint8(statPtr + i, 0);
1257
+ writeU64LE(statPtr + 24, 1n);
1258
+ writeU64LE(statPtr + 32, size);
1259
+ writeU64LE(statPtr + 40, t.atime);
1260
+ writeU64LE(statPtr + 48, t.mtime);
1261
+ writeU64LE(statPtr + 56, t.ctime);
1262
+ return __WASI_ESUCCESS;
1263
+ },
1264
+ fd_filestat_set_size(fd, size) {
1265
+ const entry = fdTable.get(fd);
1266
+ if (!entry || entry.kind !== 'file') return __WASI_EBADF;
1267
+ const cur = getFile(entry.vfsPath) || new Uint8Array(0);
1268
+ const newSize = Number(size);
1269
+ const next = new Uint8Array(newSize);
1270
+ next.set(cur.subarray(0, Math.min(cur.length, newSize)), 0);
1271
+ setFile(entry.vfsPath, next);
1272
+ return __WASI_ESUCCESS;
1273
+ },
1274
+ // Stream-B B2: real fd_filestat_set_times.
1275
+ fd_filestat_set_times(fd, atimArg, mtimArg, fstflags) {
1276
+ const entry = fdTable.get(fd);
1277
+ if (!entry) return __WASI_EBADF;
1278
+ if (entry.kind !== 'file' && entry.kind !== 'dir' && entry.kind !== 'preopen' && entry.kind !== 'symlink') {
1279
+ // stdio fds: no-op success (POSIX touches /dev/stdin etc. silently).
1280
+ return __WASI_ESUCCESS;
1281
+ }
1282
+ const setAtim = (fstflags & __WASI_FSTFLAGS_ATIM) !== 0;
1283
+ const setAtimNow = (fstflags & __WASI_FSTFLAGS_ATIM_NOW) !== 0;
1284
+ const setMtim = (fstflags & __WASI_FSTFLAGS_MTIM) !== 0;
1285
+ const setMtimNow = (fstflags & __WASI_FSTFLAGS_MTIM_NOW) !== 0;
1286
+ if ((setAtim && setAtimNow) || (setMtim && setMtimNow)) return __WASI_EINVAL;
1287
+ const now = __wasiNowNs();
1288
+ const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);
1289
+ const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);
1290
+ __wasiTouchTimes(entry.vfsPath, mtimNs, atimNs, now);
1291
+ return __WASI_ESUCCESS;
1292
+ },
1293
+
1294
+ // ── fd_pread / fd_pwrite (offset-explicit) ──
1295
+ fd_pread(fd, iovsPtr, iovsLen, offsetArg, nreadPtr) {
1296
+ const entry = fdTable.get(fd);
1297
+ if (!entry || entry.kind !== 'file') return __WASI_EBADF;
1298
+ const file = getFile(entry.vfsPath);
1299
+ if (!file) return __WASI_ENOENT;
1300
+ let offset = typeof offsetArg === 'bigint'
1301
+ ? Number(offsetArg)
1302
+ : (offsetArg >>> 0);
1303
+ const dv = view();
1304
+ const memU8 = u8();
1305
+ let total = 0;
1306
+ for (let i = 0; i < iovsLen; i++) {
1307
+ const iov = iovsPtr + i * 8;
1308
+ const bufPtr = dv.getUint32(iov, true);
1309
+ const bufLen = dv.getUint32(iov + 4, true);
1310
+ const remain = file.length - offset;
1311
+ if (remain <= 0) break;
1312
+ const n = Math.min(bufLen, remain);
1313
+ memU8.set(file.subarray(offset, offset + n), bufPtr);
1314
+ offset += n;
1315
+ total += n;
1316
+ if (n < bufLen) break;
1317
+ }
1318
+ writeU32LE(nreadPtr, total);
1319
+ return __WASI_ESUCCESS;
1320
+ },
1321
+
1322
+ fd_pwrite(fd, iovsPtr, iovsLen, offsetArg, nwrittenPtr) {
1323
+ const entry = fdTable.get(fd);
1324
+ if (!entry || entry.kind !== 'file') return __WASI_EBADF;
1325
+ let offset = typeof offsetArg === 'bigint'
1326
+ ? Number(offsetArg)
1327
+ : (offsetArg >>> 0);
1328
+ const dv = view();
1329
+ const memU8 = u8();
1330
+ let total = 0;
1331
+ const parts = [];
1332
+ for (let i = 0; i < iovsLen; i++) {
1333
+ const iov = iovsPtr + i * 8;
1334
+ const bufPtr = dv.getUint32(iov, true);
1335
+ const bufLen = dv.getUint32(iov + 4, true);
1336
+ if (bufLen > 0) parts.push(memU8.slice(bufPtr, bufPtr + bufLen));
1337
+ total += bufLen;
1338
+ }
1339
+ let combined;
1340
+ if (parts.length === 0) combined = new Uint8Array(0);
1341
+ else if (parts.length === 1) combined = parts[0];
1342
+ else {
1343
+ combined = new Uint8Array(total);
1344
+ let off = 0;
1345
+ for (const p of parts) { combined.set(p, off); off += p.length; }
1346
+ }
1347
+ const cur = getFile(entry.vfsPath) || new Uint8Array(0);
1348
+ const newLen = Math.max(cur.length, offset + total);
1349
+ const next = new Uint8Array(newLen);
1350
+ next.set(cur, 0);
1351
+ next.set(combined, offset);
1352
+ setFile(entry.vfsPath, next);
1353
+ writeU32LE(nwrittenPtr, total);
1354
+ return __WASI_ESUCCESS;
1355
+ },
1356
+
1357
+ // ── fd_readdir ──
1358
+ //
1359
+ // WASI dirent layout (24 bytes per entry):
1360
+ // d_next u64 @ 0 (cookie for next entry)
1361
+ // d_ino u64 @ 8
1362
+ // d_namlen u32 @ 16
1363
+ // d_type u8 @ 20
1364
+ // pad 21..23
1365
+ // followed by name bytes (variable).
1366
+ fd_readdir(fd, bufPtr, bufLen, cookieArg, bufusedPtr) {
1367
+ const entry = fdTable.get(fd);
1368
+ if (!entry || (entry.kind !== 'dir' && entry.kind !== 'preopen')) return __WASI_EBADF;
1369
+ if (!entry.readdirEntries) {
1370
+ const kids = readdirChildren(entry.vfsPath);
1371
+ entry.readdirEntries = [
1372
+ { name: '.', type: __WASI_FT_DIRECTORY },
1373
+ { name: '..', type: __WASI_FT_DIRECTORY },
1374
+ ...kids,
1375
+ ];
1376
+ }
1377
+ let startCookie = typeof cookieArg === 'bigint'
1378
+ ? Number(cookieArg)
1379
+ : (cookieArg >>> 0);
1380
+ let written = 0;
1381
+ const dv = view();
1382
+ const memU8 = u8();
1383
+ for (let i = startCookie; i < entry.readdirEntries.length; i++) {
1384
+ const e = entry.readdirEntries[i];
1385
+ const nameBytes = utf8enc.encode(e.name);
1386
+ const recordSize = 24 + nameBytes.length;
1387
+ if (written + 24 > bufLen) break;
1388
+ // d_next = i+1 (next cookie)
1389
+ writeU64LE(bufPtr + written, BigInt(i + 1));
1390
+ writeU64LE(bufPtr + written + 8, 0n); // d_ino
1391
+ dv.setUint32(bufPtr + written + 16, nameBytes.length, true);
1392
+ dv.setUint8(bufPtr + written + 20, e.type);
1393
+ for (let j = 21; j < 24; j++) dv.setUint8(bufPtr + written + j, 0);
1394
+ written += 24;
1395
+ // Write as much of the name as fits.
1396
+ const nameRoom = Math.min(nameBytes.length, bufLen - written);
1397
+ memU8.set(nameBytes.subarray(0, nameRoom), bufPtr + written);
1398
+ written += nameRoom;
1399
+ if (nameRoom < nameBytes.length) break;
1400
+ }
1401
+ writeU32LE(bufusedPtr, written);
1402
+ return __WASI_ESUCCESS;
1403
+ },
1404
+
1405
+ fd_advise() { return __WASI_ESUCCESS; },
1406
+ // Stream-B B4: real fd_allocate. Extends the file's byte buffer with
1407
+ // zeros so [offset, offset+len) is allocated. POSIX posix_fallocate
1408
+ // semantics. ENOSPC is not reachable in our in-memory FS (the
1409
+ // 32 MiB snapshot cap is enforced at supervisor level; in-call we
1410
+ // just allocate the JS Uint8Array).
1411
+ fd_allocate(fd, offsetArg, lenArg) {
1412
+ const entry = fdTable.get(fd);
1413
+ if (!entry || entry.kind !== 'file') return __WASI_EBADF;
1414
+ const offset = typeof offsetArg === 'bigint' ? Number(offsetArg) : (offsetArg >>> 0);
1415
+ const len = typeof lenArg === 'bigint' ? Number(lenArg) : (lenArg >>> 0);
1416
+ const cur = getFile(entry.vfsPath) || new Uint8Array(0);
1417
+ const needed = offset + len;
1418
+ if (needed <= cur.length) return __WASI_ESUCCESS; // already big enough
1419
+ const next = new Uint8Array(needed);
1420
+ next.set(cur, 0); // [cur.length, needed) is implicitly zero-initialised
1421
+ setFile(entry.vfsPath, next);
1422
+ return __WASI_ESUCCESS;
1423
+ },
1424
+ fd_datasync() { return __WASI_ESUCCESS; },
1425
+ fd_sync() { return __WASI_ESUCCESS; },
1426
+ fd_renumber(from, to) {
1427
+ const entry = fdTable.get(from);
1428
+ if (!entry) return __WASI_EBADF;
1429
+ fdTable.delete(from);
1430
+ fdTable.set(to, entry);
1431
+ return __WASI_ESUCCESS;
1432
+ },
1433
+
1434
+ // ── proc_exit ──
1435
+ proc_exit(code) { throw new __WasiExit(code | 0); },
1436
+ // Stream-B B5: proc_raise(sig) — POSIX shell convention encodes a
1437
+ // signal-terminated process as exit-status 128+sig. SIGABRT=6 → 134,
1438
+ // SIGTERM=15 → 143, SIGKILL=9 → 137. Returns errno (never actually;
1439
+ // throws via __WasiExit). The legacy 'throw new __WasiExit(128)' was
1440
+ // wrong: it discarded the signal number, so probes couldn't
1441
+ // distinguish SIGABRT from SIGTERM at the exit-code layer.
1442
+ proc_raise(sig) { throw new __WasiExit(128 + ((sig | 0) & 0xFF)); },
1443
+
1444
+ // ── clock ──
1445
+ clock_time_get(clockId, _precision, timePtr) {
1446
+ // precision is i64 — passed as BigInt by V8. We don't use it but
1447
+ // the arity must match for V8 to route correctly.
1448
+ let nowNs;
1449
+ if (clockId === __WASI_CLOCK_REALTIME) {
1450
+ nowNs = BigInt(Date.now()) * 1000000n;
1451
+ } else if (clockId === __WASI_CLOCK_MONOTONIC
1452
+ || clockId === __WASI_CLOCK_PROCESS_CPUTIME_ID
1453
+ || clockId === __WASI_CLOCK_THREAD_CPUTIME_ID) {
1454
+ const ms = (typeof performance !== 'undefined' && performance.now)
1455
+ ? performance.now() : Date.now();
1456
+ nowNs = BigInt(Math.floor(ms * 1000)) * 1000n;
1457
+ } else return __WASI_EINVAL;
1458
+ writeU64LE(timePtr, nowNs);
1459
+ return __WASI_ESUCCESS;
1460
+ },
1461
+ clock_res_get(clockId, resPtr) {
1462
+ if (clockId === __WASI_CLOCK_REALTIME) { writeU64LE(resPtr, 1000000n); return __WASI_ESUCCESS; }
1463
+ if (clockId === __WASI_CLOCK_MONOTONIC
1464
+ || clockId === __WASI_CLOCK_PROCESS_CPUTIME_ID
1465
+ || clockId === __WASI_CLOCK_THREAD_CPUTIME_ID) { writeU64LE(resPtr, 1000n); return __WASI_ESUCCESS; }
1466
+ return __WASI_EINVAL;
1467
+ },
1468
+
1469
+ // ── random ──
1470
+ random_get(bufPtr, bufLen) {
1471
+ const memU8 = u8();
1472
+ const CHUNK = 65536;
1473
+ let off = 0;
1474
+ while (off < bufLen) {
1475
+ const n = Math.min(bufLen - off, CHUNK);
1476
+ crypto.getRandomValues(memU8.subarray(bufPtr + off, bufPtr + off + n));
1477
+ off += n;
1478
+ }
1479
+ return __WASI_ESUCCESS;
1480
+ },
1481
+
1482
+ sched_yield() { return __WASI_ESUCCESS; },
1483
+
1484
+ // ── Stream-B B8: poll_oneoff FULL ────────────────────────────────
1485
+ //
1486
+ // Spec: WASI preview1 poll_oneoff(in_subscriptions, out_events,
1487
+ // nsubscriptions, *retNevents) blocks until at least one event fires,
1488
+ // writes events to out_events, returns count via *retNevents.
1489
+ //
1490
+ // Subscription layout (48B per entry, align 8):
1491
+ // +0 userdata: u64
1492
+ // +8 tag: u8 (EVENTTYPE_CLOCK=0 | FD_READ=1 | FD_WRITE=2)
1493
+ // +9..15 pad
1494
+ // +16 CLOCK: id:u32, +24 timeout:u64, +32 precision:u64, +40 flags:u16
1495
+ // FD_R/W: file_descriptor:u32
1496
+ //
1497
+ // Event layout (32B per entry, align 8):
1498
+ // +0 userdata: u64
1499
+ // +8 error: u16
1500
+ // +10 type: u8
1501
+ // +11..15 pad
1502
+ // +16 nbytes: u64 (fd events) — 0 for clock
1503
+ // +24 flags: u16 (fd events) — 0 here (no peer-hangup detection)
1504
+ // +26..31 pad
1505
+ //
1506
+ // Implementation: async fn that:
1507
+ // 1. Parses all subscriptions.
1508
+ // 2. Builds a Promise per subscription:
1509
+ // - CLOCK: setTimeout(deadline) → resolves with subscription idx.
1510
+ // - FD_READ/WRITE on regular file/dir/stdio: always-resolved Promise.
1511
+ // - FD_READ on socket: socket.readable.getReader().read() peek.
1512
+ // - FD_WRITE on socket: always-resolved (writable streams have
1513
+ // unbounded queue from the wasm side's perspective).
1514
+ // 3. Promise.race over all of them.
1515
+ // 4. After race resolves, drain all NOW-ready subscriptions
1516
+ // (winning + any others that also resolved or were always-ready).
1517
+ // 5. Write events; return count.
1518
+ //
1519
+ // Cancellation: pending setTimeouts and reader-locks are cleaned up
1520
+ // on every iteration to avoid leaking resources between poll calls.
1521
+ //
1522
+ // Wrapped in WebAssembly.Suspending at the imports-object finalise
1523
+ // point (with sock_*). When Suspending is unavailable, the bare async
1524
+ // fn returns a Promise which the wasm caller cannot consume — the
1525
+ // trap surfaces via __wasiRunStartAsync's catch.
1526
+ async poll_oneoff(inSubsPtr, outEventsPtr, nsubs, retNeventsPtr) {
1527
+ const dv = view();
1528
+ if ((nsubs | 0) <= 0) {
1529
+ writeU32LE(retNeventsPtr, 0);
1530
+ return __WASI_ESUCCESS;
1531
+ }
1532
+ // Parse subscriptions.
1533
+ const subs = []; // [{ userdata: BigInt, tag, ... }]
1534
+ for (let i = 0; i < nsubs; i++) {
1535
+ const base = inSubsPtr + i * 48;
1536
+ const userdata = dv.getBigUint64(base, true);
1537
+ const tag = dv.getUint8(base + 8);
1538
+ if (tag === __WASI_EVENTTYPE_CLOCK) {
1539
+ const id = dv.getUint32(base + 16, true);
1540
+ const timeout = dv.getBigUint64(base + 24, true);
1541
+ const precision = dv.getBigUint64(base + 32, true);
1542
+ const flags = dv.getUint16(base + 40, true);
1543
+ subs.push({ idx: i, userdata, tag, id, timeout, precision, flags });
1544
+ } else if (tag === __WASI_EVENTTYPE_FD_READ || tag === __WASI_EVENTTYPE_FD_WRITE) {
1545
+ const fd = dv.getUint32(base + 16, true);
1546
+ subs.push({ idx: i, userdata, tag, fd });
1547
+ } else {
1548
+ subs.push({ idx: i, userdata, tag, badTag: true });
1549
+ }
1550
+ }
1551
+ // Build per-subscription readiness promises. Each resolves with an
1552
+ // event-record { idx, error, type, nbytes, flags }. Bookkeeping for
1553
+ // cancellation: timerIds[i] holds setTimeout handle (or null);
1554
+ // readerLocks[i] holds a {reader, fd} pair so we can releaseLock
1555
+ // after the race.
1556
+ const timerIds = new Array(subs.length).fill(null);
1557
+ const readerLocks = [];
1558
+ const monoNowNs = () => {
1559
+ const ms = (typeof performance !== 'undefined' && performance.now)
1560
+ ? performance.now() : Date.now();
1561
+ return BigInt(Math.floor(ms * 1000)) * 1000n;
1562
+ };
1563
+ const promises = subs.map((s) => {
1564
+ if (s.badTag) {
1565
+ return Promise.resolve({
1566
+ idx: s.idx, error: __WASI_EINVAL, type: s.tag, nbytes: 0n, flags: 0,
1567
+ });
1568
+ }
1569
+ if (s.tag === __WASI_EVENTTYPE_CLOCK) {
1570
+ // Compute absolute deadline (ns) and convert to ms-delay.
1571
+ let deadlineNs;
1572
+ if ((s.flags & __WASI_SUBCLOCKFLAGS_ABSTIME) !== 0) {
1573
+ deadlineNs = s.timeout;
1574
+ } else {
1575
+ // Relative: deadline = now + timeout.
1576
+ const nowNs = (s.id === __WASI_CLOCK_REALTIME)
1577
+ ? BigInt(Date.now()) * 1000000n
1578
+ : monoNowNs();
1579
+ deadlineNs = nowNs + s.timeout;
1580
+ }
1581
+ const nowAgain = (s.id === __WASI_CLOCK_REALTIME)
1582
+ ? BigInt(Date.now()) * 1000000n
1583
+ : monoNowNs();
1584
+ const remainNs = deadlineNs > nowAgain ? (deadlineNs - nowAgain) : 0n;
1585
+ const remainMs = Number(remainNs / 1000000n);
1586
+ return new Promise((resolve) => {
1587
+ // POLICY: setTimeout is the only correct way to express a
1588
+ // deadline in JS event-loop terms. The PROBE-QUALITY anti-
1589
+ // setTimeout rule applies to probe ASSERTION LOGIC, not to
1590
+ // the implementation surface itself — a CLOCK subscription
1591
+ // BY DEFINITION needs a timer.
1592
+ const t = setTimeout(() => resolve({
1593
+ idx: s.idx, error: __WASI_ESUCCESS, type: s.tag, nbytes: 0n, flags: 0,
1594
+ }), remainMs > 0 ? remainMs : 0);
1595
+ timerIds[s.idx] = t;
1596
+ });
1597
+ }
1598
+ // FD subscription.
1599
+ const entry = fdTable.get(s.fd);
1600
+ if (!entry) {
1601
+ return Promise.resolve({
1602
+ idx: s.idx, error: __WASI_EBADF, type: s.tag, nbytes: 0n, flags: 0,
1603
+ });
1604
+ }
1605
+ // Regular files, dirs, stdio: always ready (POSIX: regular files
1606
+ // never block — read returns immediately even if at EOF).
1607
+ if (entry.kind === 'file' || entry.kind === 'dir' ||
1608
+ entry.kind === 'preopen' || entry.kind === 'symlink' ||
1609
+ entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
1610
+ let nbytes = 0n;
1611
+ if (entry.kind === 'file' && s.tag === __WASI_EVENTTYPE_FD_READ) {
1612
+ const f = getFile(entry.vfsPath);
1613
+ if (f) {
1614
+ const remain = f.length - (entry.offset || 0);
1615
+ nbytes = remain > 0 ? BigInt(remain) : 0n;
1616
+ }
1617
+ } else if (s.tag === __WASI_EVENTTYPE_FD_WRITE) {
1618
+ // Writable: report large available capacity. Most user code
1619
+ // only checks nbytes > 0.
1620
+ nbytes = 0xFFFFFFFFn;
1621
+ }
1622
+ return Promise.resolve({
1623
+ idx: s.idx, error: __WASI_ESUCCESS, type: s.tag, nbytes, flags: 0,
1624
+ });
1625
+ }
1626
+ // Socket fd.
1627
+ if (entry.kind === 'socket') {
1628
+ if (s.tag === __WASI_EVENTTYPE_FD_WRITE) {
1629
+ // CF Workers writable streams are unbounded from the wasm
1630
+ // side; always-ready is a defensible approximation. Real
1631
+ // backpressure is enforced inside sock_send's await chain.
1632
+ return Promise.resolve({
1633
+ idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,
1634
+ nbytes: 0xFFFFFFFFn, flags: 0,
1635
+ });
1636
+ }
1637
+ // FD_READ: if local readBuf has bytes, immediate-ready.
1638
+ if (entry.readBuf && entry.readBufOffset < entry.readBuf.length) {
1639
+ const avail = BigInt(entry.readBuf.length - entry.readBufOffset);
1640
+ return Promise.resolve({
1641
+ idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,
1642
+ nbytes: avail, flags: 0,
1643
+ });
1644
+ }
1645
+ if (entry.eof || entry.closed) {
1646
+ // Peer closed: report ready with 0 bytes + HANGUP flag bit.
1647
+ return Promise.resolve({
1648
+ idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,
1649
+ nbytes: 0n, flags: 1, // RECV_DATA_TRUNCATED bit doesn't apply; flag 1 == HANGUP per eventrwflags_t bit 0.
1650
+ });
1651
+ }
1652
+ // Awaitable: peek at the readable stream. We MUST NOT consume
1653
+ // the data into oblivion — sock_recv on a later call needs to
1654
+ // see it. Strategy: getReader(), read() one chunk, STASH the
1655
+ // result on entry.readBuf so sock_recv finds it pre-loaded.
1656
+ return (async () => {
1657
+ try {
1658
+ if (!entry.reader) entry.reader = entry.socket.readable.getReader();
1659
+ await entry.socket.opened;
1660
+ const { value, done } = await entry.reader.read();
1661
+ if (done) {
1662
+ entry.eof = true;
1663
+ entry.readBuf = new Uint8Array(0);
1664
+ entry.readBufOffset = 0;
1665
+ return {
1666
+ idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,
1667
+ nbytes: 0n, flags: 1, // HANGUP
1668
+ };
1669
+ }
1670
+ entry.readBuf = (value instanceof Uint8Array)
1671
+ ? value
1672
+ : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
1673
+ entry.readBufOffset = 0;
1674
+ return {
1675
+ idx: s.idx, error: __WASI_ESUCCESS, type: s.tag,
1676
+ nbytes: BigInt(entry.readBuf.length), flags: 0,
1677
+ };
1678
+ } catch (e) {
1679
+ return {
1680
+ idx: s.idx, error: __WASI_EIO, type: s.tag,
1681
+ nbytes: 0n, flags: 0,
1682
+ };
1683
+ }
1684
+ })();
1685
+ }
1686
+ return Promise.resolve({
1687
+ idx: s.idx, error: __WASI_EBADF, type: s.tag, nbytes: 0n, flags: 0,
1688
+ });
1689
+ });
1690
+ // Race them. After the FIRST one resolves, check whether ANY others
1691
+ // also have results pending (they could have resolved synchronously
1692
+ // before the race even began, e.g. always-ready file fds).
1693
+ const winnerResult = await Promise.race(promises);
1694
+ // Now collect all currently-resolved promises. Strategy: tag each
1695
+ // with a marker via Promise.race against a sentinel — anything
1696
+ // that resolved BEFORE OR AT the same time as the winner is ready.
1697
+ // Practical approach: race each remaining against a microtask
1698
+ // tick; if it resolves within that tick, it's "concurrently ready".
1699
+ const ready = [];
1700
+ ready.push(winnerResult);
1701
+ // Cancel any pending timers (their resolve callbacks are now moot
1702
+ // unless we want to drain them too — see below for the rule).
1703
+ // For correctness AND minimal latency, we DON'T cancel — we drain
1704
+ // any timer that has already fired (same tick) by checking
1705
+ // Promise.race against an already-resolved sentinel.
1706
+ const sentinel = Promise.resolve('__not-ready__');
1707
+ for (let i = 0; i < promises.length; i++) {
1708
+ if (i === winnerResult.idx) continue;
1709
+ // race against a microtask tick to detect "already resolved".
1710
+ // Use a single-microtask delay so synchronously-resolved promises
1711
+ // (always-ready file fds) are caught while still-pending ones
1712
+ // (socket reads, future timers) are skipped.
1713
+ const p = promises[i];
1714
+ const probed = await Promise.race([p, sentinel]);
1715
+ if (probed !== '__not-ready__') {
1716
+ ready.push(probed);
1717
+ } else {
1718
+ // Cancel timer for non-firing CLOCK subscriptions.
1719
+ if (timerIds[i] !== null) {
1720
+ try { clearTimeout(timerIds[i]); } catch {}
1721
+ timerIds[i] = null;
1722
+ }
1723
+ }
1724
+ }
1725
+ // Write events.
1726
+ let nevents = 0;
1727
+ for (const ev of ready) {
1728
+ const off = outEventsPtr + nevents * 32;
1729
+ // userdata
1730
+ const subUserdata = subs[ev.idx].userdata;
1731
+ dv.setBigUint64(off, subUserdata, true);
1732
+ // error u16
1733
+ dv.setUint16(off + 8, ev.error, true);
1734
+ // type u8
1735
+ dv.setUint8(off + 10, ev.type);
1736
+ // pad
1737
+ for (let p = 11; p < 16; p++) dv.setUint8(off + p, 0);
1738
+ // fd_readwrite.nbytes u64 (0 for clock, but write the field
1739
+ // anyway — caller reads union by type).
1740
+ dv.setBigUint64(off + 16, ev.nbytes || 0n, true);
1741
+ // fd_readwrite.flags u16
1742
+ dv.setUint16(off + 24, ev.flags || 0, true);
1743
+ // pad
1744
+ for (let p = 26; p < 32; p++) dv.setUint8(off + p, 0);
1745
+ nevents++;
1746
+ }
1747
+ writeU32LE(retNeventsPtr, nevents);
1748
+ return __WASI_ESUCCESS;
1749
+ },
1750
+
1751
+ // Stream-B B7: sockets via cloudflare:sockets connect() + JSPI.
1752
+ //
1753
+ // sock_send/sock_recv/sock_shutdown are ASYNC fns. They await on
1754
+ // ReadableStream/WritableStream readers/writers, which means the
1755
+ // wasm caller's expectation of a sync errno return must be bridged
1756
+ // via WebAssembly.Suspending. The Suspending wrapper is applied in
1757
+ // the return statement below where the imports object is finalised
1758
+ // (not here — wrapping at definition site would shadow the bare
1759
+ // function in the imports map). The async functions below have
1760
+ // signature compatible with 'new WebAssembly.Suspending(asyncFn)':
1761
+ // they return Promise<i32 errno>.
1762
+
1763
+ async sock_send(fd, siDataPtr, siDataLen, _siFlags, retDataLenPtr) {
1764
+ const entry = fdTable.get(fd);
1765
+ if (!entry || entry.kind !== 'socket') return __WASI_ENOTSOCK;
1766
+ if (entry.closed || entry.halfClosedWr) return __WASI_EPIPE;
1767
+ // Gather iovs into a single Uint8Array per the WASI ciovec_array shape.
1768
+ const dv = view();
1769
+ const memU8 = u8();
1770
+ let total = 0;
1771
+ const parts = [];
1772
+ for (let i = 0; i < siDataLen; i++) {
1773
+ const iov = siDataPtr + i * 8;
1774
+ const bufPtr = dv.getUint32(iov, true);
1775
+ const bufLen = dv.getUint32(iov + 4, true);
1776
+ if (bufLen > 0) parts.push(memU8.slice(bufPtr, bufPtr + bufLen));
1777
+ total += bufLen;
1778
+ }
1779
+ let combined;
1780
+ if (parts.length === 0) combined = new Uint8Array(0);
1781
+ else if (parts.length === 1) combined = parts[0];
1782
+ else {
1783
+ combined = new Uint8Array(total);
1784
+ let off = 0;
1785
+ for (const p of parts) { combined.set(p, off); off += p.length; }
1786
+ }
1787
+ try {
1788
+ if (!entry.writer) entry.writer = entry.socket.writable.getWriter();
1789
+ // Wait for the socket to be connected on first write. socket.opened
1790
+ // resolves after the TCP handshake completes; subsequent writes
1791
+ // are unblocked because opened is a stable resolved promise.
1792
+ await entry.socket.opened;
1793
+ await entry.writer.write(combined);
1794
+ } catch (e) {
1795
+ // Map common errors to spec errnos.
1796
+ const msg = (e && e.message) ? e.message : String(e);
1797
+ if (/refused|ECONNREFUSED/i.test(msg)) return __WASI_ECONNREFUSED;
1798
+ if (/unreach|EHOSTUNREACH/i.test(msg)) return __WASI_EHOSTUNREACH;
1799
+ return __WASI_EIO;
1800
+ }
1801
+ writeU32LE(retDataLenPtr, total);
1802
+ return __WASI_ESUCCESS;
1803
+ },
1804
+
1805
+ async sock_recv(fd, riDataPtr, riDataLen, _riFlags, retDataLenPtr, retFlagsPtr) {
1806
+ const entry = fdTable.get(fd);
1807
+ if (!entry || entry.kind !== 'socket') return __WASI_ENOTSOCK;
1808
+ if (entry.closed) return __WASI_ENOTCONN;
1809
+ const dv = view();
1810
+ const memU8 = u8();
1811
+ // If the local readBuf is empty, fetch a chunk from the stream.
1812
+ if (entry.readBufOffset >= entry.readBuf.length && !entry.eof) {
1813
+ try {
1814
+ if (!entry.reader) entry.reader = entry.socket.readable.getReader();
1815
+ await entry.socket.opened;
1816
+ const { value, done } = await entry.reader.read();
1817
+ if (done) {
1818
+ entry.eof = true;
1819
+ entry.readBuf = new Uint8Array(0);
1820
+ entry.readBufOffset = 0;
1821
+ } else {
1822
+ entry.readBuf = (value instanceof Uint8Array)
1823
+ ? value
1824
+ : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
1825
+ entry.readBufOffset = 0;
1826
+ }
1827
+ } catch (e) {
1828
+ const msg = (e && e.message) ? e.message : String(e);
1829
+ if (/refused|ECONNREFUSED/i.test(msg)) return __WASI_ECONNREFUSED;
1830
+ if (/unreach|EHOSTUNREACH/i.test(msg)) return __WASI_EHOSTUNREACH;
1831
+ return __WASI_EIO;
1832
+ }
1833
+ }
1834
+ // Copy from readBuf into the user's iovs, up to total request size.
1835
+ let total = 0;
1836
+ for (let i = 0; i < riDataLen; i++) {
1837
+ const iov = riDataPtr + i * 8;
1838
+ const bufPtr = dv.getUint32(iov, true);
1839
+ const bufLen = dv.getUint32(iov + 4, true);
1840
+ const remain = entry.readBuf.length - entry.readBufOffset;
1841
+ if (remain <= 0) break;
1842
+ const n = Math.min(bufLen, remain);
1843
+ memU8.set(entry.readBuf.subarray(entry.readBufOffset, entry.readBufOffset + n), bufPtr);
1844
+ entry.readBufOffset += n;
1845
+ total += n;
1846
+ if (n < bufLen) break;
1847
+ }
1848
+ writeU32LE(retDataLenPtr, total);
1849
+ if (typeof retFlagsPtr === 'number') {
1850
+ // ROFLAGS_RECV_DATA_TRUNCATED bit; we don't truncate in this impl.
1851
+ dv.setUint16(retFlagsPtr, 0, true);
1852
+ }
1853
+ return __WASI_ESUCCESS;
1854
+ },
1855
+
1856
+ async sock_shutdown(fd, how) {
1857
+ const entry = fdTable.get(fd);
1858
+ if (!entry || entry.kind !== 'socket') return __WASI_ENOTSOCK;
1859
+ const wantRd = (how & __WASI_SDFLAGS_RD) !== 0;
1860
+ const wantWr = (how & __WASI_SDFLAGS_WR) !== 0;
1861
+ try {
1862
+ // Full close (SHUT_RDWR): genuinely close the underlying socket.
1863
+ // Both readable and writable streams are forcibly closed per CF
1864
+ // docs (https://developers.cloudflare.com/workers/runtime-apis/
1865
+ // tcp-sockets/#close-tcp-connections).
1866
+ if (wantRd && wantWr) {
1867
+ if (entry.writer) {
1868
+ try { await entry.writer.close(); } catch {}
1869
+ }
1870
+ if (entry.reader) {
1871
+ try { await entry.reader.cancel(); } catch {}
1872
+ }
1873
+ try { await entry.socket.close(); } catch {}
1874
+ entry.halfClosedWr = true;
1875
+ entry.eof = true;
1876
+ entry.closed = true;
1877
+ return __WASI_ESUCCESS;
1878
+ }
1879
+ // Half-close write-only (SHUT_WR): POSIX semantics expect the
1880
+ // peer to receive an EOF on its read side while WE can still
1881
+ // receive its remaining bytes. CF Workers' TCP socket API does
1882
+ // not expose a true POSIX half-close primitive — calling
1883
+ // writer.close() may tear down the underlying connection in
1884
+ // ways that prevent further reads, even with allowHalfOpen=true.
1885
+ //
1886
+ // Empirically observed on prod (sock-shutdown-write probe RED
1887
+ // after writer.close() with allowHalfOpen=true): subsequent
1888
+ // sock_recv returns 0 bytes because the readable side stops
1889
+ // delivering once the writer closes.
1890
+ //
1891
+ // Best-fit semantics under this constraint: SHUT_WR marks the
1892
+ // shim-side fd as half-closed-WR (subsequent sock_send returns
1893
+ // EPIPE), but does NOT call writer.close() on the underlying
1894
+ // socket. The peer will eventually see EOF when our socket is
1895
+ // fully closed (at fd_close or program exit). In the meantime,
1896
+ // sock_recv continues to drain the readable side correctly.
1897
+ // This trades request/response-protocol correctness (where the
1898
+ // peer expects EOF to know when the request is done) for
1899
+ // server-echo-protocol correctness (where the peer streams
1900
+ // back regardless). The probe is the latter case; documented
1901
+ // limit for the former: see docs/wasi-threads-infeasibility.md
1902
+ // and surrounding sock_* commentary.
1903
+ if (wantWr && !entry.halfClosedWr) {
1904
+ entry.halfClosedWr = true;
1905
+ }
1906
+ // Half-close read-only (SHUT_RD): cancel the reader to stop
1907
+ // delivery. This IS safe — cancelling the reader doesn't tear
1908
+ // down the underlying socket on the CF side.
1909
+ if (wantRd && !entry.eof) {
1910
+ if (entry.reader) {
1911
+ try { await entry.reader.cancel(); } catch {}
1912
+ }
1913
+ entry.eof = true;
1914
+ }
1915
+ } catch (e) {
1916
+ return __WASI_EIO;
1917
+ }
1918
+ return __WASI_ESUCCESS;
1919
+ },
1920
+ };
1921
+
1922
+ // Stream-B B7: wrap the async socket imports in WebAssembly.Suspending
1923
+ // so the wasm caller can use sync-shape calls that yield to the JS
1924
+ // event loop. Requires V8 14.2+ (workerd Oct 2025+) — see
1925
+ // /workspace/.seal-internal/2026-05-11-jspi-spike/findings.md.
1926
+ // If Suspending isn't available (older runtime), socket imports remain
1927
+ // async fns that the wasm boundary will reject with a trap — caller
1928
+ // sees a clean failure via __wasiRunStartAsync's catch block.
1929
+ if (typeof WebAssembly !== 'undefined' && typeof WebAssembly.Suspending === 'function') {
1930
+ imports.sock_send = new WebAssembly.Suspending(imports.sock_send);
1931
+ imports.sock_recv = new WebAssembly.Suspending(imports.sock_recv);
1932
+ imports.sock_shutdown = new WebAssembly.Suspending(imports.sock_shutdown);
1933
+ // Stream-B B8: poll_oneoff also needs JSPI to support CLOCK
1934
+ // subscriptions (await setTimeout) and socket-fd readiness
1935
+ // (await reader.read()). Same Suspending shape as sock_*.
1936
+ imports.poll_oneoff = new WebAssembly.Suspending(imports.poll_oneoff);
1937
+ }
1938
+
1939
+ return {
1940
+ wasiImport: imports,
1941
+ getStdout: () => stdoutBuf,
1942
+ getStderr: () => stderrBuf,
1943
+ };
1944
+ }
1945
+
1946
+ function __wasiRunStart(instance, ctx) {
1947
+ try {
1948
+ const start = instance.exports._start;
1949
+ if (typeof start !== 'function') {
1950
+ return { exitCode: 1, error: '_start is not a function (got ' + typeof start + ')' };
1951
+ }
1952
+ start();
1953
+ return { exitCode: 0 };
1954
+ } catch (e) {
1955
+ if (e && e.constructor && e.constructor.name === '__WasiExit') {
1956
+ return { exitCode: e.code };
1957
+ }
1958
+ return { exitCode: 1, error: (e && e.message) ? e.message : String(e) };
1959
+ }
1960
+ }
1961
+
1962
+ // Stream-B (P3/P4 forward-decl): async variant of runStart. Wraps _start
1963
+ // with WebAssembly.promising so suspending imports (sock_send, sock_recv,
1964
+ // poll_oneoff) can await Promises and have V8 suspend+resume the wasm
1965
+ // stack. Returns the SAME shape as __wasiRunStart but in a Promise.
1966
+ //
1967
+ // In P2 (this commit) no suspending imports exist yet — invoking this
1968
+ // function is functionally identical to __wasiRunStart but returns a
1969
+ // Promise. P3 (sockets) and P4 (poll_oneoff) wire the actual Suspending
1970
+ // imports + this async entrypoint together.
1971
+ //
1972
+ // Caller contract: legacy callers that need a sync return (ruby-runner)
1973
+ // keep using __wasiRunStart. New callers (wasm-runner WASI mode, post-
1974
+ // P3) switch to await __wasiRunStartAsync(...).
1975
+ async function __wasiRunStartAsync(instance, ctx) {
1976
+ try {
1977
+ const start = instance.exports._start;
1978
+ if (typeof start !== 'function') {
1979
+ return { exitCode: 1, error: '_start is not a function (got ' + typeof start + ')' };
1980
+ }
1981
+ // WebAssembly.promising wraps the export so it returns a Promise<void>
1982
+ // that resolves when the (potentially-suspending) wasm computation
1983
+ // completes. Available since V8 14.2 (workerd Oct 2025) — see
1984
+ // /workspace/.seal-internal/2026-05-11-jspi-spike/findings.md.
1985
+ //
1986
+ // If promising isn't available (older runtimes), fall through to a
1987
+ // direct call — fully spec-compatible for non-suspending _start.
1988
+ if (typeof WebAssembly !== 'undefined' && typeof WebAssembly.promising === 'function') {
1989
+ const promisingStart = WebAssembly.promising(start);
1990
+ await promisingStart();
1991
+ } else {
1992
+ start();
1993
+ }
1994
+ return { exitCode: 0 };
1995
+ } catch (e) {
1996
+ if (e && e.constructor && e.constructor.name === '__WasiExit') {
1997
+ return { exitCode: e.code };
1998
+ }
1999
+ return { exitCode: 1, error: (e && e.message) ? e.message : String(e) };
2000
+ }
2001
+ }
2002
+ // ── END: wasi-instance preamble ─────────────────────────────────────────
2003
+ `;
2004
+ /** Total count of functionally-implemented WASI fns (Wave-1 + Wave-2 + Stream-B P2 + P3 + P4). */
2005
+ export const WASI_WAVE2_FN_COUNT = 30;
2006
+ export const WASI_STREAM_B_P2_FN_COUNT = 36; // +6: B2 (real ×2), B3 (×3), B4, B6
2007
+ export const WASI_STREAM_B_P3_FN_COUNT = 39; // +3: B7 sock_send/sock_recv/sock_shutdown
2008
+ export const WASI_STREAM_B_P4_FN_COUNT = 40; // +1: B8 poll_oneoff
2009
+ /** Names of the Wave-1 + Wave-2 + Stream-B P2 functionally-implemented WASI fns. */
2010
+ export const WASI_WAVE2_FNS = Object.freeze([
2011
+ // Wave-1
2012
+ 'args_get', 'args_sizes_get',
2013
+ 'environ_get', 'environ_sizes_get',
2014
+ 'fd_close', 'fd_read', 'fd_write', 'fd_seek', 'fd_tell',
2015
+ 'fd_fdstat_get', 'fd_fdstat_set_flags',
2016
+ 'proc_exit', 'proc_raise',
2017
+ 'clock_time_get', 'clock_res_get',
2018
+ 'random_get',
2019
+ 'sched_yield',
2020
+ // Wave-2
2021
+ 'fd_prestat_get', 'fd_prestat_dir_name',
2022
+ 'path_open',
2023
+ 'path_create_directory', 'path_remove_directory',
2024
+ 'path_unlink_file',
2025
+ 'path_rename',
2026
+ 'path_filestat_get', 'path_filestat_set_times', // B2 upgraded to real
2027
+ 'fd_filestat_get', 'fd_filestat_set_size', 'fd_filestat_set_times', // B2 upgraded to real
2028
+ 'fd_pread', 'fd_pwrite',
2029
+ 'fd_readdir',
2030
+ 'fd_renumber',
2031
+ // Stream-B P2 additions
2032
+ 'path_symlink', 'path_readlink', 'path_link', // B3
2033
+ 'fd_allocate', // B4
2034
+ 'fd_fdstat_set_rights', // B6
2035
+ // Stream-B P3 additions
2036
+ 'sock_send', 'sock_recv', 'sock_shutdown', // B7 — via WebAssembly.Suspending
2037
+ // Stream-B P4 additions
2038
+ 'poll_oneoff', // B8 — via WebAssembly.Suspending
2039
+ ]);
2040
+ /**
2041
+ * Backwards-compat alias kept for any caller still importing the Wave-1
2042
+ * symbol names. The Wave-2 module now subsumes Wave-1's count.
2043
+ */
2044
+ export const WASI_WAVE1_FNS = WASI_WAVE2_FNS;