sygnal 7.0.0 → 7.2.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/CHANGELOG.md +632 -0
  2. package/dist/agent-ui.cjs.js +592 -0
  3. package/dist/agent-ui.cjs.js.map +1 -0
  4. package/dist/agent-ui.esm.js +582 -0
  5. package/dist/agent-ui.esm.js.map +1 -0
  6. package/dist/api.cjs.js +63 -0
  7. package/dist/api.cjs.js.map +1 -0
  8. package/dist/api.esm.js +2 -0
  9. package/dist/api.esm.js.map +1 -0
  10. package/dist/astro/index.cjs.js +968 -4
  11. package/dist/astro/index.cjs.js.map +1 -1
  12. package/dist/astro/index.mjs +968 -4
  13. package/dist/astro/index.mjs.map +1 -1
  14. package/dist/astro/server.cjs.js +73 -45
  15. package/dist/astro/server.cjs.js.map +1 -1
  16. package/dist/astro/server.mjs +73 -45
  17. package/dist/astro/server.mjs.map +1 -1
  18. package/dist/canvas.cjs.js +2919 -0
  19. package/dist/canvas.cjs.js.map +1 -0
  20. package/dist/canvas.esm.js +2866 -0
  21. package/dist/canvas.esm.js.map +1 -0
  22. package/dist/devtools.cjs.js +38 -6
  23. package/dist/devtools.cjs.js.map +1 -1
  24. package/dist/devtools.esm.js +38 -6
  25. package/dist/devtools.esm.js.map +1 -1
  26. package/dist/diagnostics.cjs.js +224 -20
  27. package/dist/diagnostics.cjs.js.map +1 -1
  28. package/dist/diagnostics.esm.js +224 -20
  29. package/dist/diagnostics.esm.js.map +1 -1
  30. package/dist/guide/accessibility.md +1 -1
  31. package/dist/guide/ai-compose.md +150 -0
  32. package/dist/guide/behaviors.md +2 -1
  33. package/dist/guide/collaborative-state.md +265 -0
  34. package/dist/guide/element-commands.md +36 -2
  35. package/dist/guide/forms-reference.md +5 -5
  36. package/dist/guide/forms.md +3 -2
  37. package/dist/guide/http.md +28 -1
  38. package/dist/guide/islands.md +240 -0
  39. package/dist/guide/recipes/code-editor.md +1 -1
  40. package/dist/guide/renderers/agent-ui.md +138 -0
  41. package/dist/guide/renderers/custom-targets.md +126 -0
  42. package/dist/guide/renderers/overview.md +120 -0
  43. package/dist/guide/renderers/react-native.md +128 -0
  44. package/dist/guide/renderers/terminal.md +306 -0
  45. package/dist/guide/renderers/three.md +332 -0
  46. package/dist/guide/renderers/write-once.md +137 -0
  47. package/dist/guide/resources.md +2 -0
  48. package/dist/guide/sandboxed-components.md +223 -0
  49. package/dist/guide/timers.md +32 -5
  50. package/dist/guide/typed-apis.md +442 -0
  51. package/dist/guide/ui/accordion.md +11 -0
  52. package/dist/guide/ui/combobox.md +112 -8
  53. package/dist/guide/ui/dialog.md +96 -20
  54. package/dist/guide/ui/menu.md +57 -5
  55. package/dist/guide/ui/overview.md +8 -4
  56. package/dist/guide/ui/popover.md +69 -9
  57. package/dist/guide/ui/select.md +54 -4
  58. package/dist/guide/ui/tabs.md +1 -1
  59. package/dist/guide/ui/tooltip.md +7 -5
  60. package/dist/guide/virtual-collections.md +3 -0
  61. package/dist/guide/widgets.md +90 -1
  62. package/dist/host.cjs.js +2181 -0
  63. package/dist/host.cjs.js.map +1 -0
  64. package/dist/host.esm.js +2146 -0
  65. package/dist/host.esm.js.map +1 -0
  66. package/dist/index.cjs.js +3948 -586
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +584 -44
  69. package/dist/index.esm.js +3918 -586
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/primitives.cjs.js +143 -0
  72. package/dist/primitives.cjs.js.map +1 -0
  73. package/dist/primitives.esm.js +133 -0
  74. package/dist/primitives.esm.js.map +1 -0
  75. package/dist/react-native.cjs.js +248 -0
  76. package/dist/react-native.cjs.js.map +1 -0
  77. package/dist/react-native.esm.js +244 -0
  78. package/dist/react-native.esm.js.map +1 -0
  79. package/dist/replay.cjs.js +39 -0
  80. package/dist/replay.cjs.js.map +1 -0
  81. package/dist/replay.esm.js +2 -0
  82. package/dist/replay.esm.js.map +1 -0
  83. package/dist/sandbox-build.cjs.js +140 -0
  84. package/dist/sandbox-build.cjs.js.map +1 -0
  85. package/dist/sandbox-build.esm.js +137 -0
  86. package/dist/sandbox-build.esm.js.map +1 -0
  87. package/dist/sandbox-kernel.esm.js +212 -0
  88. package/dist/sandbox-kernel.esm.js.map +1 -0
  89. package/dist/sandbox-worker.cjs.js +857 -0
  90. package/dist/sandbox-worker.cjs.js.map +1 -0
  91. package/dist/sandbox-worker.esm.js +854 -0
  92. package/dist/sandbox-worker.esm.js.map +1 -0
  93. package/dist/sandbox.cjs.js +1425 -0
  94. package/dist/sandbox.cjs.js.map +1 -0
  95. package/dist/sandbox.esm.js +1406 -0
  96. package/dist/sandbox.esm.js.map +1 -0
  97. package/dist/sygnal.min.js +1 -1
  98. package/dist/sygnal.min.js.map +1 -1
  99. package/dist/terminal.cjs.js +1314 -0
  100. package/dist/terminal.cjs.js.map +1 -0
  101. package/dist/terminal.esm.js +1304 -0
  102. package/dist/terminal.esm.js.map +1 -0
  103. package/dist/three.cjs.js +1111 -0
  104. package/dist/three.cjs.js.map +1 -0
  105. package/dist/three.esm.js +1079 -0
  106. package/dist/three.esm.js.map +1 -0
  107. package/dist/ui-combobox.cjs.js +256 -18
  108. package/dist/ui-combobox.cjs.js.map +1 -1
  109. package/dist/ui-combobox.esm.js +256 -18
  110. package/dist/ui-combobox.esm.js.map +1 -1
  111. package/dist/ui-menu.cjs.js +150 -16
  112. package/dist/ui-menu.cjs.js.map +1 -1
  113. package/dist/ui-menu.esm.js +151 -17
  114. package/dist/ui-menu.esm.js.map +1 -1
  115. package/dist/ui-select.cjs.js +95 -9
  116. package/dist/ui-select.cjs.js.map +1 -1
  117. package/dist/ui-select.esm.js +95 -9
  118. package/dist/ui-select.esm.js.map +1 -1
  119. package/dist/ui.cjs.js +679 -173
  120. package/dist/ui.cjs.js.map +1 -1
  121. package/dist/ui.esm.js +679 -175
  122. package/dist/ui.esm.js.map +1 -1
  123. package/dist/vike/config/package.json +1 -1
  124. package/dist/vite/plugin.cjs.js +968 -4
  125. package/dist/vite/plugin.cjs.js.map +1 -1
  126. package/dist/vite/plugin.mjs +968 -4
  127. package/dist/vite/plugin.mjs.map +1 -1
  128. package/dist/worker-page.cjs.js +669 -0
  129. package/dist/worker-page.cjs.js.map +1 -0
  130. package/dist/worker-page.esm.js +665 -0
  131. package/dist/worker-page.esm.js.map +1 -0
  132. package/dist/worker.cjs.js +377 -0
  133. package/dist/worker.cjs.js.map +1 -0
  134. package/dist/worker.esm.js +374 -0
  135. package/dist/worker.esm.js.map +1 -0
  136. package/dist/yjs-testing.cjs.js +98 -0
  137. package/dist/yjs-testing.cjs.js.map +1 -0
  138. package/dist/yjs-testing.esm.js +77 -0
  139. package/dist/yjs-testing.esm.js.map +1 -0
  140. package/dist/yjs.cjs.js +923 -0
  141. package/dist/yjs.cjs.js.map +1 -0
  142. package/dist/yjs.esm.js +900 -0
  143. package/dist/yjs.esm.js.map +1 -0
  144. package/dist/zag.cjs.js +11 -1
  145. package/dist/zag.cjs.js.map +1 -1
  146. package/dist/zag.esm.js +11 -1
  147. package/dist/zag.esm.js.map +1 -1
  148. package/llms/ai.md +65 -0
  149. package/llms/collaboration.md +5 -0
  150. package/llms/graphics.md +10 -0
  151. package/llms/renderers.md +7 -0
  152. package/llms/replay.md +5 -0
  153. package/llms/sandbox.md +5 -0
  154. package/llms/typed-apis.md +5 -0
  155. package/llms/ui.md +10 -0
  156. package/llms-full.txt +444 -0
  157. package/llms.txt +37 -35
  158. package/package.json +139 -3
  159. package/src/agent-ui/mcp.ts +152 -0
  160. package/src/agent-ui/snapshot.ts +212 -0
  161. package/src/agent-ui.d.ts +106 -0
  162. package/src/agent-ui.ts +249 -0
  163. package/src/ai.d.ts +95 -2
  164. package/src/api.d.ts +303 -0
  165. package/src/api.ts +9 -0
  166. package/src/canvas/Canvas.ts +585 -0
  167. package/src/canvas/backend.ts +627 -0
  168. package/src/canvas/geometry.ts +187 -0
  169. package/src/canvas/gesture.ts +23 -0
  170. package/src/canvas/image.ts +197 -0
  171. package/src/canvas/kit.ts +130 -0
  172. package/src/canvas/paint.ts +260 -0
  173. package/src/canvas/pointer.ts +25 -0
  174. package/src/canvas/snap.ts +68 -0
  175. package/src/canvas/svg.ts +99 -0
  176. package/src/canvas.d.ts +373 -0
  177. package/src/canvas.ts +46 -0
  178. package/src/core/actions.ts +14 -3
  179. package/src/core/cell.ts +12 -4
  180. package/src/core/debug.ts +8 -3
  181. package/src/core/hooks.ts +16 -2
  182. package/src/core/hosts/collection.ts +1 -1
  183. package/src/core/hosts/switchable.ts +26 -8
  184. package/src/core/instance.ts +98 -45
  185. package/src/core/markers/portal.ts +4 -0
  186. package/src/core/registry.ts +4 -0
  187. package/src/core/runtime.ts +34 -25
  188. package/src/core/statics.ts +5 -5
  189. package/src/cycle/dom/EventDelegator.ts +37 -33
  190. package/src/cycle/dom/MainDOMSource.ts +16 -14
  191. package/src/cycle/dom/enrichEventStream.ts +13 -9
  192. package/src/cycle/dom/fromEvent.ts +6 -10
  193. package/src/cycle/dom/index.ts +11 -7
  194. package/src/cycle/dom/isolate.ts +13 -6
  195. package/src/cycle/dom/makeDOMDriver.ts +6 -2
  196. package/src/cycle/dom/utils.ts +9 -2
  197. package/src/cycle/run/adapt.ts +4 -12
  198. package/src/devtools.d.ts +9 -0
  199. package/src/devtools.ts +2 -1
  200. package/src/extra/ai/compose/catalog.ts +2 -1
  201. package/src/extra/ai/compose/decisions.ts +1 -2
  202. package/src/extra/ai/compose/generated.ts +234 -0
  203. package/src/extra/ai/compose/index.ts +6 -2
  204. package/src/extra/ai/compose/layout.ts +3 -1
  205. package/src/extra/ai/compose/llm.ts +7 -3
  206. package/src/extra/ai/compose/types.ts +2 -2
  207. package/src/extra/ai/transports/shared.ts +3 -1
  208. package/src/extra/api/contract.ts +485 -0
  209. package/src/extra/api/index.ts +26 -0
  210. package/src/extra/behaviors.ts +3 -3
  211. package/src/extra/controlled.ts +44 -0
  212. package/src/extra/copyAsTest.ts +22 -0
  213. package/src/extra/devtools.ts +5 -3
  214. package/src/extra/devtoolsActions.ts +8 -0
  215. package/src/extra/devtoolsNext.ts +1 -1
  216. package/src/extra/diagnostics/checks/actionLog.ts +8 -3
  217. package/src/extra/diagnostics/checks/behaviors.ts +25 -0
  218. package/src/extra/diagnostics/checks/elementCommands.ts +17 -5
  219. package/src/extra/diagnostics/checks/index.ts +1 -1
  220. package/src/extra/diagnostics/checks/next.ts +78 -4
  221. package/src/extra/diagnostics/checks/props.ts +6 -1
  222. package/src/extra/diagnostics/checks/replies.ts +11 -7
  223. package/src/extra/diagnostics/checks/timers.ts +8 -2
  224. package/src/extra/diagnostics/checks/virtual.ts +11 -0
  225. package/src/extra/diagnostics/codes.ts +51 -1
  226. package/src/extra/elementCommands.ts +20 -4
  227. package/src/extra/fetchDriver.ts +4 -1
  228. package/src/extra/fetchEventSource.ts +6 -2
  229. package/src/extra/field.ts +29 -0
  230. package/src/extra/form.ts +42 -16
  231. package/src/extra/frame.ts +54 -0
  232. package/src/extra/keymap.ts +41 -0
  233. package/src/extra/pager.ts +2 -0
  234. package/src/extra/persist.ts +2 -1
  235. package/src/extra/pointer.ts +262 -0
  236. package/src/extra/processForm.ts +34 -1
  237. package/src/extra/replay/address.ts +49 -0
  238. package/src/extra/replay/entropy.ts +99 -0
  239. package/src/extra/replay/format.ts +263 -0
  240. package/src/extra/replay/index.ts +8 -0
  241. package/src/extra/replay/inert.ts +22 -0
  242. package/src/extra/replay/recorder.ts +247 -0
  243. package/src/extra/replay/replayer.ts +234 -0
  244. package/src/extra/replay/report.ts +126 -0
  245. package/src/extra/replay/schema.ts +52 -0
  246. package/src/extra/replay/version.ts +3 -0
  247. package/src/extra/router.ts +7 -3
  248. package/src/extra/selection.ts +2 -0
  249. package/src/extra/socketDriver.ts +51 -6
  250. package/src/extra/ssr.ts +128 -42
  251. package/src/extra/testing.ts +295 -39
  252. package/src/extra/timers.ts +96 -7
  253. package/src/extra/undo.ts +2 -0
  254. package/src/extra/virtual.ts +25 -2
  255. package/src/extra/widget.ts +106 -17
  256. package/src/host/actions.ts +383 -0
  257. package/src/host/delegation.ts +101 -0
  258. package/src/host/dom.ts +553 -0
  259. package/src/host/driver.ts +185 -0
  260. package/src/host/selector.ts +119 -0
  261. package/src/host/style.ts +51 -0
  262. package/src/host.d.ts +298 -0
  263. package/src/host.ts +46 -0
  264. package/src/index.d.ts +302 -38
  265. package/src/index.ts +15 -3
  266. package/src/pragma/blocks.ts +211 -0
  267. package/src/pragma/index.ts +44 -10
  268. package/src/primitives.d.ts +54 -0
  269. package/src/primitives.ts +136 -0
  270. package/src/react-native.d.ts +57 -0
  271. package/src/react-native.ts +285 -0
  272. package/src/react-peers.d.ts +21 -3
  273. package/src/replay.d.ts +185 -0
  274. package/src/replay.ts +6 -0
  275. package/src/sandbox/build.ts +119 -0
  276. package/src/sandbox/create.ts +187 -0
  277. package/src/sandbox/engine.ts +267 -0
  278. package/src/sandbox/generated.ts +195 -0
  279. package/src/sandbox/kernel.ts +180 -0
  280. package/src/sandbox/mirror.ts +269 -0
  281. package/src/sandbox/protocol.ts +128 -0
  282. package/src/sandbox/quickjs.ts +74 -0
  283. package/src/sandbox/ssr.ts +41 -0
  284. package/src/sandbox/widget.ts +60 -0
  285. package/src/sandbox/worker-entry.ts +59 -0
  286. package/src/sandbox/worker.ts +137 -0
  287. package/src/sandbox-build.d.ts +12 -0
  288. package/src/sandbox-build.ts +6 -0
  289. package/src/sandbox.d.ts +299 -0
  290. package/src/sandbox.ts +25 -0
  291. package/src/shared.ts +12 -0
  292. package/src/terminal/paint.ts +712 -0
  293. package/src/terminal.d.ts +167 -0
  294. package/src/terminal.ts +530 -0
  295. package/src/three/assets.ts +170 -0
  296. package/src/three/host.ts +36 -0
  297. package/src/three/scene.ts +217 -0
  298. package/src/three/target.ts +252 -0
  299. package/src/three.d.ts +221 -0
  300. package/src/three.ts +30 -0
  301. package/src/ui/accordion.ts +21 -7
  302. package/src/ui/dialog.ts +192 -90
  303. package/src/ui/disclosure.ts +2 -0
  304. package/src/ui/dom.ts +30 -0
  305. package/src/ui/layers.ts +132 -0
  306. package/src/ui/popover.ts +120 -21
  307. package/src/ui/shared.ts +98 -12
  308. package/src/ui/stack.ts +18 -0
  309. package/src/ui/tabs.ts +2 -0
  310. package/src/ui/toaster.ts +16 -15
  311. package/src/ui/tooltip.ts +24 -8
  312. package/src/ui/tooltips.ts +26 -14
  313. package/src/ui/zag/combobox.ts +180 -13
  314. package/src/ui/zag/menu.ts +97 -17
  315. package/src/ui/zag/select.ts +15 -4
  316. package/src/ui/zag/shared.ts +60 -8
  317. package/src/ui-combobox.d.ts +16 -1
  318. package/src/ui-menu.d.ts +22 -7
  319. package/src/ui-zag-types.d.ts +13 -5
  320. package/src/ui.d.ts +62 -7
  321. package/src/ui.ts +2 -2
  322. package/src/vite/api.ts +186 -0
  323. package/src/vite/compile.ts +280 -0
  324. package/src/vite/plugin.d.ts +34 -0
  325. package/src/vite/plugin.ts +78 -4
  326. package/src/vite/sandbox.ts +16 -0
  327. package/src/worker/page.ts +396 -0
  328. package/src/worker/protocol.ts +48 -0
  329. package/src/worker/target.ts +282 -0
  330. package/src/worker-page.d.ts +42 -0
  331. package/src/worker-page.ts +12 -0
  332. package/src/worker.d.ts +65 -0
  333. package/src/worker.ts +16 -0
  334. package/src/yjs/driver.ts +253 -0
  335. package/src/yjs/mapping.ts +389 -0
  336. package/src/yjs/shared.ts +138 -0
  337. package/src/yjs/testing.ts +91 -0
  338. package/src/yjs-testing.d.ts +34 -0
  339. package/src/yjs-testing.ts +7 -0
  340. package/src/yjs.d.ts +109 -0
  341. package/src/yjs.ts +18 -0
  342. package/src/zag.d.ts +2 -0
  343. package/src/zag.ts +8 -1
@@ -0,0 +1,857 @@
1
+ 'use strict';
2
+
3
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
4
+ /*
5
+ * sygnal/sandbox: what the page and the guest kernel agree on (research: dev-plans/research/sandbox).
6
+ *
7
+ * The protocol is internal and versioned: a kernel built from another Sygnal version is refused
8
+ * at boot ('protocol'), never half understood. It is the part a native or edge host would
9
+ * reimplement:
10
+ *
11
+ * page → guest (QuickJS entries on the kernel's `__sandbox`):
12
+ * dispatch(id, type, initJson) → 1 when prevented fire(timerId) resolve(fetchId, ok, json)
13
+ * props(json) start(exportName) flush() v (the protocol number)
14
+ * guest → page (host functions on `__host`, each checked):
15
+ * render(json) {v, tree, types, f?}: tree = [node], node = {i, t, a, s?, v?, k?, c} | {i, x} | id
16
+ * (an id alone: an unchanged subtree), types = DOM event types to forward,
17
+ * f = the id the guest moved focus to
18
+ * emit(json) {type, data} log(level, text)
19
+ * timer(id, ms) (ms < 0: setImmediate) clearTimer(id) fetch(id, url, initJson)
20
+ * page ↔ worker (postMessage; `isolation: 'worker'`):
21
+ * → init {v, kernel, source, props, limits, fetch} event {id, type, init} props {p} ping fetched {id, ok, res|error}
22
+ * ← ready {v, wasmMs, bootMs} render {json} emit {e} log {l, x} fetch {id, url, init} pong error {reason, message}
23
+ */
24
+ const PROTOCOL = 1;
25
+ class SandboxError extends Error {
26
+ constructor(reason, message) {
27
+ super(message);
28
+ this.name = 'SandboxError';
29
+ this.reason = reason;
30
+ }
31
+ }
32
+ // README §8: an instance per sandbox capped at 64 MB, heap 32 MB, stack 256 KB, boot 1 s, 100 ms per entry, CPU 50 %
33
+ const DEFAULT_LIMITS = {
34
+ memoryBytes: 32 << 20, // QuickJS's heap (the kernel alone takes ~4 MB)
35
+ // QuickJS's own stack check must fire before the host engine's: at 512 KB a guest recursion
36
+ // overflows V8's native stack first, a host RangeError unwinds the WASM mid-call, and freeing
37
+ // that runtime aborts the whole module (README §5, finding 3). 384 KB and below are caught.
38
+ stackBytes: 256 << 10,
39
+ // the instance's whole linear memory (heap, ArrayBuffers, C stack): setMemoryLimit() doesn't
40
+ // count ArrayBuffer storage (README finding 1), a capped WebAssembly.Memory does
41
+ maxWasmBytes: 64 << 20,
42
+ bootMs: 1000, // kernel + component + first render
43
+ callMs: 100, // one event, timer or fetch reply, with the flush it causes
44
+ cpuShare: 0.5, // guest time per rolling second
45
+ hardMs: 1000, // worker: the watchdog terminates a worker silent this long
46
+ maxTimers: 64,
47
+ minTimerMs: 4,
48
+ maxFetches: 4, // in flight
49
+ maxMessageBytes: 1 << 20,
50
+ maxLogs: 100,
51
+ };
52
+ /** above this, a guest recursion can overflow the host's stack before QuickJS's check (a crash) */
53
+ const MAX_STACK_BYTES = 384 << 10;
54
+ function limitsOf(l) {
55
+ const out = { ...DEFAULT_LIMITS };
56
+ for (const k of Object.keys(l || {})) {
57
+ if (!(k in DEFAULT_LIMITS))
58
+ continue;
59
+ const v = Number(l[k]);
60
+ if (!(v > 0))
61
+ throw new TypeError(`sygnal/sandbox: limits.${k} must be a positive number`);
62
+ out[k] = v;
63
+ }
64
+ if (out.stackBytes > MAX_STACK_BYTES)
65
+ throw new RangeError(`sygnal/sandbox: limits.stackBytes over ${MAX_STACK_BYTES} can crash the WASM module instead of throwing in the guest`);
66
+ return out;
67
+ }
68
+ /** an allowlist entry is a URL prefix: same origin, path under its path */
69
+ function allowed(u, allow) {
70
+ if (u.username || u.password)
71
+ return false;
72
+ return allow.some((a) => {
73
+ if (a instanceof RegExp)
74
+ return a.test(u.href);
75
+ const p = new URL(a);
76
+ return u.origin === p.origin && u.pathname.startsWith(p.pathname);
77
+ });
78
+ }
79
+ /**
80
+ * The request the page makes for a guest (both tiers): the allowlist, the methods, accept /
81
+ * content-type headers only, `credentials: 'omit'` (never the user's cookies), `redirect: 'error'`
82
+ * (a redirect could leave the allowlist), the response size capped. Resolves with a plain
83
+ * {status, headers, body} or rejects with the reason.
84
+ */
85
+ async function capFetch(f, url, init, maxBytes, signal) {
86
+ if (!f)
87
+ throw new Error('no fetch capability');
88
+ let u;
89
+ try {
90
+ u = f.base ? new URL(url, f.base) : new URL(url);
91
+ }
92
+ catch {
93
+ throw new Error(`bad URL ${String(url).slice(0, 80)}`);
94
+ }
95
+ if (!allowed(u, f.allow || []))
96
+ throw new Error(`${u.origin}${u.pathname} is not allowed`);
97
+ const method = String(init?.method || 'GET').toUpperCase();
98
+ if (!(f.methods || ['GET']).map((m) => m.toUpperCase()).includes(method))
99
+ throw new Error(`method ${method} is not allowed`);
100
+ const headers = {};
101
+ for (const [k, v] of Object.entries(init?.headers || {}))
102
+ if (/^(accept|content-type)$/i.test(k))
103
+ headers[k] = String(v);
104
+ const body = typeof init?.body == 'string' && method !== 'GET' && method !== 'HEAD' ? init.body : undefined;
105
+ const r = await (f.impl || globalThis.fetch)(u.href, { method, headers, body, credentials: 'omit', redirect: 'error', signal });
106
+ const text = await r.text();
107
+ if (text.length > (f.maxBodyBytes || maxBytes))
108
+ throw new Error('response too large');
109
+ return { status: r.status, headers: { 'content-type': r.headers.get('content-type') || '' }, body: text };
110
+ }
111
+
112
+ /*
113
+ * The QuickJS WASM module: `quickjs-emscripten-core` + `@jitl/quickjs-wasmfile-release-sync`,
114
+ * optional peer dependencies (D209), loaded on first use. Stock packages only (README §9 Q6):
115
+ * a quickjs-ng variant can be passed as `variant`.
116
+ */
117
+ let compiled = null;
118
+ let shared = null;
119
+ const isNode = () => typeof globalThis.process == 'object' && !!globalThis.process.versions?.node && typeof globalThis.window == 'undefined';
120
+ /**
121
+ * A QuickJS module. By default a fresh instance whose own `WebAssembly.Memory` is capped at
122
+ * `maxWasmBytes` (64 MB): QuickJS's `setMemoryLimit()` doesn't count ArrayBuffer storage, so
123
+ * without the cap a guest can grow the instance to 2 GB (README finding 1). Instances share
124
+ * one compiled `WebAssembly.Module` (compiled once per page).
125
+ */
126
+ async function loadQuickJS(opts = {}) {
127
+ if (opts.shared && !opts.wasm && !opts.variant && shared)
128
+ return shared;
129
+ const p = load(opts);
130
+ if (opts.shared && !opts.wasm && !opts.variant)
131
+ shared = p;
132
+ return p;
133
+ }
134
+ async function load({ maxWasmBytes = 64 << 20, shared: sh, variant, wasm }) {
135
+ let core, base;
136
+ try {
137
+ core = await import('quickjs-emscripten-core');
138
+ base = variant || (await import('@jitl/quickjs-wasmfile-release-sync')).default;
139
+ }
140
+ catch (e) {
141
+ throw new Error("sygnal/sandbox needs quickjs-emscripten-core and @jitl/quickjs-wasmfile-release-sync (optional peer dependencies): npm install quickjs-emscripten-core @jitl/quickjs-wasmfile-release-sync (" + (e?.message || e) + ')');
142
+ }
143
+ const custom = {};
144
+ if (wasm)
145
+ custom.wasmModule = wasm instanceof WebAssembly.Module ? wasm : await WebAssembly.compile(wasm);
146
+ else if (!variant && isNode())
147
+ custom.wasmModule = await (compiled ?? (compiled = nodeWasm()));
148
+ // a browser without `wasm` lets the variant fetch its .wasm itself (the bundler emits the file)
149
+ if (!sh)
150
+ custom.wasmMemory = new WebAssembly.Memory({ initial: 256, maximum: Math.max(256, Math.ceil(maxWasmBytes / 65536)) });
151
+ return core.newQuickJSWASMModuleFromVariant(Object.keys(custom).length ? core.newVariant(base, custom) : base);
152
+ }
153
+ async function nodeWasm() {
154
+ const [{ readFile }, { createRequire }] = await Promise.all([nodeImport('node:fs/promises'), nodeImport('node:module')]);
155
+ // next to sygnal (a peer), else from the app's root
156
+ let file;
157
+ try {
158
+ file = createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('sandbox-worker.cjs.js', document.baseURI).href))).resolve('@jitl/quickjs-wasmfile-release-sync/wasm');
159
+ }
160
+ catch {
161
+ file = createRequire(globalThis.process.cwd() + '/').resolve('@jitl/quickjs-wasmfile-release-sync/wasm');
162
+ }
163
+ return WebAssembly.compile(await readFile(file));
164
+ }
165
+ // Node built-ins by a name bundlers don't follow (a browser bundle never loads them)
166
+ const nodeImport = (name) => import(/* @vite-ignore */ /* webpackIgnore: true */ name);
167
+ /**
168
+ * Modules a guest crashed: the host's stack overflowed or the WASM trapped in the middle of a
169
+ * QuickJS call, so its C state was unwound without cleanup. createSandbox refuses them.
170
+ */
171
+ const poisoned = new WeakSet();
172
+ const checkModule = (QJS) => {
173
+ if (!QJS || typeof QJS.newRuntime != 'function')
174
+ throw new TypeError('sygnal/sandbox: quickjs must be a module from loadQuickJS()');
175
+ if (poisoned.has(QJS))
176
+ throw new SandboxError('crash', 'this QuickJS module crashed earlier; load a fresh one');
177
+ };
178
+
179
+ /*
180
+ * The page's side of the tree: mirrors the guest's serialized host tree into the real DOM through
181
+ * a sanitizer, and forwards DOM events back.
182
+ *
183
+ * Everything that arrives is untrusted (the guest can send any JSON). The mirror:
184
+ * - creates only allowlisted tags and attributes (no on*, srcdoc, formaction, <script>,
185
+ * <iframe>, <style>, <svg>, …); unknown tags are dropped, never rendered;
186
+ * - checks URLs: <a href> only http(s)/mailto (clicks never navigate by themselves: see
187
+ * bindEvents), <img src> only data:image/* or an allowlisted origin (an image is a request);
188
+ * - filters inline style (no url(), image-set(), expression(), escapes; plain names only);
189
+ * - prefixes ids (and for / aria-* references to them) so the guest can't clobber the page's
190
+ * ids or `window.<id>`; `name` only on form fields; classes are kept (the page's design system
191
+ * styles the guest) unless `classPrefix` is set;
192
+ * - limits nodes, depth and text so a huge tree can't stall the page;
193
+ * - confines layout: the root has `contain: content` (a containing block for position: fixed,
194
+ * paint clipped), `isolation: isolate` and `overflow: hidden` (no overlaying the page).
195
+ * A violation is recorded (violations[]) and the offending part dropped; a malformed tree (a
196
+ * cycle, a node inside itself) throws, and the sandbox is killed.
197
+ */
198
+ const TAGS = new Set(('div span p a button input textarea select option optgroup label ul ol li h1 h2 h3 h4 h5 h6 strong em b i u s small ' +
199
+ 'code pre kbd br hr img table caption thead tbody tfoot tr td th colgroup col form fieldset legend section article header ' +
200
+ 'footer nav main aside figure figcaption details summary progress meter time abbr blockquote q cite dl dt dd mark sub sup output').split(' '));
201
+ const ATTRS = /^(class|title|role|tabindex|lang|dir|hidden|alt|width|height|colspan|rowspan|scope|open|value|placeholder|checked|selected|disabled|readonly|required|multiple|min|max|step|minlength|maxlength|pattern|size|rows|cols|wrap|autocomplete|spellcheck|start|reversed|datetime|high|low|optimum|label|draggable)$|^aria-[a-z-]+$|^data-[a-z0-9-]+$/;
202
+ // no password or file inputs (phishing, file exfiltration)
203
+ const INPUT_TYPES = /^(text|search|email|url|tel|number|range|date|time|datetime-local|month|week|color|checkbox|radio|button|submit|reset|hidden)$/;
204
+ const ID_REFS = /^(for|aria-labelledby|aria-describedby|aria-controls|aria-owns|aria-activedescendant|list)$/;
205
+ const SAFE_HREF = /^(https?:|mailto:)/i;
206
+ const BAD_STYLE = /url\s*\(|image-set|expression|javascript:|\\|@|<|behavior|binding/i;
207
+ const FIELD = /^(input|textarea|select|button|output|fieldset)$/;
208
+ const CLASS_TOKEN = /^[A-Za-z0-9_-]+$/;
209
+ const DEFAULT_POLICY = { maxNodes: 5000, maxDepth: 128, maxText: 1 << 20, imageOrigins: [], classPrefix: '' };
210
+ /** one element's attributes and inline style through the allowlist (the mirror and SSR share it) */
211
+ function sanitize(tag, n, prefix, policy, violate) {
212
+ const attrs = new Map();
213
+ const a = n.a && typeof n.a == 'object' ? n.a : {};
214
+ for (const k0 of Object.keys(a)) {
215
+ const k = String(k0).toLowerCase(), v = String(a[k0]);
216
+ if (k === 'id')
217
+ attrs.set('id', prefix + v);
218
+ else if (ID_REFS.test(k))
219
+ attrs.set(k, v.split(/\s+/).filter(Boolean).map((x) => prefix + x).join(' '));
220
+ else if (k === 'name') {
221
+ if (FIELD.test(tag))
222
+ attrs.set(k, v);
223
+ else
224
+ violate(`name on <${tag}>`);
225
+ }
226
+ else if (k === 'type') {
227
+ if (tag !== 'input' || INPUT_TYPES.test(v))
228
+ attrs.set(k, v);
229
+ else
230
+ violate(`input type ${v}`);
231
+ }
232
+ else if (k === 'href') {
233
+ if (tag === 'a' && SAFE_HREF.test(v.trim()))
234
+ attrs.set(k, v);
235
+ else
236
+ violate(`href ${v.slice(0, 40)}`);
237
+ }
238
+ else if (k === 'src') {
239
+ if (tag === 'img' && imageOk(v, policy))
240
+ attrs.set(k, v);
241
+ else
242
+ violate(`src ${v.slice(0, 40)}`);
243
+ }
244
+ else if (k === 'style')
245
+ violate('style attribute'); // inline style arrives as n.s, filtered below
246
+ else if (k === 'class' && policy.classPrefix)
247
+ attrs.set(k, v.split(/\s+/).filter((c) => CLASS_TOKEN.test(c)).map((c) => policy.classPrefix + c).join(' '));
248
+ else if (ATTRS.test(k))
249
+ attrs.set(k, v);
250
+ else
251
+ violate(`attribute ${k}`);
252
+ }
253
+ if (tag === 'a' && attrs.has('href')) {
254
+ attrs.set('rel', 'noopener noreferrer');
255
+ attrs.set('target', '_blank');
256
+ }
257
+ const css = [];
258
+ const s = n.s && typeof n.s == 'object' ? n.s : {};
259
+ for (const k of Object.keys(s)) {
260
+ const name = String(k).replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()), v = String(s[k]);
261
+ if (!/^-?[a-z][a-z-]*$/.test(name) || BAD_STYLE.test(v)) {
262
+ violate(`style ${name}`);
263
+ continue;
264
+ }
265
+ css.push(`${name}: ${v}`);
266
+ }
267
+ return { attrs, css: css.join('; ') };
268
+ }
269
+ function imageOk(v, policy) {
270
+ if (/^data:image\/(png|gif|jpeg|webp);base64,/i.test(v))
271
+ return true;
272
+ try {
273
+ return policy.imageOrigins.includes(new URL(v).origin);
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ }
279
+ let roots = 0;
280
+ function createMirror(container, opts = {}) {
281
+ const policy = { ...DEFAULT_POLICY, ...opts.policy };
282
+ const doc = container.ownerDocument;
283
+ const prefix = policy.idPrefix ?? `sbx${++roots}-`;
284
+ const root = doc.createElement('div');
285
+ root.setAttribute('data-sandbox-root', '');
286
+ root.style.cssText = 'contain: content; isolation: isolate; position: relative; overflow: hidden';
287
+ container.appendChild(root);
288
+ const nodes = new Map(); // guest id → real node
289
+ const idOf = new WeakMap(); // real node → guest id
290
+ const violations = [];
291
+ const violate = (what) => { if (violations.length < 100)
292
+ violations.push(what); };
293
+ let count = 0, text = 0, alive = new Set();
294
+ const keepSubtree = (n) => {
295
+ const i = idOf.get(n);
296
+ if (i !== undefined)
297
+ alive.add(i);
298
+ n.childNodes.forEach(keepSubtree);
299
+ };
300
+ function build(n, depth) {
301
+ if (depth > policy.maxDepth)
302
+ throw new Error('sandbox tree: too deep');
303
+ if (++count > policy.maxNodes)
304
+ throw new Error('sandbox tree: too many nodes');
305
+ if (typeof n == 'number') {
306
+ const real = nodes.get(n);
307
+ if (!real)
308
+ return null; // dropped by the sanitizer when it was sent
309
+ keepSubtree(real);
310
+ return real;
311
+ }
312
+ if (!n || typeof n != 'object' || typeof n.i != 'number')
313
+ throw new Error('sandbox tree: malformed node');
314
+ alive.add(n.i);
315
+ if (typeof n.x == 'string') {
316
+ if ((text += n.x.length) > policy.maxText)
317
+ throw new Error('sandbox tree: too much text');
318
+ let t = nodes.get(n.i);
319
+ if (!t || t.nodeType !== 3) {
320
+ t = doc.createTextNode(n.x);
321
+ nodes.set(n.i, t);
322
+ idOf.set(t, n.i);
323
+ }
324
+ else if (t.data !== n.x)
325
+ t.data = n.x;
326
+ return t;
327
+ }
328
+ const tag = String(n.t);
329
+ if (!TAGS.has(tag)) {
330
+ violate(`tag <${tag}>`);
331
+ return null;
332
+ }
333
+ let el = nodes.get(n.i);
334
+ if (!el || el.localName !== tag) {
335
+ el = doc.createElement(tag);
336
+ nodes.set(n.i, el);
337
+ idOf.set(el, n.i);
338
+ }
339
+ const { attrs: want, css } = sanitize(tag, n, prefix, policy, violate);
340
+ for (const a of [...el.attributes])
341
+ if (a.name !== 'style' && !want.has(a.name))
342
+ el.removeAttribute(a.name);
343
+ for (const [k, v] of want)
344
+ if (el.getAttribute(k) !== v)
345
+ el.setAttribute(k, v);
346
+ if ((el.getAttribute('style') || '') !== css)
347
+ css ? el.setAttribute('style', css) : el.removeAttribute('style');
348
+ // field state the guest controls
349
+ const f = el;
350
+ if ('v' in n && f.value !== String(n.v))
351
+ f.value = String(n.v);
352
+ if ('k' in n && (tag === 'input' || tag === 'option'))
353
+ tag === 'option' ? (f.selected = !!n.k) : (f.checked = !!n.k);
354
+ const kids = [];
355
+ for (const c of Array.isArray(n.c) ? n.c : []) {
356
+ const r = build(c, depth + 1);
357
+ if (r)
358
+ kids.push(r);
359
+ }
360
+ for (let i = 0; i < kids.length; i++) {
361
+ if (kids[i].contains(el))
362
+ throw new Error('sandbox tree: a node inside itself');
363
+ if (el.childNodes[i] !== kids[i])
364
+ el.insertBefore(kids[i], el.childNodes[i] || null);
365
+ }
366
+ while (el.childNodes.length > kids.length)
367
+ el.removeChild(el.lastChild);
368
+ return el;
369
+ }
370
+ return {
371
+ root, violations, prefix,
372
+ /** apply one render's tree; throws on a malformed one */
373
+ apply(tree) {
374
+ count = 0;
375
+ text = 0;
376
+ alive = new Set();
377
+ const top = [];
378
+ for (const n of Array.isArray(tree) ? tree : []) {
379
+ const r = build(n, 0);
380
+ if (r)
381
+ top.push(r);
382
+ }
383
+ for (let i = 0; i < top.length; i++) {
384
+ if (root.childNodes[i] !== top[i])
385
+ root.insertBefore(top[i], root.childNodes[i] || null);
386
+ }
387
+ while (root.childNodes.length > top.length)
388
+ root.removeChild(root.lastChild);
389
+ for (const k of [...nodes.keys()])
390
+ if (!alive.has(k))
391
+ nodes.delete(k);
392
+ },
393
+ /**
394
+ * The guest moved focus (README §9 Q5): only with the focus capability, and only while the
395
+ * page's focus is already inside the box. A guest can manage focus in its own dialog, never
396
+ * take it from the page.
397
+ */
398
+ focus(id) {
399
+ const el = nodes.get(id);
400
+ if (!opts.focus)
401
+ return void violate('focus (no focus capability)');
402
+ if (!el || el.nodeType !== 1 || !root.contains(doc.activeElement) || doc.activeElement === el)
403
+ return;
404
+ el.focus?.();
405
+ },
406
+ /** the guest id of a real node, or of its nearest mirrored ancestor inside the root */
407
+ idFor(target) {
408
+ for (let n = target; n && n !== root; n = n.parentNode) {
409
+ const i = idOf.get(n);
410
+ if (i !== undefined)
411
+ return i;
412
+ }
413
+ return undefined;
414
+ },
415
+ clear() { root.remove(); nodes.clear(); },
416
+ };
417
+ }
418
+ const FORWARD = /^(click|dblclick|input|change|submit|keydown|keyup|focus|blur|focusin|focusout|pointerdown|pointerup|mouseenter|mouseleave)$/;
419
+ const NO_BUBBLE = /^(focus|blur|mouseenter|mouseleave)$/;
420
+ const STOP = ('click dblclick auxclick contextmenu mousedown mouseup mouseover mouseout pointerdown pointerup pointerover pointerout ' +
421
+ 'input change submit reset keydown keyup keypress beforeinput focusin focusout touchstart touchend wheel select').split(' ');
422
+ const KEYS = ['key', 'code', 'shiftKey', 'ctrlKey', 'altKey', 'metaKey', 'button', 'clientX', 'clientY'];
423
+ /**
424
+ * DOM events → the guest: one listener at the mirror's root per type the guest listens for
425
+ * (delegation, as the DOM driver does), with the event's plain fields and the target's value /
426
+ * checked. `dispatch` returns whether the guest prevented the default (in-thread: synchronous;
427
+ * a worker answers too late, so only declared keys are prevented). A form submit and a link
428
+ * click never act on the page by themselves: an unprevented link click goes to `navigate(href)`.
429
+ */
430
+ function bindEvents(mirror, dispatch, navigate) {
431
+ const root = mirror.root, on = new Map();
432
+ const add = (key, l, capture = false) => {
433
+ on.set(key, [l, capture]);
434
+ root.addEventListener(key.replace(/^\$/, ''), l, capture);
435
+ };
436
+ function onEvent(e) {
437
+ const id = mirror.idFor(e.target);
438
+ if (id === undefined)
439
+ return;
440
+ const t = e.target, init = {};
441
+ for (const k of KEYS)
442
+ if (e[k] !== undefined)
443
+ init[k] = e[k];
444
+ if (t && /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)) {
445
+ init.value = t.value;
446
+ if (t.type === 'checkbox' || t.type === 'radio')
447
+ init.checked = t.checked;
448
+ }
449
+ if (dispatch(id, e.type, init))
450
+ e.preventDefault();
451
+ }
452
+ add('$submit', (e) => e.preventDefault());
453
+ // The guest's elements are not the host app's: its delegated intents (DOM.select('.danger'),
454
+ // bubbling at the app root) must never see a sandboxed element's events, or a guest could render
455
+ // a class the host listens to and have the user trigger a host action. The page's own capture
456
+ // listeners still see them.
457
+ for (const t of STOP)
458
+ add('$' + t, (e) => e.stopPropagation());
459
+ const link = (e) => {
460
+ const a = e.target?.closest?.('a[href]');
461
+ if (!a || !root.contains(a))
462
+ return;
463
+ const was = e.defaultPrevented;
464
+ e.preventDefault();
465
+ if (!was)
466
+ navigate?.(a.getAttribute('href'));
467
+ };
468
+ add('$click', link);
469
+ return {
470
+ listen(types) {
471
+ for (const t of types)
472
+ if (!on.has(t) && FORWARD.test(t))
473
+ add(t, onEvent, NO_BUBBLE.test(t));
474
+ // the link check runs after the guest's own click (whose preventDefault it honours): last
475
+ if (types.includes('click')) {
476
+ root.removeEventListener('click', link);
477
+ root.addEventListener('click', link);
478
+ }
479
+ },
480
+ detach() {
481
+ for (const [t, [l, c]] of on)
482
+ root.removeEventListener(t.replace(/^\$/, ''), l, c);
483
+ root.removeEventListener('click', link);
484
+ on.clear();
485
+ },
486
+ };
487
+ }
488
+
489
+ /*
490
+ * The in-thread engine: one guest in its own QuickJS runtime and context. Used directly for
491
+ * `isolation: 'thread'`, and inside the Worker for `isolation: 'worker'` (no container there:
492
+ * renders go out as JSON).
493
+ *
494
+ * Isolation comes from QuickJS: the guest's globals are QuickJS's own (no window, document,
495
+ * fetch, storage; the Function constructor reaches only the guest global). Its only way out is
496
+ * the `__host` functions below, each of which checks its arguments. Every entry (boot, an event,
497
+ * a timer, a fetch reply) runs under a deadline (the interrupt handler) with the runtime's
498
+ * memory and stack limits, and a CPU share over a rolling second. A guest that trips a limit is
499
+ * killed: there is no safe way to resume a context interrupted half way through a flush.
500
+ *
501
+ * The deadline stops accidents, not hostile code: QuickJS polls the interrupt handler once per
502
+ * 10,000 ticks and never inside a builtin (README finding 2). The Worker's watchdog is the hard stop.
503
+ */
504
+ const ENTRIES = ['dispatch', 'fire', 'resolve', 'props', 'start', 'flush'];
505
+ function createEngine(QJS, opts) {
506
+ checkModule(QJS);
507
+ const limits = opts.limits;
508
+ const now = () => performance.now();
509
+ const stats = { bootMs: 0, calls: 0, guestMs: 0, renders: 0, lastCallMs: 0 };
510
+ let deadline = Infinity;
511
+ const rt = QJS.newRuntime();
512
+ rt.setMemoryLimit(limits.memoryBytes);
513
+ rt.setMaxStackSize(limits.stackBytes);
514
+ rt.setInterruptHandler(() => now() > deadline);
515
+ const ctx = rt.newContext();
516
+ let dead = false, crashed = false, logs = 0, immediates = 0;
517
+ // renders are deltas against the previous one (unchanged subtrees are bare ids): all are applied, in order
518
+ const renders = [], emits = [];
519
+ const timers = new Map(), fetches = new Map();
520
+ const mirror = opts.container ? createMirror(opts.container, { policy: opts.policy, focus: !!opts.focus }) : null;
521
+ // a DOM event goes into the guest synchronously, so its preventDefault() reaches the page's event
522
+ const events = mirror && bindEvents(mirror, (id, type, init) => !!enter('dispatch', [id, type, JSON.stringify(init)]), (href) => opts.navigate?.(href));
523
+ // --- the bridge: the only functions the guest can reach --------------------------------------
524
+ const str = (h, max = limits.maxMessageBytes) => {
525
+ if (ctx.typeof(h) !== 'string')
526
+ throw new SandboxError('protocol', 'expected a string');
527
+ const s = ctx.getString(h);
528
+ if (s.length > max)
529
+ throw new SandboxError('protocol', `message over ${max} bytes`);
530
+ return s;
531
+ };
532
+ const num = (h) => {
533
+ const n = ctx.typeof(h) === 'number' ? ctx.getNumber(h) : NaN;
534
+ if (!Number.isFinite(n))
535
+ throw new SandboxError('protocol', 'expected a number');
536
+ return n;
537
+ };
538
+ const host = ctx.newObject();
539
+ const fn = (name, f) => {
540
+ const h = ctx.newFunction(name, (...a) => {
541
+ try {
542
+ return f(...a);
543
+ }
544
+ catch (e) {
545
+ // a protocol violation kills; anything else is thrown into the guest
546
+ if (e instanceof SandboxError)
547
+ queueMicrotask(() => kill(e.reason, e.message));
548
+ return { error: ctx.newError(String(e?.message || e)) };
549
+ }
550
+ });
551
+ ctx.setProp(host, name, h);
552
+ h.dispose();
553
+ };
554
+ fn('render', (h) => {
555
+ if (renders.length >= 4)
556
+ throw new SandboxError('protocol', 'too many renders in one call');
557
+ renders.push(str(h));
558
+ stats.renders++;
559
+ });
560
+ fn('emit', (h) => {
561
+ // delivered when the guest call returns, after its render (never into a running guest call)
562
+ if (emits.length >= 64)
563
+ throw new SandboxError('protocol', 'too many EVENTS in one call');
564
+ const ev = JSON.parse(str(h, 64 << 10));
565
+ if (!ev || typeof ev != 'object' || typeof ev.type != 'string')
566
+ throw new SandboxError('protocol', 'an event needs a string type');
567
+ emits.push(ev);
568
+ });
569
+ fn('log', (lv, h) => {
570
+ if (++logs > limits.maxLogs)
571
+ return;
572
+ const level = str(lv, 16), text = str(h, 4096);
573
+ if (opts.log)
574
+ opts.log(level, text);
575
+ else
576
+ console[level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log']('[sandbox]', text);
577
+ });
578
+ fn('timer', (idH, msH) => {
579
+ const id = num(idH), raw = num(msH);
580
+ // < 0: setImmediate (the core's macrotask hop), counted apart from the component's timers
581
+ if (raw < 0) {
582
+ if (immediates >= 16)
583
+ throw new Error('sandbox: too many setImmediate');
584
+ immediates++;
585
+ timers.set(id, setTimeout(() => { immediates--; timers.delete(id); enter('fire', [id]); }, 0));
586
+ return;
587
+ }
588
+ const ms = Math.max(limits.minTimerMs, Math.min(raw, 2 ** 31 - 1));
589
+ if (timers.size - immediates >= limits.maxTimers)
590
+ throw new Error(`sandbox: more than ${limits.maxTimers} timers`);
591
+ timers.set(id, setTimeout(() => { timers.delete(id); enter('fire', [id]); }, ms));
592
+ });
593
+ fn('clearTimer', (idH) => {
594
+ const id = num(idH);
595
+ clearTimeout(timers.get(id));
596
+ timers.delete(id);
597
+ });
598
+ fn('fetch', (idH, urlH, initH) => {
599
+ const id = num(idH), url = str(urlH, 4096), init = JSON.parse(str(initH, 256 << 10));
600
+ const done = (ok, payload) => { fetches.delete(id); if (!dead)
601
+ enter('resolve', [id, ok ? 1 : 0, payload]); };
602
+ if (fetches.size >= limits.maxFetches)
603
+ return void queueMicrotask(() => done(false, 'sandbox fetch denied: too many requests in flight'));
604
+ const ac = new AbortController();
605
+ fetches.set(id, ac);
606
+ capFetch(opts.fetch, url, init, limits.maxMessageBytes, ac.signal)
607
+ .then((res) => done(true, JSON.stringify(res)), (e) => done(false, `sandbox fetch denied: ${String(e?.message || e)}`));
608
+ });
609
+ ctx.setProp(ctx.global, '__host', host);
610
+ host.dispose();
611
+ // --- entering the guest -----------------------------------------------------------------------
612
+ let windowStart = now(), windowMs = 0;
613
+ const entries = {};
614
+ function enter(name, args, budget = limits.callMs) {
615
+ if (dead)
616
+ return;
617
+ const t0 = now();
618
+ deadline = t0 + budget;
619
+ let out;
620
+ try {
621
+ const hs = args.map((a) => (typeof a == 'number' ? ctx.newNumber(a) : ctx.newString(String(a))));
622
+ const r = ctx.callFunction(entries[name], ctx.undefined, ...hs);
623
+ hs.forEach((h) => h.dispose());
624
+ out = settle(r);
625
+ // the promise jobs (the core's flush is a microtask) run under the same deadline
626
+ settle(rt.executePendingJobs(10000));
627
+ if (rt.hasPendingJob())
628
+ throw new SandboxError('timeout', 'endless promise jobs');
629
+ // then the tree, once
630
+ settle(ctx.callFunction(entries.flush, ctx.undefined));
631
+ }
632
+ catch (e) {
633
+ deadline = Infinity;
634
+ kill(e.reason || classify(e), e.message || String(e));
635
+ return;
636
+ }
637
+ deadline = Infinity;
638
+ const ms = now() - t0;
639
+ stats.calls++;
640
+ stats.guestMs += ms;
641
+ stats.lastCallMs = ms;
642
+ if (t0 - windowStart > 1000) {
643
+ windowStart = t0;
644
+ windowMs = 0;
645
+ }
646
+ if ((windowMs += ms) > limits.cpuShare * 1000) {
647
+ kill('cpu', `over ${limits.cpuShare * 100}% CPU`);
648
+ return;
649
+ }
650
+ deliver();
651
+ return out;
652
+ }
653
+ function settle(r) {
654
+ if (r.error) {
655
+ const e = ctx.dump(r.error);
656
+ r.error.dispose();
657
+ const err = new Error(typeof e == 'object' && e ? `${e.name}: ${e.message}` : String(e));
658
+ err.guest = true;
659
+ throw err;
660
+ }
661
+ const v = ctx.typeof(r.value) === 'number' ? ctx.getNumber(r.value) : undefined;
662
+ r.value.dispose?.();
663
+ return v;
664
+ }
665
+ function classify(e) {
666
+ // not the guest's own exception: the host's stack or the WASM gave out mid-call (finding 3)
667
+ if (!e.guest) {
668
+ poisoned.add(QJS);
669
+ crashed = true;
670
+ return 'crash';
671
+ }
672
+ if (/out of memory|stack overflow/i.test(e.message))
673
+ return 'memory';
674
+ if (/interrupted/i.test(e.message)) {
675
+ // near the memory limit every allocation collects the whole heap: that thrash ends as a timeout
676
+ try {
677
+ const h = rt.computeMemoryUsage(), m = ctx.dump(h);
678
+ h.dispose();
679
+ if (m.memory_used_size > 0.9 * limits.memoryBytes)
680
+ return 'memory';
681
+ }
682
+ catch { }
683
+ return 'timeout';
684
+ }
685
+ return 'error';
686
+ }
687
+ function deliver() {
688
+ while (renders.length && !dead) {
689
+ const json = renders.shift();
690
+ if (!mirror) {
691
+ opts.onRender?.(json);
692
+ continue;
693
+ }
694
+ try {
695
+ applyRender(mirror, events, json);
696
+ }
697
+ catch (e) {
698
+ kill('protocol', e.message);
699
+ }
700
+ }
701
+ while (emits.length && !dead)
702
+ opts.onEmit?.(emits.shift());
703
+ }
704
+ // --- lifecycle --------------------------------------------------------------------------------
705
+ function kill(reason, message) {
706
+ if (dead)
707
+ return;
708
+ dead = true;
709
+ cleanup();
710
+ opts.onError?.(new SandboxError(reason, message));
711
+ }
712
+ function cleanup() {
713
+ for (const t of timers.values())
714
+ clearTimeout(t);
715
+ for (const ac of fetches.values())
716
+ ac.abort();
717
+ timers.clear();
718
+ fetches.clear();
719
+ if (mirror) {
720
+ events.detach();
721
+ mirror.clear();
722
+ }
723
+ // a crashed runtime is leaked: freeing it asserts (an abort that would end the whole module)
724
+ if (crashed)
725
+ return;
726
+ for (const k in entries)
727
+ entries[k]?.alive && entries[k].dispose();
728
+ try {
729
+ ctx.dispose();
730
+ rt.dispose();
731
+ }
732
+ catch {
733
+ poisoned.add(QJS);
734
+ }
735
+ }
736
+ const t0 = now();
737
+ deadline = t0 + limits.bootMs;
738
+ try {
739
+ settle(ctx.evalCode(opts.kernel, 'sygnal-sandbox-kernel.js'));
740
+ const sbx = ctx.getProp(ctx.global, '__sandbox');
741
+ const v = ctx.typeof(sbx) === 'object' ? ctx.getProp(sbx, 'v') : null;
742
+ const version = v && ctx.typeof(v) === 'number' ? ctx.getNumber(v) : 0;
743
+ v?.dispose();
744
+ if (version !== PROTOCOL) {
745
+ sbx.dispose();
746
+ throw new SandboxError('protocol', version
747
+ ? `the kernel speaks protocol ${version}, this sygnal/sandbox ${PROTOCOL}: rebuild it from the installed sygnal`
748
+ : 'not a sandbox kernel: import it from sygnal/sandbox/kernel?raw through sygnal/vite, or build it with buildSandboxKernel()');
749
+ }
750
+ // the kernel's own functions, taken before the component runs (it could replace __sandbox)
751
+ for (const k of ENTRIES)
752
+ entries[k] = ctx.getProp(sbx, k);
753
+ sbx.dispose();
754
+ settle(ctx.evalCode(opts.source, 'component.js'));
755
+ deadline = Infinity;
756
+ if (opts.props !== undefined)
757
+ enter('props', [JSON.stringify(opts.props)], limits.bootMs);
758
+ enter('start', [opts.exportName || ''], Math.max(1, limits.bootMs - (now() - t0)));
759
+ }
760
+ catch (e) {
761
+ deadline = Infinity;
762
+ kill(e.reason || classify(e), e.message);
763
+ }
764
+ stats.bootMs = now() - t0;
765
+ return {
766
+ stats,
767
+ get dead() { return dead; },
768
+ get violations() { return mirror ? mirror.violations : []; },
769
+ root: mirror ? mirror.root : null,
770
+ setProps(p) { enter('props', [JSON.stringify(p ?? null)]); },
771
+ /** an event as if it came from the element with that guest id (tests, benchmarks, the worker) */
772
+ dispatch(id, type, init = {}) { return enter('dispatch', [id, type, JSON.stringify(init)]); },
773
+ memory() {
774
+ if (dead)
775
+ return null;
776
+ const h = rt.computeMemoryUsage(), m = ctx.dump(h);
777
+ h.dispose();
778
+ return m;
779
+ },
780
+ dispose() { if (!dead) {
781
+ dead = true;
782
+ cleanup();
783
+ } },
784
+ };
785
+ }
786
+ /** one render message onto a mirror (both tiers); throws on a malformed one */
787
+ function applyRender(mirror, events, json) {
788
+ const msg = JSON.parse(json);
789
+ if (!msg || typeof msg != 'object' || msg.v !== PROTOCOL)
790
+ throw new Error('sandbox render: wrong protocol');
791
+ mirror.apply(msg.tree);
792
+ events.listen(Array.isArray(msg.types) ? msg.types.map(String) : []);
793
+ if (typeof msg.f == 'number')
794
+ mirror.focus(msg.f);
795
+ }
796
+
797
+ /*
798
+ * Inside the Worker (a browser Worker or Node's worker_threads): one sandbox, the QuickJS side of
799
+ * `isolation: 'worker'` (worker.ts). Built as dist/sandbox-worker.{esm,cjs}.js; its only imports
800
+ * are the optional peers. No DOM here: renders go to the page as JSON, fetches go to the page
801
+ * (its policy, its fetch), and the page's watchdog terminates the worker when it stops answering.
802
+ */
803
+ const g = globalThis;
804
+ let port = null;
805
+ const post = (m) => port.postMessage(m);
806
+ const fetches = new Map();
807
+ let sb = null, fetchSeq = 0;
808
+ function onMessage(m) {
809
+ if (!m || typeof m != 'object')
810
+ return;
811
+ if (m.t === 'init')
812
+ return void init(m).catch((e) => post({ t: 'error', reason: 'error', message: String(e?.message || e) }));
813
+ if (m.t === 'ping')
814
+ return post({ t: 'pong' });
815
+ if (m.t === 'fetched') {
816
+ const f = fetches.get(m.id);
817
+ fetches.delete(m.id);
818
+ return f && (m.ok ? f.ok(m.res) : f.fail(new Error(m.error)));
819
+ }
820
+ if (!sb)
821
+ return;
822
+ if (m.t === 'event')
823
+ sb.dispatch(m.id, String(m.type), m.init || {});
824
+ else if (m.t === 'props')
825
+ sb.setProps(m.p);
826
+ }
827
+ async function init(m) {
828
+ if (m.v !== PROTOCOL)
829
+ return post({ t: 'error', reason: 'protocol', message: `the page speaks protocol ${m.v}, the worker ${PROTOCOL}` });
830
+ const limits = limitsOf(m.limits);
831
+ const t0 = performance.now();
832
+ const QJS = await loadQuickJS({ maxWasmBytes: limits.maxWasmBytes, wasm: m.wasm });
833
+ const wasmMs = performance.now() - t0;
834
+ sb = createEngine(QJS, {
835
+ kernel: m.kernel, source: m.source, exportName: m.exportName, props: m.props, limits,
836
+ // checked here (the allowlist) and again on the page, which makes the request
837
+ fetch: m.fetch && { ...m.fetch, impl: (url, init) => new Promise((ok, fail) => {
838
+ const id = ++fetchSeq;
839
+ fetches.set(id, { ok, fail });
840
+ post({ t: 'fetch', id, url, init: { method: init.method, headers: init.headers, body: init.body } });
841
+ }).then((r) => ({ status: r.status, headers: { get: (k) => r.headers[k.toLowerCase()] ?? null }, text: async () => r.body })) },
842
+ onEmit: (e) => post({ t: 'emit', e }),
843
+ log: (l, x) => post({ t: 'log', l, x }),
844
+ onRender: (json) => post({ t: 'render', json }),
845
+ onError: (e) => post({ t: 'error', reason: e.reason, message: e.message }),
846
+ });
847
+ post({ t: 'ready', v: PROTOCOL, wasmMs, bootMs: sb.stats.bootMs });
848
+ }
849
+ if (typeof g.process == 'object' && g.process.versions?.node && typeof g.WorkerGlobalScope == 'undefined') {
850
+ // worker_threads: parentPort (it holds messages until the listener is attached)
851
+ nodeImport('node:worker_threads').then((m) => { port = m.parentPort; port.on('message', onMessage); });
852
+ }
853
+ else {
854
+ port = g;
855
+ g.addEventListener('message', (e) => onMessage(e.data));
856
+ }
857
+ //# sourceMappingURL=sandbox-worker.cjs.js.map