@markii/host 0.0.1 → 0.17.0-next.3

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 (193) hide show
  1. package/README.md +20 -2
  2. package/dist/browser-worker.js +639 -0
  3. package/dist/browser.d.ts +61 -0
  4. package/dist/browser.js +172 -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 +218 -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 +159 -0
  86. package/dist/host/conformance-runner.js +632 -0
  87. package/dist/host/create-host.d.ts +251 -0
  88. package/dist/host/create-host.js +329 -0
  89. package/dist/host/editor-behavior.d.ts +153 -0
  90. package/dist/host/editor-behavior.js +223 -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 +111 -0
  94. package/dist/host/export-behavior.js +257 -0
  95. package/dist/host/labels.d.ts +23 -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 +62 -0
  99. package/dist/host/pack-install.d.ts +87 -0
  100. package/dist/host/pack-install.js +186 -0
  101. package/dist/host/pack-load.d.ts +71 -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 +37 -0
  108. package/dist/host/refresh-interval.js +59 -0
  109. package/dist/host/run-behavior.d.ts +88 -0
  110. package/dist/host/run-behavior.js +189 -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 +69 -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-worker.js +20657 -0
  126. package/dist/node.d.ts +6 -0
  127. package/dist/node.js +13 -0
  128. package/dist/package.json +3 -0
  129. package/dist/packs/discover.d.ts +119 -0
  130. package/dist/packs/discover.js +266 -0
  131. package/dist/packs/pack-diagnostics.d.ts +79 -0
  132. package/dist/packs/pack-diagnostics.js +103 -0
  133. package/dist/packs/pack-paths.d.ts +53 -0
  134. package/dist/packs/pack-paths.js +105 -0
  135. package/dist/packs/pack-render-registry.d.ts +82 -0
  136. package/dist/packs/pack-render-registry.js +182 -0
  137. package/dist/packs/pack-scripts.d.ts +20 -0
  138. package/dist/packs/pack-scripts.js +96 -0
  139. package/dist/packs/pack-trust.d.ts +57 -0
  140. package/dist/packs/pack-trust.js +111 -0
  141. package/dist/packs/prebuilt.d.ts +49 -0
  142. package/dist/packs/prebuilt.js +97 -0
  143. package/dist/path-util.d.ts +24 -0
  144. package/dist/path-util.js +82 -0
  145. package/dist/run/browser-isolate.d.ts +45 -0
  146. package/dist/run/browser-isolate.js +85 -0
  147. package/dist/run/bundle-document.d.ts +29 -0
  148. package/dist/run/bundle-document.js +40 -0
  149. package/dist/run/bundle-run.d.ts +188 -0
  150. package/dist/run/bundle-run.js +232 -0
  151. package/dist/run/grant-flow.d.ts +232 -0
  152. package/dist/run/grant-flow.js +390 -0
  153. package/dist/run/ip-address.d.ts +41 -0
  154. package/dist/run/ip-address.js +255 -0
  155. package/dist/run/isolate.d.ts +59 -0
  156. package/dist/run/isolate.js +22 -0
  157. package/dist/run/lua-resolver.d.ts +21 -0
  158. package/dist/run/lua-resolver.js +63 -0
  159. package/dist/run/net-bridge-worker.d.ts +26 -0
  160. package/dist/run/net-bridge-worker.js +66 -0
  161. package/dist/run/net-bridge.d.ts +56 -0
  162. package/dist/run/net-bridge.js +59 -0
  163. package/dist/run/net-pinning.d.ts +82 -0
  164. package/dist/run/net-pinning.js +137 -0
  165. package/dist/run/net-provider.d.ts +72 -0
  166. package/dist/run/net-provider.js +303 -0
  167. package/dist/run/node-isolate.d.ts +22 -0
  168. package/dist/run/node-isolate.js +68 -0
  169. package/dist/run/run-flow.d.ts +186 -0
  170. package/dist/run/run-flow.js +323 -0
  171. package/dist/run/run-host.d.ts +107 -0
  172. package/dist/run/run-host.js +241 -0
  173. package/dist/run/run-job.d.ts +130 -0
  174. package/dist/run/run-job.js +279 -0
  175. package/dist/run/run-progress.d.ts +55 -0
  176. package/dist/run/run-progress.js +42 -0
  177. package/dist/run/run-trace.d.ts +44 -0
  178. package/dist/run/run-trace.js +36 -0
  179. package/dist/run/script-requirements.d.ts +98 -0
  180. package/dist/run/script-requirements.js +212 -0
  181. package/dist/run/snapshot-storage.d.ts +25 -0
  182. package/dist/run/snapshot-storage.js +31 -0
  183. package/dist/run/stale-values.d.ts +25 -0
  184. package/dist/run/stale-values.js +32 -0
  185. package/dist/run/worker-entry-browser.d.ts +62 -0
  186. package/dist/run/worker-entry-browser.js +172 -0
  187. package/dist/run/worker-entry.d.ts +1 -0
  188. package/dist/run/worker-entry.js +98 -0
  189. package/dist/values-failure.d.ts +17 -0
  190. package/dist/values-failure.js +1 -0
  191. package/dist/values-merge.d.ts +39 -0
  192. package/dist/values-merge.js +33 -0
  193. package/package.json +80 -4
@@ -0,0 +1,232 @@
1
+ /** The three bundle-relative prefixes a run's snapshot ever collects — see this module's top doc comment. */
2
+ const SNAPSHOT_PREFIXES = ['scripts/', '.cache/', 'assets/'];
3
+ /**
4
+ * Total byte budget for one run's bundle snapshot (across `scripts/` +
5
+ * `.cache/` + `assets/` combined). Mirrors the same order of magnitude as
6
+ * `bundle-resolve.ts`'s `DEFAULT_MAX_EMBEDDED_ASSET_BYTES` (20MB) — a
7
+ * personal note bundle's scripts and cache are typically tiny; this budget
8
+ * exists so a hostile or oversized bundle degrades quietly (files beyond
9
+ * the cap are simply absent from the snapshot, exactly like a path the
10
+ * snapshot never collected) rather than exhausting worker/host memory or
11
+ * ballooning the `postMessage` structured-clone payload.
12
+ */
13
+ export const DEFAULT_MAX_BUNDLE_SNAPSHOT_BYTES = 20 * 1024 * 1024;
14
+ /**
15
+ * Per-file cap applied on top of the total budget (C-1, PENTEST-REPORT
16
+ * follow-up): a single huge file could otherwise consume the ENTIRE total
17
+ * budget by itself, and — before this fix — would be read into memory in
18
+ * full before the total-budget check ever saw it. Sized the same as the
19
+ * total budget by default; a caller wanting a stricter per-file limit can
20
+ * override it independently via `maxFileBytes`.
21
+ */
22
+ export const DEFAULT_MAX_BUNDLE_SNAPSHOT_FILE_BYTES = DEFAULT_MAX_BUNDLE_SNAPSHOT_BYTES;
23
+ /**
24
+ * Reads every file under `scripts/`, `.cache/`, and `assets/` out of
25
+ * `storage`, up to `maxTotalBytes` total. `storage.list()` already returns
26
+ * paths sorted, so which files get dropped once the budget is hit is
27
+ * deterministic rather than depending on iteration order.
28
+ */
29
+ export async function buildBundleSnapshot(storage, options = {}) {
30
+ const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_BUNDLE_SNAPSHOT_BYTES;
31
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_BUNDLE_SNAPSHOT_FILE_BYTES;
32
+ const allPaths = await storage.list();
33
+ const eligible = allPaths.filter((path) => SNAPSHOT_PREFIXES.some((prefix) => path.startsWith(prefix)));
34
+ const files = {};
35
+ let total = 0;
36
+ let truncated = false;
37
+ for (const path of eligible) {
38
+ // C-1: consult `size()` BEFORE ever calling `read()` — a file whose
39
+ // declared size alone would blow the per-file or remaining total budget
40
+ // is skipped without materializing its bytes at all, which is the whole
41
+ // point: a host-side `readFile` of a multi-gigabyte file (directory
42
+ // form) would otherwise allocate that much in the extension host before
43
+ // any cap ever saw it. `size()` returning `undefined` (path vanished
44
+ // between `list()` and here, or the storage form has no size for it) is
45
+ // treated exactly like the file being absent: a quiet skip, never a
46
+ // fallback to reading it blind.
47
+ const size = await storage.size(path);
48
+ if (size === undefined)
49
+ continue;
50
+ if (size > maxFileBytes || total + size > maxTotalBytes) {
51
+ truncated = true;
52
+ continue;
53
+ }
54
+ const bytes = await storage.read(path);
55
+ if (bytes === undefined)
56
+ continue;
57
+ // Defense in depth: re-check the actual byte length against the budget.
58
+ // A well-behaved `BundleStorage` never disagrees between `size()` and
59
+ // `read()`, but the total-budget accounting below must never trust
60
+ // `size()` blindly if it ever did.
61
+ if (total + bytes.length > maxTotalBytes) {
62
+ truncated = true;
63
+ continue;
64
+ }
65
+ total += bytes.length;
66
+ files[path] = bytes;
67
+ }
68
+ return { files, truncated };
69
+ }
70
+ const utf8Decoder = new TextDecoder('utf-8', { fatal: false });
71
+ /**
72
+ * F-1 fix: the `bundleModules` half of the grant closure `computeGrantKey`
73
+ * (`@markii/runtime`) hashes — resolved from `snapshot` (the same in-memory
74
+ * bundle snapshot a run's `ScriptView` is backed by, see this module's top
75
+ * doc comment) rather than left empty.
76
+ *
77
+ * Before this fix, `grant-flow.ts`'s `closureFrom` always passed
78
+ * `bundleModules: {}`, so a `src=scripts/etl.lua` block's `code` was `""`
79
+ * (@markii/core's `ScriptBlock` leaves it empty for a `src=` reference — the
80
+ * actual source lives in the referenced file) and the grant key never
81
+ * reflected that file's bytes at all: swapping the file's contents while
82
+ * the note's own text stayed byte-identical re-ran new code under an old,
83
+ * un-reprompted grant.
84
+ *
85
+ * Only `src=` targets are resolved here — `require()` (bundle-local or
86
+ * pack) stays unwired for the whole `.mkz` Run-path arc (see
87
+ * `grant-flow.ts`'s top doc comment), so a script's closure has no other
88
+ * bundle-local code to fold in yet. A `src=` path missing from `snapshot`
89
+ * (file deleted, or dropped by `buildBundleSnapshot`'s own budget) is
90
+ * simply omitted from the result — never thrown — so a note whose script
91
+ * references a missing file still gets a (now-narrower, but well-defined)
92
+ * grant key instead of failing the whole grant flow.
93
+ */
94
+ export function bundleModulesFromSnapshot(scripts, snapshot) {
95
+ const modules = {};
96
+ for (const script of scripts) {
97
+ if (script.src === undefined)
98
+ continue;
99
+ const bytes = snapshot[script.src];
100
+ if (bytes === undefined)
101
+ continue;
102
+ modules[script.src] = utf8Decoder.decode(bytes);
103
+ }
104
+ return modules;
105
+ }
106
+ /** Every `.cache/`-prefixed entry in `files`, unchanged — the shape a worker's `RunResult.cacheOut` carries back and a host persists. */
107
+ export function cacheFilesFrom(files) {
108
+ const out = {};
109
+ for (const [path, bytes] of Object.entries(files)) {
110
+ if (path.startsWith('.cache/'))
111
+ out[path] = bytes;
112
+ }
113
+ return out;
114
+ }
115
+ /**
116
+ * Overlays `persisted` (a prior run's `.cache/` output, read back from
117
+ * wherever the host keeps it) onto `base` (a freshly built snapshot),
118
+ * persisted entries winning on a path collision — the "seed the next run's
119
+ * snapshot from the persisted cache" half of the design. Used for the
120
+ * zip-form path, where the archive's own on-disk `.cache/` entries (if any)
121
+ * are stale the moment a run has written a persisted cache elsewhere (the
122
+ * host never rewrites the user's zip — see `docs` design comment).
123
+ */
124
+ export function withPersistedCache(base, persisted) {
125
+ return { ...base, ...persisted };
126
+ }
127
+ /** Bare, lowercased hostnames a manifest declares under `permissions.net.get`/`.post`, deduplicated. */
128
+ export function manifestNetHosts(manifest) {
129
+ const get = manifest.permissions?.net?.get ?? [];
130
+ const post = manifest.permissions?.net?.post ?? [];
131
+ return [...new Set([...get, ...post].map((host) => host.toLowerCase()))];
132
+ }
133
+ /**
134
+ * Consent unification (SECURITY-RELEVANT, GitHub issue #9 follow-up): a
135
+ * bundle's manifest `permissions.net` is DECLARED INTENT only. The static
136
+ * scan of the run's executable closure (`./script-requirements.ts`) is the
137
+ * ONLY source of the hostnames a run prompts for, for a bundle exactly as
138
+ * for a bare `.mk.md` document — the manifest's declaration never widens
139
+ * that prompt (a declared-but-unused host is never prompted for) and never
140
+ * narrows it (a host a script actually reaches is always prompted for,
141
+ * declared or not).
142
+ *
143
+ * A mismatch between the two is not silently absorbed: this pure function
144
+ * returns one diagnostics line per mismatched host, in both directions, for
145
+ * the host's diagnostics surface (docs/integration.md) — never the rendered
146
+ * page. Wording lives here, the one place both apps' adapters read it from,
147
+ * so it can never drift between VS Code's output channel and Obsidian's
148
+ * console/notice surface.
149
+ *
150
+ * Returns `[]` when the two sets agree (including when both are empty).
151
+ * Order: every declared-but-unused host first (in `declaredHosts` order),
152
+ * then every used-but-undeclared host (in `scannedHosts` order) — matching
153
+ * how `manifestNetHosts`/`extractRunRequirements` already dedupe and order
154
+ * their inputs, so this function does no case-folding or deduping of its
155
+ * own.
156
+ */
157
+ export function netDeclarationDiagnostics(declaredHosts, scannedHosts) {
158
+ const declared = new Set(declaredHosts);
159
+ const scanned = new Set(scannedHosts);
160
+ const lines = [];
161
+ for (const host of declaredHosts) {
162
+ if (!scanned.has(host)) {
163
+ lines.push(`The manifest declares net access to ${host}. No script in this run uses that host.`);
164
+ }
165
+ }
166
+ for (const host of scannedHosts) {
167
+ if (!declared.has(host)) {
168
+ lines.push(`A script in this run uses net access to ${host}. The manifest does not declare that host.`);
169
+ }
170
+ }
171
+ return lines;
172
+ }
173
+ /** The bundle-filesystem grants a manifest declares under `permissions.bundle` (`'read'` / `'write:.cache/'`), or `[]` when it declares none. */
174
+ export function manifestBundleFsGrants(manifest) {
175
+ return manifest.permissions?.bundle ?? [];
176
+ }
177
+ /**
178
+ * Cap on a zip-form bundle's persisted `.cache/` state (extension storage,
179
+ * keyed by bundle identity — see `preview-panel.ts`'s adapters). Mirrors
180
+ * `run-flow.ts`'s `MAX_CACHE_SNAPSHOT_BYTES` for the unrelated `cache.get`
181
+ * Memento snapshot: real script-written cache files are small, so this
182
+ * exists purely to guarantee a runaway/adversarial bundle can never grow
183
+ * `workspaceState` without bound. Oversize state is DROPPED wholesale, not
184
+ * partially persisted — see that constant's doc comment for why a partial
185
+ * write is worse than none.
186
+ */
187
+ export const MAX_PERSISTED_BUNDLE_CACHE_BYTES = 1_000_000;
188
+ /**
189
+ * Encodes `files` (a bundle run's `.cache/` output) for Memento storage, or
190
+ * `undefined` when it must be dropped instead — either it doesn't encode at
191
+ * all, or the encoded form exceeds `MAX_PERSISTED_BUNDLE_CACHE_BYTES`.
192
+ * Callers write `undefined` back to storage in that case, exactly like
193
+ * `run-flow.ts`'s `serializeCacheSnapshotIfSmallEnough`.
194
+ */
195
+ export function encodeBundleCacheForStorage(files) {
196
+ const encoded = {};
197
+ let total = 0;
198
+ for (const [path, bytes] of Object.entries(files)) {
199
+ total += bytes.length;
200
+ if (total > MAX_PERSISTED_BUNDLE_CACHE_BYTES)
201
+ return undefined;
202
+ encoded[path] = Buffer.from(bytes).toString('base64');
203
+ }
204
+ return encoded;
205
+ }
206
+ /**
207
+ * Decodes a value read back from a `vscode.Memento` into a `path -> bytes`
208
+ * cache map — never trusts the stored shape further than "every value is a
209
+ * string that decodes as base64"; a corrupt/foreign/hand-edited value
210
+ * degrades to an empty map (no persisted cache), matching this whole
211
+ * module's fail-safe posture (`grant-flow.ts`'s `isStoredGrant` takes the
212
+ * same stance for its own Memento reads).
213
+ */
214
+ export function decodeBundleCacheFromStorage(value) {
215
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
216
+ return {};
217
+ }
218
+ const out = {};
219
+ for (const [path, encoded] of Object.entries(value)) {
220
+ if (typeof encoded !== 'string')
221
+ continue;
222
+ try {
223
+ out[path] = new Uint8Array(Buffer.from(encoded, 'base64'));
224
+ }
225
+ catch {
226
+ // Skip a value that isn't valid base64 rather than failing the whole
227
+ // decode — one corrupt entry shouldn't take down every other cached
228
+ // file.
229
+ }
230
+ }
231
+ return out;
232
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Slice 2's grant/prompt flow (docs: GitHub issue #1's locked design
3
+ * comment) — vscode-free by design, same split as `./run-host.ts` and
4
+ * `./script-requirements.ts`: this module decides WHAT to prompt and WHAT
5
+ * to persist, but never calls `vscode.window.showInformationMessage` or
6
+ * `context.workspaceState` directly. The extension-host adapter
7
+ * (`extension.ts`/`preview-panel.ts` — the only files allowed to import
8
+ * `vscode`) supplies the actual prompt UI and Memento, both behind the thin
9
+ * interfaces below, so this file stays unit-testable with plain fakes.
10
+ *
11
+ * ## Keying
12
+ *
13
+ * `computeGrantKey` (`@markii/runtime`) hashes the note's executable
14
+ * closure — that closure is its own script blocks
15
+ * (`RunRequirements.grantScripts`) plus, for a bundle-backed run, the
16
+ * resolved source text of every `src=`-referenced script file
17
+ * (`GrantFlowRequirements.bundleModules`, F-1 fix), with `vaultModules`/
18
+ * `packs` left empty: `require` (bundle-local or pack) stays unwired for
19
+ * the whole `.mkz` Run-path arc (GitHub issue #9's design comment), so a
20
+ * bundle-backed run's closure has no OTHER bundle-local code to fold in
21
+ * yet beyond its `src=` targets. Any edit to any script's code — inline or
22
+ * `src=`-referenced — changes the hash, which is what makes "any code
23
+ * change re-prompts" fall out of the keying scheme for free rather than
24
+ * needing its own diffing logic here.
25
+ *
26
+ * The bundle-fs capability (`permissions.bundle`) is out of this module
27
+ * entirely (consent unification, SECURITY-RELEVANT): it needs no
28
+ * user-facing prompt, so it is never part of `GrantFlowRequirements`, the
29
+ * stored grant, or the grant key. `./run-flow.ts` passes the manifest's
30
+ * declared bundle-fs grants straight through to `spawnRun` — see its own
31
+ * comment on why that is still safe (path-jail + the read-only tier for
32
+ * auto/scheduled, both unaffected by this module).
33
+ *
34
+ * ## Storage
35
+ *
36
+ * One `{key, allowedHosts}` record per document, keyed by the document's
37
+ * URI string, all held under one Memento key (`GRANTS_STORAGE_KEY`) as a
38
+ * plain map. A stored record whose `key` no longer matches the freshly
39
+ * computed one is a MISS — this module never trusts a stale record's
40
+ * `allowedHosts`, even partially; the whole grant re-derives from scratch.
41
+ * A record is only ever WRITTEN when `allowedHosts` ends up non-empty — a
42
+ * full decline is never persisted, so it simply re-prompts next time (see
43
+ * `runGrantFlow`'s own comment on this, C-3) — and `clearGrantForDocument`
44
+ * removes a document's record outright, for the `markii.resetScriptGrants`
45
+ * command.
46
+ *
47
+ * ## The prompt shapes
48
+ *
49
+ * - One prompt per statically-known host (`promptHost`), worded exactly
50
+ * "This note's scripts can send data to <host>. Allow?" — see
51
+ * `hostPromptMessage`. Used only while the distinct safe host count is at
52
+ * or under `MAX_HOST_PROMPTS`; above that, `promptManyHosts` (below) takes
53
+ * over instead so a note can never turn one Run press into a wall of
54
+ * modals (the PROMPT-STORM fix — see `MAX_HOST_PROMPTS`'s doc comment).
55
+ * - `promptManyHosts`, exactly once, in place of the whole per-host loop
56
+ * when there are more than `MAX_HOST_PROMPTS` distinct safe hosts: one
57
+ * all-or-nothing gate ("This note requests network access to many hosts
58
+ * (N). Allow all or deny all?", `manyHostsPromptMessage`) rather than N
59
+ * separate dialogs.
60
+ * - Exactly one extra prompt (`promptUnknownHosts`,
61
+ * `UNKNOWN_HOSTS_PROMPT_MESSAGE`) when the closure could reach hosts that
62
+ * cannot be listed in advance (`RunRequirements.hasUnknownHosts`, or a
63
+ * host string this module refuses to render — see `isSafeHostForPrompt`).
64
+ * Accepting it does NOT add anything to `allowedHosts` — there is no host
65
+ * name to grant — it is a consent gate: declining it clears every
66
+ * already-collected per-host answer too, since a script that can reach
67
+ * addresses it never named cannot be trusted to only use the hosts it DID
68
+ * name. A dynamically-built address a script hits at RUN TIME, whether or
69
+ * not this prompt was accepted, is never added to `allowedHosts` here (it
70
+ * is not known in advance); it fails at execution with the ordinary
71
+ * `'capability-denied'` kind, same as any other ungranted host.
72
+ */
73
+ import { type GrantClosureScript } from '@markii/runtime';
74
+ /** The subset of `RunRequirements` (`./script-requirements.ts`) this flow needs — kept structural rather than importing the whole interface, so a fake in tests doesn't need to fabricate `scripts`. */
75
+ export interface GrantFlowRequirements {
76
+ readonly hosts: readonly string[];
77
+ readonly hasUnknownHosts: boolean;
78
+ readonly grantScripts: readonly GrantClosureScript[];
79
+ /**
80
+ * Bundle-relative `src=` script path -> source text, for every `src=`
81
+ * block among `grantScripts` whose file content the host has resolved
82
+ * (F-1 fix) — `./bundle-run.ts`'s `bundleModulesFromSnapshot`, run over
83
+ * the same in-memory bundle snapshot the run itself will use. Omitted or
84
+ * empty for a bare `.mk.md` document (which has no bundle to resolve
85
+ * `src=` targets from) or a bundle whose scripts have no `src=` blocks.
86
+ * Folded into `closureFrom`'s `GrantClosure.bundleModules` so
87
+ * `computeGrantKey` hashes the REFERENCED file's bytes, not just the
88
+ * (empty) `code` field a `src=` block carries in the note's own text —
89
+ * without this, swapping a bundle script's contents while the note text
90
+ * stays byte-identical would silently re-run new code under an old,
91
+ * un-reprompted grant.
92
+ */
93
+ readonly bundleModules?: Readonly<Record<string, string>>;
94
+ }
95
+ /**
96
+ * The `Thenable` shape `vscode.Memento.update` actually returns — restated
97
+ * here (rather than importing `vscode`, which this file must never do) so
98
+ * a real `vscode.Memento` is assignable to `GrantMemento` with no adapter
99
+ * class, and a plain `Promise`-returning fake satisfies it in tests too.
100
+ */
101
+ export interface Thenable<T> {
102
+ then<TResult1 = T, TResult2 = never>(onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
103
+ }
104
+ /** The exact shape of `vscode.Memento` this module uses — `context.workspaceState` satisfies this directly. */
105
+ export interface GrantMemento {
106
+ get<T>(key: string): T | undefined;
107
+ get<T>(key: string, defaultValue: T): T;
108
+ update(key: string, value: unknown): Thenable<void>;
109
+ }
110
+ /** Prompts once for `host`, resolving `true` for Allow, `false` for Don't allow (or the prompt being dismissed). */
111
+ export type PromptHost = (host: string, declaredHosts: readonly string[]) => Promise<boolean>;
112
+ /** Prompts once for the "this note builds a network address dynamically" consent gate. */
113
+ export type PromptUnknownHosts = () => Promise<boolean>;
114
+ /** Prompts once for the consolidated "many hosts" gate (PROMPT-STORM fix) — resolves `true` for Allow all, `false` for Deny all. */
115
+ export type PromptManyHosts = (hostCount: number) => Promise<boolean>;
116
+ export interface GrantFlowOptions {
117
+ /** Stable identity for the note this run belongs to — `vscode.Uri.toString()` of the document, in the real adapter. */
118
+ documentKey: string;
119
+ requirements: GrantFlowRequirements;
120
+ memento: GrantMemento;
121
+ promptHost: PromptHost;
122
+ /**
123
+ * Hostnames the note's own scripts declare through their `permissions`
124
+ * fence attribute, for `hostPromptMessage` to show beside the host being
125
+ * asked about. Display only: what is actually prompted for, and what can
126
+ * be granted, comes from `requirements.hosts` (the literal-URL scan) and
127
+ * nothing else.
128
+ */
129
+ declaredHosts?: readonly string[];
130
+ promptUnknownHosts: PromptUnknownHosts;
131
+ /** Consolidated gate used instead of the per-host loop once the distinct static host count exceeds `MAX_HOST_PROMPTS` — see that constant's doc comment. */
132
+ promptManyHosts: PromptManyHosts;
133
+ }
134
+ export interface GrantFlowResult {
135
+ /** The hostnames this run's `net.*` calls may reach — exactly what `spawnRun`'s `netAllowlist` (`./run-host.ts`) expects. */
136
+ allowedHosts: string[];
137
+ }
138
+ /**
139
+ * Exact prompt wording — normative per the locked design comment. Exported
140
+ * so a test (and the adapter that renders it) both anchor on the same
141
+ * string.
142
+ *
143
+ * `declaredHosts` is what the note's own scripts declare through their
144
+ * `permissions` fence attribute (`./script-requirements.ts`'s
145
+ * `scriptDeclaredHosts`). It is DISPLAY-ONLY: it never changes what is
146
+ * prompted for or granted, only what the user reads before answering, so
147
+ * a note cannot talk its way into a host the literal-URL scan did not
148
+ * find. `host` is always the one host this prompt asks about. With no
149
+ * declaration the wording is unchanged, so a host that does not pass the
150
+ * list sees exactly the message it saw before.
151
+ */
152
+ export declare function hostPromptMessage(host: string, declaredHosts?: readonly string[]): string;
153
+ /** Exact wording of the one extra "hosts can't be listed in advance" prompt. */
154
+ export declare const UNKNOWN_HOSTS_PROMPT_MESSAGE = "This note builds network addresses at run time, which can't be listed in advance and will be denied; only the hosts written directly in the note can be granted. Allow those?";
155
+ /** Button labels every prompt in this flow uses — both a plain "Allow" and a "Don't allow" refusal are always offered, never a bare dismiss-only message. */
156
+ export declare const ALLOW_LABEL = "Allow";
157
+ export declare const DONT_ALLOW_LABEL = "Don't allow";
158
+ /**
159
+ * PROMPT-STORM fix (report section 8, item 6, docs/archive/PENTEST-REPORT-2026-08-23.md):
160
+ * `runGrantFlow`'s per-host loop below is sequential and modal, one dialog
161
+ * per statically-extracted host. A hostile note with hundreds of literal
162
+ * `net.fetch_json("https://hostN/")` calls would otherwise re-open hundreds
163
+ * of modals on EVERY `markii.runScripts` press — and because a FULL decline
164
+ * is deliberately never persisted (C-3, so one mis-click can't permanently
165
+ * lock out network access), nothing about answering them once makes them
166
+ * stop recurring on the next Run press either. That makes the per-host loop
167
+ * itself a denial-of-service surface against the user's own attention, not
168
+ * just an annoyance.
169
+ *
170
+ * The guard: cap how many per-host prompts one run will ever show. When the
171
+ * distinct SAFE static host count is at or under this cap, the ordinary
172
+ * per-host flow runs exactly as before (this is overwhelmingly the common
173
+ * case — most notes touch a small, fixed set of APIs). Above the cap, the
174
+ * per-host loop is skipped entirely in favor of ONE consolidated prompt
175
+ * (`promptManyHosts`) that grants either every one of those hosts or none of
176
+ * them — still a single, honest, all-or-nothing consent decision, just sized
177
+ * to the actual scale of what's being asked, rather than a wall of modals a
178
+ * user would rationally start rubber-stamping. 10 is comfortably above what
179
+ * a legitimate dashboard-style note (docs/scripting.md's callout on
180
+ * dashboards being a first-class use case) is expected to reach while still
181
+ * catching a hostile host list before it becomes hundreds of dialogs.
182
+ */
183
+ export declare const MAX_HOST_PROMPTS = 10;
184
+ /** Exact wording of the consolidated "many hosts" gate — normative, mirroring `hostPromptMessage`/`UNKNOWN_HOSTS_PROMPT_MESSAGE`'s pattern of an exported, test-anchored string. */
185
+ export declare function manyHostsPromptMessage(hostCount: number): string;
186
+ export declare function isSafeHostForPrompt(host: string): boolean;
187
+ /**
188
+ * Runs the whole grant flow for one Run press: computes the closure's grant
189
+ * key, reuses a matching stored grant with NO prompting at all, or (on a
190
+ * miss — first run or any code change) prompts fresh for every host and the
191
+ * unknown-hosts gate, persists the result, and returns the allowlist
192
+ * `spawnRun` should use.
193
+ */
194
+ export declare function runGrantFlow(options: GrantFlowOptions): Promise<GrantFlowResult>;
195
+ /**
196
+ * The NON-INTERACTIVE half of `runGrantFlow`, for auto/scheduled runs
197
+ * (GitHub issue #11): resolves this note's effective net + bundle-fs
198
+ * allowlist from ONLY what is already persisted, and NEVER prompts, NEVER
199
+ * writes. It is exactly `runGrantFlow`'s stored-grant fast path in
200
+ * isolation.
201
+ *
202
+ * This is the code half of the security promise auto/scheduled execution
203
+ * makes (docs/security.md, docs/scripting.md): a run that was not triggered
204
+ * by a click must never open a modal on a timer, and must never widen what
205
+ * the note can reach beyond a grant the user already made by hand under the
206
+ * exact same executable closure. On ANY miss — no stored grant, a grant for
207
+ * different code (the key no longer matches), or a stored host that fails
208
+ * today's `isSafeHostForPrompt` re-check (N-6) — this returns an EMPTY
209
+ * allowlist rather than falling back to prompting. An ungranted host then
210
+ * fails at execution with the ordinary `'capability-denied'` kind, exactly
211
+ * as it would on a manual run the user declined, and the consuming
212
+ * component shows its quiet stale/empty state.
213
+ *
214
+ * The read-only tier (`tierForTrigger('auto'|'scheduled')` in
215
+ * `@markii/runtime`) is a SEPARATE, independent gate applied in the worker;
216
+ * this function governs only which hosts are reachable, that one governs
217
+ * which capabilities (no POST/PATCH/bundle-write) may run at all.
218
+ */
219
+ export declare function resolveStoredGrant(options: {
220
+ documentKey: string;
221
+ requirements: GrantFlowRequirements;
222
+ memento: GrantMemento;
223
+ }): Promise<GrantFlowResult>;
224
+ /**
225
+ * Clears the persisted grant (if any) for `documentKey` — the
226
+ * `markii.resetScriptGrants` command's whole implementation (C-3): with no
227
+ * stored record, `runGrantFlow`'s next call for this document is
228
+ * indistinguishable from a first run, so it prompts fresh for every host
229
+ * again. A no-op, not an error, when there is nothing stored for this
230
+ * document.
231
+ */
232
+ export declare function clearGrantForDocument(memento: GrantMemento, documentKey: string): Promise<void>;