@markii/host 0.0.1 → 0.17.0-next.2

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 (192) hide show
  1. package/README.md +18 -2
  2. package/dist/browser-worker.js +639 -0
  3. package/dist/browser.d.ts +57 -0
  4. package/dist/browser.js +167 -0
  5. package/dist/conformance/host/01-plain-open/expected.json +5 -0
  6. package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
  7. package/dist/conformance/host/01-plain-open/scenario.json +6 -0
  8. package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
  9. package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
  10. package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
  11. package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
  12. package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
  13. package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
  14. package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
  15. package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
  16. package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
  17. package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
  18. package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
  19. package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
  20. package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
  21. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
  22. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
  23. package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
  24. package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
  25. package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
  26. package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
  27. package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
  28. package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
  29. package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
  30. package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
  31. package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
  32. package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
  33. package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
  34. package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
  35. package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
  36. package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
  37. package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
  38. package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
  39. package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
  40. package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
  41. package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
  42. package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
  43. package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
  44. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
  45. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
  46. package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
  47. package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
  48. package/dist/conformance/host/14-remove-pack/expected.json +14 -0
  49. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
  50. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
  51. package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
  52. package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
  53. package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
  54. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
  55. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
  56. package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
  57. package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
  58. package/dist/conformance/host/16-pack-trust/expected.json +37 -0
  59. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
  60. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
  61. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
  62. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
  63. package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
  64. package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
  65. package/dist/conformance/host/README.md +221 -0
  66. package/dist/conformance.d.ts +2 -0
  67. package/dist/conformance.js +5 -0
  68. package/dist/diagnostics/render-diagnostics.d.ts +41 -0
  69. package/dist/diagnostics/render-diagnostics.js +46 -0
  70. package/dist/export/cascade.d.ts +98 -0
  71. package/dist/export/cascade.js +159 -0
  72. package/dist/export/export-zip.d.ts +19 -0
  73. package/dist/export/export-zip.js +38 -0
  74. package/dist/export/image-embed.d.ts +126 -0
  75. package/dist/export/image-embed.js +274 -0
  76. package/dist/export/note-export.d.ts +332 -0
  77. package/dist/export/note-export.js +366 -0
  78. package/dist/export/note-links.d.ts +83 -0
  79. package/dist/export/note-links.js +241 -0
  80. package/dist/glue.wasm +0 -0
  81. package/dist/host/adapter.d.ts +223 -0
  82. package/dist/host/adapter.js +9 -0
  83. package/dist/host/cascade-export-behavior.d.ts +120 -0
  84. package/dist/host/cascade-export-behavior.js +151 -0
  85. package/dist/host/conformance-runner.d.ts +156 -0
  86. package/dist/host/conformance-runner.js +634 -0
  87. package/dist/host/create-host.d.ts +253 -0
  88. package/dist/host/create-host.js +329 -0
  89. package/dist/host/editor-behavior.d.ts +157 -0
  90. package/dist/host/editor-behavior.js +227 -0
  91. package/dist/host/export/md-plain.d.ts +15 -0
  92. package/dist/host/export/md-plain.js +125 -0
  93. package/dist/host/export-behavior.d.ts +113 -0
  94. package/dist/host/export-behavior.js +257 -0
  95. package/dist/host/labels.d.ts +24 -0
  96. package/dist/host/labels.js +32 -0
  97. package/dist/host/pack-archive.d.ts +30 -0
  98. package/dist/host/pack-archive.js +68 -0
  99. package/dist/host/pack-install.d.ts +88 -0
  100. package/dist/host/pack-install.js +189 -0
  101. package/dist/host/pack-load.d.ts +72 -0
  102. package/dist/host/pack-load.js +57 -0
  103. package/dist/host/pack-remove.d.ts +21 -0
  104. package/dist/host/pack-remove.js +65 -0
  105. package/dist/host/pack-trust-behavior.d.ts +79 -0
  106. package/dist/host/pack-trust-behavior.js +262 -0
  107. package/dist/host/refresh-interval.d.ts +39 -0
  108. package/dist/host/refresh-interval.js +61 -0
  109. package/dist/host/run-behavior.d.ts +89 -0
  110. package/dist/host/run-behavior.js +168 -0
  111. package/dist/host/run-diagnostics.d.ts +26 -0
  112. package/dist/host/run-diagnostics.js +43 -0
  113. package/dist/host/script-execution.d.ts +72 -0
  114. package/dist/host/script-execution.js +68 -0
  115. package/dist/host/terminal-packs.d.ts +39 -0
  116. package/dist/host/terminal-packs.js +51 -0
  117. package/dist/html-engine.d.ts +2 -0
  118. package/dist/html-engine.js +29 -0
  119. package/dist/index.d.ts +95 -0
  120. package/dist/index.js +70 -0
  121. package/dist/insert/component-catalog.d.ts +23 -0
  122. package/dist/insert/component-catalog.js +96 -0
  123. package/dist/node-fs.d.ts +14 -0
  124. package/dist/node-fs.js +70 -0
  125. package/dist/node.d.ts +5 -0
  126. package/dist/node.js +13 -0
  127. package/dist/package.json +3 -0
  128. package/dist/packs/discover.d.ts +119 -0
  129. package/dist/packs/discover.js +266 -0
  130. package/dist/packs/pack-diagnostics.d.ts +81 -0
  131. package/dist/packs/pack-diagnostics.js +105 -0
  132. package/dist/packs/pack-paths.d.ts +53 -0
  133. package/dist/packs/pack-paths.js +106 -0
  134. package/dist/packs/pack-render-registry.d.ts +83 -0
  135. package/dist/packs/pack-render-registry.js +182 -0
  136. package/dist/packs/pack-scripts.d.ts +20 -0
  137. package/dist/packs/pack-scripts.js +96 -0
  138. package/dist/packs/pack-trust.d.ts +57 -0
  139. package/dist/packs/pack-trust.js +111 -0
  140. package/dist/packs/prebuilt.d.ts +49 -0
  141. package/dist/packs/prebuilt.js +97 -0
  142. package/dist/path-util.d.ts +24 -0
  143. package/dist/path-util.js +82 -0
  144. package/dist/run/browser-isolate.d.ts +45 -0
  145. package/dist/run/browser-isolate.js +81 -0
  146. package/dist/run/bundle-document.d.ts +29 -0
  147. package/dist/run/bundle-document.js +40 -0
  148. package/dist/run/bundle-run.d.ts +188 -0
  149. package/dist/run/bundle-run.js +232 -0
  150. package/dist/run/grant-flow.d.ts +232 -0
  151. package/dist/run/grant-flow.js +390 -0
  152. package/dist/run/ip-address.d.ts +41 -0
  153. package/dist/run/ip-address.js +255 -0
  154. package/dist/run/isolate.d.ts +57 -0
  155. package/dist/run/isolate.js +22 -0
  156. package/dist/run/lua-resolver.d.ts +21 -0
  157. package/dist/run/lua-resolver.js +63 -0
  158. package/dist/run/net-bridge-worker.d.ts +26 -0
  159. package/dist/run/net-bridge-worker.js +66 -0
  160. package/dist/run/net-bridge.d.ts +56 -0
  161. package/dist/run/net-bridge.js +59 -0
  162. package/dist/run/net-pinning.d.ts +82 -0
  163. package/dist/run/net-pinning.js +137 -0
  164. package/dist/run/net-provider.d.ts +70 -0
  165. package/dist/run/net-provider.js +289 -0
  166. package/dist/run/node-isolate.d.ts +30 -0
  167. package/dist/run/node-isolate.js +117 -0
  168. package/dist/run/run-flow.d.ts +187 -0
  169. package/dist/run/run-flow.js +323 -0
  170. package/dist/run/run-host.d.ts +107 -0
  171. package/dist/run/run-host.js +241 -0
  172. package/dist/run/run-job.d.ts +130 -0
  173. package/dist/run/run-job.js +279 -0
  174. package/dist/run/run-progress.d.ts +55 -0
  175. package/dist/run/run-progress.js +42 -0
  176. package/dist/run/run-trace.d.ts +44 -0
  177. package/dist/run/run-trace.js +36 -0
  178. package/dist/run/script-requirements.d.ts +98 -0
  179. package/dist/run/script-requirements.js +212 -0
  180. package/dist/run/snapshot-storage.d.ts +25 -0
  181. package/dist/run/snapshot-storage.js +31 -0
  182. package/dist/run/stale-values.d.ts +25 -0
  183. package/dist/run/stale-values.js +32 -0
  184. package/dist/run/worker-entry-browser.d.ts +42 -0
  185. package/dist/run/worker-entry-browser.js +77 -0
  186. package/dist/run/worker-entry.d.ts +1 -0
  187. package/dist/run/worker-entry.js +96 -0
  188. package/dist/values-failure.d.ts +17 -0
  189. package/dist/values-failure.js +1 -0
  190. package/dist/values-merge.d.ts +39 -0
  191. package/dist/values-merge.js +33 -0
  192. package/package.json +77 -4
@@ -0,0 +1,212 @@
1
+ import { extractScripts, parse } from '@markii/core';
2
+ /** The three host-reaching capability calls this scan looks for (docs/scripting.md's `net` table). */
3
+ const NET_CALL_NAMES = ['fetch_json', 'post', 'patch'];
4
+ const CALL_SITE_PATTERN = new RegExp(`\\bnet\\.(${NET_CALL_NAMES.join('|')})\\s*\\(`, 'g');
5
+ /**
6
+ * Given `source` and the index of the character immediately after a call's
7
+ * opening `(` (already matched by `CALL_SITE_PATTERN`), returns the raw
8
+ * text of that call's FIRST argument — up to (not including) the first
9
+ * top-level `,` or the call's closing `)`, tracking nested
10
+ * `(`/`)`/`[`/`]`/`{`/`}` depth and quoted strings so a comma or paren
11
+ * INSIDE a nested expression or a quoted string never ends the scan early.
12
+ * Returns `undefined` if the call is unterminated (no matching close found
13
+ * before the source ends) — a malformed/incomplete script, treated the
14
+ * same as any other non-literal argument by the caller.
15
+ */
16
+ function extractFirstArgument(source, argStart) {
17
+ let depth = 0;
18
+ let quote;
19
+ for (let i = argStart; i < source.length; i++) {
20
+ const ch = source[i];
21
+ if (quote) {
22
+ if (ch === '\\') {
23
+ i++; // skip the escaped character, whatever it is
24
+ continue;
25
+ }
26
+ if (ch === quote)
27
+ quote = undefined;
28
+ continue;
29
+ }
30
+ if (ch === '"' || ch === "'") {
31
+ quote = ch;
32
+ continue;
33
+ }
34
+ if (ch === '(' || ch === '[' || ch === '{') {
35
+ depth++;
36
+ continue;
37
+ }
38
+ if (ch === ')' || ch === ']' || ch === '}') {
39
+ if (depth === 0 && ch === ')') {
40
+ return source.slice(argStart, i);
41
+ }
42
+ depth--;
43
+ continue;
44
+ }
45
+ if (ch === ',' && depth === 0) {
46
+ return source.slice(argStart, i);
47
+ }
48
+ }
49
+ return undefined;
50
+ }
51
+ /**
52
+ * Whether `text` is EXACTLY one quoted string literal with nothing else
53
+ * around it (only leading/trailing whitespace) — the one shape this scan
54
+ * trusts as "a literal URL", per this module's top doc comment. Anything
55
+ * else (a `..` concatenation, a bare identifier, a function call, a second
56
+ * literal joined to the first) fails this and is reported as unknown
57
+ * rather than guessed at.
58
+ */
59
+ const SOLE_STRING_LITERAL = /^\s*(["'])((?:\\.|(?!\1)[^\\])*)\1\s*$/;
60
+ function literalStringValue(argument) {
61
+ const match = SOLE_STRING_LITERAL.exec(argument);
62
+ if (!match)
63
+ return undefined;
64
+ // Unescape only the two escapes Lua string literals actually need for a
65
+ // URL to round-trip (`\\` and the literal's own quote character) — this
66
+ // is a best-effort textual scan, not a full Lua string-literal decoder;
67
+ // any other backslash escape is left as-is (harmless: it would simply
68
+ // fail `new URL(...)` below, falling through to "not a literal we can
69
+ // use" as intended for `hasUnknownHosts`, rather than corrupting a URL
70
+ // that didn't have JavaScript-different escapes to begin with).
71
+ return (match[2] ?? '').replace(/\\(.)/g, '$1');
72
+ }
73
+ /** Bare, lowercased hostname from a URL string, or `undefined` if it doesn't parse as an absolute URL. */
74
+ function hostnameOf(url) {
75
+ try {
76
+ return new URL(url).hostname.toLowerCase();
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ /**
83
+ * Scans one script block's Lua source for `net.*` call sites and folds
84
+ * their findings into `hosts`/`markUnknown` — see this module's top doc
85
+ * comment for the literal-only matching rule.
86
+ */
87
+ function scanForHosts(code, hosts, markUnknown) {
88
+ CALL_SITE_PATTERN.lastIndex = 0;
89
+ let match;
90
+ while ((match = CALL_SITE_PATTERN.exec(code)) !== null) {
91
+ const argStart = match.index + match[0].length;
92
+ const argument = extractFirstArgument(code, argStart);
93
+ if (argument === undefined) {
94
+ markUnknown();
95
+ continue;
96
+ }
97
+ const literal = literalStringValue(argument);
98
+ if (literal === undefined) {
99
+ // Present but not a sole literal -- a concatenation, a variable, a
100
+ // nested call, ... exactly the "cannot resolve statically" case.
101
+ markUnknown();
102
+ continue;
103
+ }
104
+ const host = hostnameOf(literal);
105
+ if (host === undefined) {
106
+ // Syntactically a literal, but not a URL `new URL()` can parse
107
+ // (e.g. a bare path or template-ish string) -- still unknown to us.
108
+ markUnknown();
109
+ continue;
110
+ }
111
+ hosts.add(host);
112
+ }
113
+ }
114
+ /**
115
+ * Bare, already-lowercased hostnames every script block in `scripts`
116
+ * declares via its own `permissions` fence attribute
117
+ * (`@markii/core`'s `ScriptBlock.permissions`), deduplicated, in
118
+ * first-seen (document, then per-script list) order. Mirrors
119
+ * `./bundle-run.ts`'s `manifestNetHosts` for the bundle-manifest
120
+ * equivalent: this is DECLARED INTENT only, exactly like a manifest's
121
+ * `permissions.net` — `scanRunRequirementHosts` above stays the ONLY
122
+ * source of the hostnames a run is actually prompted for. A caller
123
+ * compares this against the scanned set with
124
+ * `./bundle-run.ts`'s `netDeclarationDiagnostics` (the same
125
+ * declared-vs-scanned diff a bundle manifest already gets), and may show
126
+ * it next to the scanned hosts in a grant prompt as "declares: ...".
127
+ */
128
+ export function scriptDeclaredHosts(scripts) {
129
+ const hosts = new Set();
130
+ for (const block of scripts) {
131
+ for (const host of block.permissions ?? []) {
132
+ hosts.add(host);
133
+ }
134
+ }
135
+ return [...hosts];
136
+ }
137
+ function toGrantClosureScript(block) {
138
+ const script = {
139
+ name: block.name,
140
+ lang: block.lang,
141
+ code: block.code,
142
+ };
143
+ if (block.src !== undefined)
144
+ script.src = block.src;
145
+ return script;
146
+ }
147
+ /**
148
+ * Parses `text` and returns its script blocks, in both the raw
149
+ * `@markii/core` `ScriptBlock` shape and the `GrantClosureScript` shape
150
+ * `computeGrantKey` hashes. Split out from `extractRunRequirements` (below)
151
+ * so a caller that needs to resolve `src=` script content BEFORE scanning
152
+ * for hosts — a bundle run, which must read the referenced file out of its
153
+ * snapshot first — can do so without parsing the document twice.
154
+ */
155
+ export function extractScriptBlocks(text) {
156
+ const tree = parse(text);
157
+ const scripts = extractScripts(tree);
158
+ return { scripts, grantScripts: scripts.map(toGrantClosureScript) };
159
+ }
160
+ /**
161
+ * Scans `scripts` for statically-resolvable `net.*` hosts — an INLINE
162
+ * block's own `code`, or (when `resolveScriptSource` is given and resolves
163
+ * it) a `src=` block's referenced source. A `src=` block with no resolution
164
+ * available folds into `hasUnknownHosts`, same as this module has always
165
+ * done for a bare `.mk.md` document's `src=` references (their source
166
+ * genuinely isn't in `text` at all).
167
+ */
168
+ export function scanRunRequirementHosts(scripts, resolveScriptSource) {
169
+ const hosts = new Set();
170
+ let hasUnknownHosts = false;
171
+ const markUnknown = () => {
172
+ hasUnknownHosts = true;
173
+ };
174
+ for (const block of scripts) {
175
+ if (block.src !== undefined) {
176
+ const resolved = resolveScriptSource?.(block);
177
+ if (resolved === undefined) {
178
+ // The referenced file's source isn't available to this scan --
179
+ // its net usage, if any, is entirely unknown. See this module's
180
+ // top doc comment.
181
+ markUnknown();
182
+ continue;
183
+ }
184
+ scanForHosts(resolved, hosts, markUnknown);
185
+ continue;
186
+ }
187
+ scanForHosts(block.code, hosts, markUnknown);
188
+ }
189
+ return { hosts: [...hosts], hasUnknownHosts };
190
+ }
191
+ /**
192
+ * Computes `RunRequirements` for `text` — parses it with `@markii/core`'s
193
+ * `parse`, pulls its script blocks with `extractScripts`, and scans each
194
+ * INLINE block's own source for statically-resolvable `net.*` hosts. Pure:
195
+ * no I/O, no `vscode`, safe to call on every keystroke if a future slice
196
+ * wants a live preview of what a run would ask for.
197
+ *
198
+ * `resolveScriptSource`, when given, lets a `src=` block's hosts be
199
+ * resolved too (a bundle run passes one backed by its snapshot); omitted,
200
+ * every `src=` block folds into `hasUnknownHosts` exactly as before this
201
+ * option existed.
202
+ */
203
+ export function extractRunRequirements(text, resolveScriptSource) {
204
+ const { scripts, grantScripts } = extractScriptBlocks(text);
205
+ const { hosts, hasUnknownHosts } = scanRunRequirementHosts(scripts, resolveScriptSource);
206
+ return {
207
+ scripts,
208
+ hosts,
209
+ hasUnknownHosts,
210
+ grantScripts,
211
+ };
212
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A `@markii/bundle` `BundleStorage` backed by a plain in-memory snapshot
3
+ * (a `path -> bytes` record) — never a live zip handle or a disk path. This
4
+ * is what the worker (`./worker-entry.ts`) hands to `@markii/bundle`'s
5
+ * `createScriptView`, so a run's `bundle.read`/`write`/`exists` calls never
6
+ * touch anything outside the bytes the host chose to include in the
7
+ * snapshot (`./bundle-run.ts`'s `buildBundleSnapshot`).
8
+ *
9
+ * `write` is reachable only through `ScriptView` (`@markii/bundle`), which
10
+ * already gates every write with `isWriteAllowed` before ever calling
11
+ * here — this storage does not re-derive that policy, matching
12
+ * `./storage.ts`'s own division of labor (`ScriptView` decides WHETHER a
13
+ * path may be written; a `BundleStorage` only knows HOW). What this module
14
+ * DOES enforce, unconditionally, is the same path-jail every
15
+ * `BundleStorage` implementation must (`@markii/bundle`'s own doc comment
16
+ * on `BundleStorage`): every path is normalized via `normalizeBundlePath`
17
+ * before it ever touches the underlying `Map`.
18
+ */
19
+ import type { BundleStorage } from '@markii/bundle';
20
+ export interface SnapshotStorage extends BundleStorage {
21
+ /** The storage's current contents, after any writes a run performed — used by the host to extract `.cache/` output once a run finishes. */
22
+ currentFiles(): Record<string, Uint8Array>;
23
+ }
24
+ /** Builds a `BundleStorage` over `initialFiles`, copying it so the caller's own object is never mutated by a later write. */
25
+ export declare function createSnapshotStorage(initialFiles: Record<string, Uint8Array>): SnapshotStorage;
@@ -0,0 +1,31 @@
1
+ import { BundlePathError, normalizeBundlePath } from '@markii/bundle';
2
+ function normalizeOrThrow(path) {
3
+ const result = normalizeBundlePath(path);
4
+ if (!result.ok) {
5
+ throw new BundlePathError(path, result.reason);
6
+ }
7
+ return result.path;
8
+ }
9
+ /** Builds a `BundleStorage` over `initialFiles`, copying it so the caller's own object is never mutated by a later write. */
10
+ export function createSnapshotStorage(initialFiles) {
11
+ const map = new Map(Object.entries(initialFiles));
12
+ return {
13
+ async read(path) {
14
+ return map.get(normalizeOrThrow(path));
15
+ },
16
+ async write(path, data) {
17
+ map.set(normalizeOrThrow(path), data);
18
+ },
19
+ async list() {
20
+ return [...map.keys()].sort();
21
+ },
22
+ async exists(path) {
23
+ return map.has(normalizeOrThrow(path));
24
+ },
25
+ async size(path) {
26
+ const data = map.get(normalizeOrThrow(path));
27
+ return data === undefined ? undefined : data.length;
28
+ },
29
+ currentFiles: () => Object.fromEntries(map),
30
+ };
31
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The tiny, vscode-free transform that turns a document's PERSISTED value
3
+ * store (`./run-flow.ts`'s `readPersistedValues`) into the value store the
4
+ * host re-posts to the webview when a note is (re)opened — GitHub issue #11,
5
+ * gap 1. "Opening a note ... showing its last data marked as stale where
6
+ * appropriate" (docs/spec.md, docs/scripting.md): a reopened monitoring note
7
+ * renders its last figures instantly and offline, visibly stale, before (or
8
+ * without) any re-run.
9
+ *
10
+ * The only transform is on `status`: a value that was `'fresh'` becomes
11
+ * `'stale'` (it is last-known data, not the product of the current session's
12
+ * run); a value that is already `'stale'` stays stale; an `'error'`/
13
+ * `'missing'` value is left untouched (there is nothing usable to mark
14
+ * stale — the consuming component shows its own error/empty state). Every
15
+ * other field is passed through verbatim, and the raw `error` text was
16
+ * already stripped before these values were ever persisted
17
+ * (`./run-flow.ts`'s `scrubValuesForWire`), so this never has to scrub again.
18
+ */
19
+ import type { StoredValue } from '@markii/runtime';
20
+ /**
21
+ * A copy of `persisted` with every `'fresh'` value demoted to `'stale'` —
22
+ * see this module's doc comment. Returns a fresh object; never mutates the
23
+ * input (which may be the caller's live persisted snapshot).
24
+ */
25
+ export declare function staleValuesForRehydration(persisted: Record<string, StoredValue>): Record<string, StoredValue>;
@@ -0,0 +1,32 @@
1
+ /** The `status` a persisted value is shown with on rehydration — `'fresh'` demotes to `'stale'`; everything else is unchanged. */
2
+ function staleStatus(status) {
3
+ return status === 'fresh' ? 'stale' : status;
4
+ }
5
+ /**
6
+ * A copy of `persisted` with every `'fresh'` value demoted to `'stale'` —
7
+ * see this module's doc comment. Returns a fresh object; never mutates the
8
+ * input (which may be the caller's live persisted snapshot).
9
+ */
10
+ export function staleValuesForRehydration(persisted) {
11
+ const entries = [];
12
+ for (const [name, value] of Object.entries(persisted)) {
13
+ // A persisted store is only guarded down to "plain object" when it is
14
+ // read back (`./run-flow.ts`'s `isStoredValueRecord`), so a corrupt or
15
+ // foreign ENTRY reaches here unchecked. Reading `.status` off a `null`
16
+ // entry would throw, and this transform runs on the host before the
17
+ // protocol guard ever sees the values, so the throw would land in the
18
+ // rehydration path rather than being rejected on the wire. Skip an
19
+ // unusable entry instead: a value that cannot be understood is exactly
20
+ // a value with nothing to re-show.
21
+ if (typeof value !== 'object' || value === null)
22
+ continue;
23
+ entries.push([name, { ...value, status: staleStatus(value.status) }]);
24
+ }
25
+ // `Object.fromEntries`, not `out[name] = ...`: a plain assignment of the
26
+ // name `__proto__` hits `Object.prototype`'s setter, which sets the
27
+ // object's prototype and creates NO own property, silently dropping that
28
+ // value. `__proto__` is a legal script name (docs/spec.md's
29
+ // `[A-Za-z_][A-Za-z0-9_-]*`), so this is reachable from an ordinary note.
30
+ // `fromEntries` defines each key as an own property, `__proto__` included.
31
+ return Object.fromEntries(entries);
32
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The Web Worker isolate entry, for a host that cannot create a worker
3
+ * thread. An Obsidian plugin is the case that forced it: its Electron
4
+ * renderer rejects `node:worker_threads` outright ("The V8 platform used by
5
+ * this instance of Node does not support creating Workers"), and forking a
6
+ * Node child through `ELECTRON_RUN_AS_NODE` was measured failing there too.
7
+ *
8
+ * Everything a run actually DOES lives in `./run-job.ts` and is shared with
9
+ * the Node entry, so the sandbox, the tier gate, the marshaling limits, and
10
+ * the failure classification cannot drift between hosts. This file supplies
11
+ * only the two things a Web Worker has to do differently:
12
+ *
13
+ * 1. Network goes through `./net-bridge.ts` to the host, which still has
14
+ * Node and runs the same pinned provider as before. The isolate itself
15
+ * has no network stack, which makes the allowlist unbypassable from
16
+ * inside it.
17
+ * 2. wasmoon's `glue.wasm` arrives as a URL on the job, because there is
18
+ * no `__dirname` here to find it beside the bundle.
19
+ *
20
+ * The protocol is the same one-job-in, one-result-out shape `run-host.ts`
21
+ * expects, with net traffic interleaved: any message that is not a net
22
+ * reply is the job.
23
+ */
24
+ import { type RunJob } from './run-job.js';
25
+ /** The Web Worker global surface this entry uses, named so the file typechecks without DOM lib types. */
26
+ interface WorkerGlobal {
27
+ postMessage: (message: unknown) => void;
28
+ addEventListener: (type: 'message', listener: (event: {
29
+ data: unknown;
30
+ }) => void) => void;
31
+ }
32
+ /**
33
+ * A job for this entry additionally carries where `glue.wasm` can be
34
+ * fetched from — a blob URL the host minted from the file on disk. Absent,
35
+ * wasmoon falls back to its own resolution, which in a Web Worker will not
36
+ * find anything; the host is expected to always send it.
37
+ */
38
+ export interface BrowserRunJob extends RunJob {
39
+ wasmUri?: string;
40
+ }
41
+ declare function start(scope: WorkerGlobal): void;
42
+ export { start as startBrowserWorker };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The Web Worker isolate entry, for a host that cannot create a worker
3
+ * thread. An Obsidian plugin is the case that forced it: its Electron
4
+ * renderer rejects `node:worker_threads` outright ("The V8 platform used by
5
+ * this instance of Node does not support creating Workers"), and forking a
6
+ * Node child through `ELECTRON_RUN_AS_NODE` was measured failing there too.
7
+ *
8
+ * Everything a run actually DOES lives in `./run-job.ts` and is shared with
9
+ * the Node entry, so the sandbox, the tier gate, the marshaling limits, and
10
+ * the failure classification cannot drift between hosts. This file supplies
11
+ * only the two things a Web Worker has to do differently:
12
+ *
13
+ * 1. Network goes through `./net-bridge.ts` to the host, which still has
14
+ * Node and runs the same pinned provider as before. The isolate itself
15
+ * has no network stack, which makes the allowlist unbypassable from
16
+ * inside it.
17
+ * 2. wasmoon's `glue.wasm` arrives as a URL on the job, because there is
18
+ * no `__dirname` here to find it beside the bundle.
19
+ *
20
+ * The protocol is the same one-job-in, one-result-out shape `run-host.ts`
21
+ * expects, with net traffic interleaved: any message that is not a net
22
+ * reply is the job.
23
+ */
24
+ import { isRunJob, resultForInternalError, runJob, } from './run-job.js';
25
+ import { createNetBridge } from './net-bridge-worker.js';
26
+ function start(scope) {
27
+ const bridge = createNetBridge((message) => {
28
+ scope.postMessage(message);
29
+ });
30
+ let running = false;
31
+ scope.addEventListener('message', (event) => {
32
+ const message = event.data;
33
+ // Net replies arrive throughout the run; the bridge ignores anything
34
+ // that is not one of its own.
35
+ bridge.handleMessage(message);
36
+ if (running)
37
+ return;
38
+ if (typeof message === 'object' &&
39
+ message !== null &&
40
+ message.kind === 'markii:net-reply') {
41
+ return;
42
+ }
43
+ running = true;
44
+ void (async () => {
45
+ if (!isRunJob(message)) {
46
+ scope.postMessage(resultForInternalError(new Error('worker received a malformed job message'), {}));
47
+ return;
48
+ }
49
+ const job = message;
50
+ let result;
51
+ try {
52
+ result = await runJob(job, {
53
+ createNet: () => bridge.provider,
54
+ wasmUri: job.wasmUri,
55
+ // GitHub issue #35: the same per-script message the Node entry
56
+ // sends, on the same channel this worker posts its result on, so
57
+ // ordering (progress before result) holds here too. Built in
58
+ // `./run-job.ts`, never here.
59
+ postProgress: (progress) => {
60
+ scope.postMessage(progress);
61
+ },
62
+ });
63
+ }
64
+ catch (err) {
65
+ result = resultForInternalError(err, job.cacheSnapshot ?? {});
66
+ }
67
+ scope.postMessage(result);
68
+ })();
69
+ });
70
+ }
71
+ // Guarded so importing this module in a test (or anywhere that is not a
72
+ // worker) is inert rather than throwing on a missing `self`.
73
+ if (typeof self !== 'undefined' &&
74
+ typeof self.addEventListener === 'function') {
75
+ start(self);
76
+ }
77
+ export { start as startBrowserWorker };
@@ -0,0 +1 @@
1
+ export type { RunJob, RunFailure, RunProgress, RunResult } from './run-job.js';
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The `worker_thread` entry point for slice 1 of the extension's v2 Run
3
+ * arc (docs: GitHub issue #1's locked design comment). This file is
4
+ * vscode-free — it never imports `vscode` and knows nothing about VS
5
+ * Code's API — so it can be unit-tested directly with Vitest (spawned as a
6
+ * REAL `node:worker_threads` worker, exercising the real wasmoon sandbox)
7
+ * and, unmodified, be the bundled worker `esbuild.config.mjs` produces for
8
+ * the packaged extension.
9
+ *
10
+ * ## Why a whole worker per run
11
+ *
12
+ * The design (docs/security.md's isolate requirement) is "one ephemeral
13
+ * worker per run": the host spawns a fresh thread, this file boots
14
+ * `@markii/lua` + `@markii/runtime` once, runs exactly one batch, posts
15
+ * back one progress message per script and then exactly one result
16
+ * message, and the whole thread is expected to be
17
+ * torn down by the host afterward (`run-host.ts`) regardless of how this
18
+ * run went. That is what makes the external wall-clock watchdog
19
+ * (`worker.terminate()`) an unconditional, always-available kill switch:
20
+ * it can never be blocked by anything this file's own code does, because
21
+ * `terminate()` acts on the OS/V8 thread itself, not on anything
22
+ * cooperative running inside it.
23
+ *
24
+ * ## Never an unhandled rejection
25
+ *
26
+ * Every path through `main()` below is wrapped so that ANY failure —
27
+ * a malformed job message, `parse()` throwing on pathological input,
28
+ * `runDocumentScripts` itself misbehaving — becomes an ordinary result
29
+ * message carrying a synthetic failure, never a thrown/rejected error that
30
+ * could surface as an "Unhandled Promise Rejection" on the worker thread
31
+ * (which Node would otherwise report noisily, or in the worst case treat
32
+ * as fatal depending on the host's process-wide `--unhandled-rejections`
33
+ * setting). `run-host.ts` therefore never needs an `unhandledRejection`
34
+ * listener on the worker to stay safe.
35
+ */
36
+ import { parentPort } from 'node:worker_threads';
37
+ import { existsSync } from 'node:fs';
38
+ import * as path from 'node:path';
39
+ import { netProviderDenial } from '@markii/lua';
40
+ import { createNetProvider } from './net-provider.js';
41
+ import { isRunJob, resultForInternalError, runJob } from './run-job.js';
42
+ /**
43
+ * Resolves the `wasmUri` to hand `createLuaExecutor` (forwarded to
44
+ * `@markii/lua`'s `runScript` -> `createEmptyLuaEngine` -> wasmoon's
45
+ * `LuaFactory`). In the BUNDLED extension, `esbuild.config.mjs`'s worker
46
+ * build copies wasmoon's `glue.wasm` next to this file's compiled output
47
+ * (`dist/run/glue.wasm`) specifically so this lookup succeeds; in
48
+ * dev/Vitest (this file run straight from `src/run/`, no copy step has
49
+ * ever run), the file is absent and `undefined` is returned, letting
50
+ * wasmoon fall back to its own default Node resolution (the real
51
+ * `node_modules/wasmoon/dist/glue.wasm`) — see `@markii/lua`'s
52
+ * `createEmptyLuaEngine` doc comment for why `undefined` is always safe to
53
+ * pass here.
54
+ */
55
+ function resolveWasmUri() {
56
+ const candidate = path.join(__dirname, 'glue.wasm');
57
+ return existsSync(candidate) ? candidate : undefined;
58
+ }
59
+ async function main() {
60
+ if (!parentPort) {
61
+ // Not actually running as a worker thread (e.g. required directly by
62
+ // mistake) — nothing to do, and nothing to post a result to.
63
+ return;
64
+ }
65
+ const port = parentPort;
66
+ port.once('message', (message) => {
67
+ void (async () => {
68
+ if (!isRunJob(message)) {
69
+ port.postMessage(resultForInternalError(new Error('worker received a malformed job message'), {}));
70
+ return;
71
+ }
72
+ try {
73
+ const result = await runJob(message, {
74
+ // GitHub issue #35: one message per script, on the SAME port the
75
+ // final result goes out on, so the host receives them in order
76
+ // and always ahead of the result. Identical to what
77
+ // `./worker-entry-browser.ts` does; the message itself is built
78
+ // in `./run-job.ts` so the two cannot drift.
79
+ postProgress: (progress) => {
80
+ port.postMessage(progress);
81
+ },
82
+ // The Lua brand is supplied HERE, where the runtime is already
83
+ // present, rather than imported by the provider itself — see
84
+ // `./net-provider.ts`'s doc comment.
85
+ createNet: (allowlist, maxFetchBytes, policy) => createNetProvider(allowlist, maxFetchBytes, policy, netProviderDenial),
86
+ wasmUri: resolveWasmUri(),
87
+ });
88
+ port.postMessage(result);
89
+ }
90
+ catch (err) {
91
+ port.postMessage(resultForInternalError(err, message.cacheSnapshot ?? {}));
92
+ }
93
+ })();
94
+ });
95
+ }
96
+ void main();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `ValuesFailure` is the wire-protocol shape a host uses to report one
3
+ * script's failure back to its renderer/webview: the script's name and a
4
+ * closed `FailureKind`, never the raw error message (the rendered page
5
+ * shows quiet markers, never error dumps — see AGENTS.md's cleanliness
6
+ * principle). Defined here, in the shared host layer, so every host
7
+ * (`apps/vscode`, and any future host) and this package's own `run-flow.ts`
8
+ * agree on a single definition instead of drifting copies; a consuming
9
+ * app's own wire-protocol module (e.g. `apps/vscode/src/protocol.ts`) is
10
+ * expected to import and re-export this type rather than restate it.
11
+ */
12
+ import type { FailureKind } from '@markii/runtime';
13
+ /** One script's outcome, reduced to just its name and failure kind — never the raw error message. */
14
+ export interface ValuesFailure {
15
+ readonly name: string;
16
+ readonly kind: FailureKind;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The one way a host folds a single arriving value into the value store its
3
+ * preview is rendering from (GitHub issue #35).
4
+ *
5
+ * A run reports its values one at a time, in document order, while it is
6
+ * still going. Each arrival has to replace exactly one name and leave every
7
+ * other name exactly as it was — that is what makes the bound component go
8
+ * fresh while the rest of the note stays visibly stale, instead of the
9
+ * whole page flipping at the end of the batch.
10
+ *
11
+ * It lives here, in the shared host layer, because both hosts do it: the VS
12
+ * Code webview folds a `value` wire message into its React state, and the
13
+ * Obsidian view folds a callback's value into the store it re-renders from.
14
+ * Two copies of a merge this small still drift, and the `__proto__` rule
15
+ * below is exactly the kind of detail one copy would quietly lose.
16
+ *
17
+ * Environment-free on purpose (no `node:*`, no React), so the main entry
18
+ * can export it and a browser bundle can import it.
19
+ */
20
+ import type { StoredValue } from '@markii/runtime';
21
+ /**
22
+ * `values` with `name` set to `value`, as a NEW object — the input is never
23
+ * mutated, so a caller holding the previous store (React state, a view's
24
+ * last-rendered snapshot) can still compare against it.
25
+ *
26
+ * A name already present keeps its POSITION and takes the new value; a new
27
+ * name is appended. Order is not semantically meaningful to a renderer, but
28
+ * a store whose keys reshuffle on every value would make any diff of two
29
+ * snapshots harder to read than it needs to be.
30
+ *
31
+ * Built through `Object.fromEntries` rather than `{...values, [name]: value}`
32
+ * because a script may legitimately be named `__proto__` (docs/spec.md's
33
+ * name grammar allows it): a computed assignment of that key hits
34
+ * `Object.prototype`'s setter, which sets the object's prototype and
35
+ * creates no own property at all, silently dropping the value. The same
36
+ * guard is in `run/run-flow.ts`'s `mergePersistedValues` and
37
+ * `run/stale-values.ts`, for the same reason.
38
+ */
39
+ export declare function mergeArrivingValue(values: Readonly<Record<string, StoredValue>> | undefined, name: string, value: StoredValue): Record<string, StoredValue>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `values` with `name` set to `value`, as a NEW object — the input is never
3
+ * mutated, so a caller holding the previous store (React state, a view's
4
+ * last-rendered snapshot) can still compare against it.
5
+ *
6
+ * A name already present keeps its POSITION and takes the new value; a new
7
+ * name is appended. Order is not semantically meaningful to a renderer, but
8
+ * a store whose keys reshuffle on every value would make any diff of two
9
+ * snapshots harder to read than it needs to be.
10
+ *
11
+ * Built through `Object.fromEntries` rather than `{...values, [name]: value}`
12
+ * because a script may legitimately be named `__proto__` (docs/spec.md's
13
+ * name grammar allows it): a computed assignment of that key hits
14
+ * `Object.prototype`'s setter, which sets the object's prototype and
15
+ * creates no own property at all, silently dropping the value. The same
16
+ * guard is in `run/run-flow.ts`'s `mergePersistedValues` and
17
+ * `run/stale-values.ts`, for the same reason.
18
+ */
19
+ export function mergeArrivingValue(values, name, value) {
20
+ const entries = [];
21
+ let replaced = false;
22
+ for (const [key, existing] of Object.entries(values ?? {})) {
23
+ if (key === name) {
24
+ entries.push([key, value]);
25
+ replaced = true;
26
+ continue;
27
+ }
28
+ entries.push([key, existing]);
29
+ }
30
+ if (!replaced)
31
+ entries.push([name, value]);
32
+ return Object.fromEntries(entries);
33
+ }