@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.
- package/README.md +20 -2
- package/dist/browser-worker.js +639 -0
- package/dist/browser.d.ts +61 -0
- package/dist/browser.js +172 -0
- package/dist/conformance/host/01-plain-open/expected.json +5 -0
- package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
- package/dist/conformance/host/01-plain-open/scenario.json +6 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
- package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
- package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
- package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
- package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
- package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
- package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
- package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
- package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
- package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
- package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
- package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
- package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
- package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
- package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
- package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
- package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
- package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
- package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
- package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
- package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
- package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
- package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
- package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
- package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
- package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
- package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
- package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
- package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
- package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
- package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
- package/dist/conformance/host/14-remove-pack/expected.json +14 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
- package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
- package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
- package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
- package/dist/conformance/host/16-pack-trust/expected.json +37 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
- package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
- package/dist/conformance/host/README.md +221 -0
- package/dist/conformance.d.ts +2 -0
- package/dist/conformance.js +5 -0
- package/dist/diagnostics/render-diagnostics.d.ts +41 -0
- package/dist/diagnostics/render-diagnostics.js +46 -0
- package/dist/export/cascade.d.ts +98 -0
- package/dist/export/cascade.js +159 -0
- package/dist/export/export-zip.d.ts +19 -0
- package/dist/export/export-zip.js +38 -0
- package/dist/export/image-embed.d.ts +126 -0
- package/dist/export/image-embed.js +274 -0
- package/dist/export/note-export.d.ts +332 -0
- package/dist/export/note-export.js +366 -0
- package/dist/export/note-links.d.ts +83 -0
- package/dist/export/note-links.js +241 -0
- package/dist/glue.wasm +0 -0
- package/dist/host/adapter.d.ts +218 -0
- package/dist/host/adapter.js +9 -0
- package/dist/host/cascade-export-behavior.d.ts +120 -0
- package/dist/host/cascade-export-behavior.js +151 -0
- package/dist/host/conformance-runner.d.ts +159 -0
- package/dist/host/conformance-runner.js +632 -0
- package/dist/host/create-host.d.ts +251 -0
- package/dist/host/create-host.js +329 -0
- package/dist/host/editor-behavior.d.ts +153 -0
- package/dist/host/editor-behavior.js +223 -0
- package/dist/host/export/md-plain.d.ts +15 -0
- package/dist/host/export/md-plain.js +125 -0
- package/dist/host/export-behavior.d.ts +111 -0
- package/dist/host/export-behavior.js +257 -0
- package/dist/host/labels.d.ts +23 -0
- package/dist/host/labels.js +32 -0
- package/dist/host/pack-archive.d.ts +30 -0
- package/dist/host/pack-archive.js +62 -0
- package/dist/host/pack-install.d.ts +87 -0
- package/dist/host/pack-install.js +186 -0
- package/dist/host/pack-load.d.ts +71 -0
- package/dist/host/pack-load.js +57 -0
- package/dist/host/pack-remove.d.ts +21 -0
- package/dist/host/pack-remove.js +65 -0
- package/dist/host/pack-trust-behavior.d.ts +79 -0
- package/dist/host/pack-trust-behavior.js +262 -0
- package/dist/host/refresh-interval.d.ts +37 -0
- package/dist/host/refresh-interval.js +59 -0
- package/dist/host/run-behavior.d.ts +88 -0
- package/dist/host/run-behavior.js +189 -0
- package/dist/host/run-diagnostics.d.ts +26 -0
- package/dist/host/run-diagnostics.js +43 -0
- package/dist/host/script-execution.d.ts +72 -0
- package/dist/host/script-execution.js +68 -0
- package/dist/host/terminal-packs.d.ts +39 -0
- package/dist/host/terminal-packs.js +51 -0
- package/dist/html-engine.d.ts +2 -0
- package/dist/html-engine.js +29 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +69 -0
- package/dist/insert/component-catalog.d.ts +23 -0
- package/dist/insert/component-catalog.js +96 -0
- package/dist/node-fs.d.ts +14 -0
- package/dist/node-fs.js +70 -0
- package/dist/node-worker.js +20657 -0
- package/dist/node.d.ts +6 -0
- package/dist/node.js +13 -0
- package/dist/package.json +3 -0
- package/dist/packs/discover.d.ts +119 -0
- package/dist/packs/discover.js +266 -0
- package/dist/packs/pack-diagnostics.d.ts +79 -0
- package/dist/packs/pack-diagnostics.js +103 -0
- package/dist/packs/pack-paths.d.ts +53 -0
- package/dist/packs/pack-paths.js +105 -0
- package/dist/packs/pack-render-registry.d.ts +82 -0
- package/dist/packs/pack-render-registry.js +182 -0
- package/dist/packs/pack-scripts.d.ts +20 -0
- package/dist/packs/pack-scripts.js +96 -0
- package/dist/packs/pack-trust.d.ts +57 -0
- package/dist/packs/pack-trust.js +111 -0
- package/dist/packs/prebuilt.d.ts +49 -0
- package/dist/packs/prebuilt.js +97 -0
- package/dist/path-util.d.ts +24 -0
- package/dist/path-util.js +82 -0
- package/dist/run/browser-isolate.d.ts +45 -0
- package/dist/run/browser-isolate.js +85 -0
- package/dist/run/bundle-document.d.ts +29 -0
- package/dist/run/bundle-document.js +40 -0
- package/dist/run/bundle-run.d.ts +188 -0
- package/dist/run/bundle-run.js +232 -0
- package/dist/run/grant-flow.d.ts +232 -0
- package/dist/run/grant-flow.js +390 -0
- package/dist/run/ip-address.d.ts +41 -0
- package/dist/run/ip-address.js +255 -0
- package/dist/run/isolate.d.ts +59 -0
- package/dist/run/isolate.js +22 -0
- package/dist/run/lua-resolver.d.ts +21 -0
- package/dist/run/lua-resolver.js +63 -0
- package/dist/run/net-bridge-worker.d.ts +26 -0
- package/dist/run/net-bridge-worker.js +66 -0
- package/dist/run/net-bridge.d.ts +56 -0
- package/dist/run/net-bridge.js +59 -0
- package/dist/run/net-pinning.d.ts +82 -0
- package/dist/run/net-pinning.js +137 -0
- package/dist/run/net-provider.d.ts +72 -0
- package/dist/run/net-provider.js +303 -0
- package/dist/run/node-isolate.d.ts +22 -0
- package/dist/run/node-isolate.js +68 -0
- package/dist/run/run-flow.d.ts +186 -0
- package/dist/run/run-flow.js +323 -0
- package/dist/run/run-host.d.ts +107 -0
- package/dist/run/run-host.js +241 -0
- package/dist/run/run-job.d.ts +130 -0
- package/dist/run/run-job.js +279 -0
- package/dist/run/run-progress.d.ts +55 -0
- package/dist/run/run-progress.js +42 -0
- package/dist/run/run-trace.d.ts +44 -0
- package/dist/run/run-trace.js +36 -0
- package/dist/run/script-requirements.d.ts +98 -0
- package/dist/run/script-requirements.js +212 -0
- package/dist/run/snapshot-storage.d.ts +25 -0
- package/dist/run/snapshot-storage.js +31 -0
- package/dist/run/stale-values.d.ts +25 -0
- package/dist/run/stale-values.js +32 -0
- package/dist/run/worker-entry-browser.d.ts +62 -0
- package/dist/run/worker-entry-browser.js +172 -0
- package/dist/run/worker-entry.d.ts +1 -0
- package/dist/run/worker-entry.js +98 -0
- package/dist/values-failure.d.ts +17 -0
- package/dist/values-failure.js +1 -0
- package/dist/values-merge.d.ts +39 -0
- package/dist/values-merge.js +33 -0
- package/package.json +80 -4
|
@@ -0,0 +1,390 @@
|
|
|
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 { computeGrantKey, } from '@markii/runtime';
|
|
74
|
+
/**
|
|
75
|
+
* Exact prompt wording — normative per the locked design comment. Exported
|
|
76
|
+
* so a test (and the adapter that renders it) both anchor on the same
|
|
77
|
+
* string.
|
|
78
|
+
*
|
|
79
|
+
* `declaredHosts` is what the note's own scripts declare through their
|
|
80
|
+
* `permissions` fence attribute (`./script-requirements.ts`'s
|
|
81
|
+
* `scriptDeclaredHosts`). It is DISPLAY-ONLY: it never changes what is
|
|
82
|
+
* prompted for or granted, only what the user reads before answering, so
|
|
83
|
+
* a note cannot talk its way into a host the literal-URL scan did not
|
|
84
|
+
* find. `host` is always the one host this prompt asks about. With no
|
|
85
|
+
* declaration the wording is unchanged, so a host that does not pass the
|
|
86
|
+
* list sees exactly the message it saw before.
|
|
87
|
+
*/
|
|
88
|
+
export function hostPromptMessage(host, declaredHosts) {
|
|
89
|
+
const ask = `This note's scripts can send data to ${host}. Allow?`;
|
|
90
|
+
if (!declaredHosts || declaredHosts.length === 0)
|
|
91
|
+
return ask;
|
|
92
|
+
return `This note's scripts can send data to ${host}. The note declares ${declaredHosts.join(', ')}. Allow?`;
|
|
93
|
+
}
|
|
94
|
+
/** Exact wording of the one extra "hosts can't be listed in advance" prompt. */
|
|
95
|
+
export 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?";
|
|
96
|
+
/** 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. */
|
|
97
|
+
export const ALLOW_LABEL = 'Allow';
|
|
98
|
+
export const DONT_ALLOW_LABEL = "Don't allow";
|
|
99
|
+
/**
|
|
100
|
+
* PROMPT-STORM fix (report section 8, item 6, docs/archive/PENTEST-REPORT-2026-08-23.md):
|
|
101
|
+
* `runGrantFlow`'s per-host loop below is sequential and modal, one dialog
|
|
102
|
+
* per statically-extracted host. A hostile note with hundreds of literal
|
|
103
|
+
* `net.fetch_json("https://hostN/")` calls would otherwise re-open hundreds
|
|
104
|
+
* of modals on EVERY `markii.runScripts` press — and because a FULL decline
|
|
105
|
+
* is deliberately never persisted (C-3, so one mis-click can't permanently
|
|
106
|
+
* lock out network access), nothing about answering them once makes them
|
|
107
|
+
* stop recurring on the next Run press either. That makes the per-host loop
|
|
108
|
+
* itself a denial-of-service surface against the user's own attention, not
|
|
109
|
+
* just an annoyance.
|
|
110
|
+
*
|
|
111
|
+
* The guard: cap how many per-host prompts one run will ever show. When the
|
|
112
|
+
* distinct SAFE static host count is at or under this cap, the ordinary
|
|
113
|
+
* per-host flow runs exactly as before (this is overwhelmingly the common
|
|
114
|
+
* case — most notes touch a small, fixed set of APIs). Above the cap, the
|
|
115
|
+
* per-host loop is skipped entirely in favor of ONE consolidated prompt
|
|
116
|
+
* (`promptManyHosts`) that grants either every one of those hosts or none of
|
|
117
|
+
* them — still a single, honest, all-or-nothing consent decision, just sized
|
|
118
|
+
* to the actual scale of what's being asked, rather than a wall of modals a
|
|
119
|
+
* user would rationally start rubber-stamping. 10 is comfortably above what
|
|
120
|
+
* a legitimate dashboard-style note (docs/scripting.md's callout on
|
|
121
|
+
* dashboards being a first-class use case) is expected to reach while still
|
|
122
|
+
* catching a hostile host list before it becomes hundreds of dialogs.
|
|
123
|
+
*/
|
|
124
|
+
export const MAX_HOST_PROMPTS = 10;
|
|
125
|
+
/** Exact wording of the consolidated "many hosts" gate — normative, mirroring `hostPromptMessage`/`UNKNOWN_HOSTS_PROMPT_MESSAGE`'s pattern of an exported, test-anchored string. */
|
|
126
|
+
export function manyHostsPromptMessage(hostCount) {
|
|
127
|
+
return `This note requests network access to many hosts (${hostCount}). Allow all or deny all?`;
|
|
128
|
+
}
|
|
129
|
+
const GRANTS_STORAGE_KEY = 'markii.netGrants';
|
|
130
|
+
function hasOwn(value, key) {
|
|
131
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
132
|
+
}
|
|
133
|
+
function isPlainObject(value) {
|
|
134
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
135
|
+
}
|
|
136
|
+
/** Never trusts a Memento's stored shape — a corrupt/foreign/hand-edited value degrades to "no stored grant" rather than throwing or partially applying. */
|
|
137
|
+
function isStoredGrant(value) {
|
|
138
|
+
if (!isPlainObject(value))
|
|
139
|
+
return false;
|
|
140
|
+
if (!hasOwn(value, 'key') || typeof value.key !== 'string')
|
|
141
|
+
return false;
|
|
142
|
+
if (!hasOwn(value, 'allowedHosts') ||
|
|
143
|
+
!Array.isArray(value.allowedHosts) ||
|
|
144
|
+
!value.allowedHosts.every((host) => typeof host === 'string')) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
function readAllGrants(memento) {
|
|
150
|
+
const raw = memento.get(GRANTS_STORAGE_KEY, {});
|
|
151
|
+
return isPlainObject(raw) ? raw : {};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Reads the stored grant for `documentKey`, if any, and re-runs every one of
|
|
155
|
+
* its `allowedHosts` through `isSafeHostForPrompt` before returning it (N-6):
|
|
156
|
+
* a stored record's hosts were only ever validated at COLLECTION time,
|
|
157
|
+
* possibly under an older/buggier version of this module, or (with direct
|
|
158
|
+
* `workspaceState` write access) never validated at all. Trusting the shape
|
|
159
|
+
* check alone and not re-deriving the safety property at read time would let
|
|
160
|
+
* a planted or stale record become the effective allowlist verbatim. Any
|
|
161
|
+
* host that fails re-validation is dropped from the returned grant, and
|
|
162
|
+
* `droppedUnsafeHost` is set so `runGrantFlow` know it must not treat this as
|
|
163
|
+
* a clean cache hit — see its own comment at the call site.
|
|
164
|
+
*/
|
|
165
|
+
function readGrant(memento, documentKey) {
|
|
166
|
+
const all = readAllGrants(memento);
|
|
167
|
+
if (!hasOwn(all, documentKey))
|
|
168
|
+
return undefined;
|
|
169
|
+
const candidate = all[documentKey];
|
|
170
|
+
if (!isStoredGrant(candidate))
|
|
171
|
+
return undefined;
|
|
172
|
+
const safeHosts = candidate.allowedHosts.filter(isSafeHostForPrompt);
|
|
173
|
+
return {
|
|
174
|
+
grant: {
|
|
175
|
+
key: candidate.key,
|
|
176
|
+
allowedHosts: safeHosts,
|
|
177
|
+
},
|
|
178
|
+
droppedUnsafeHost: safeHosts.length !== candidate.allowedHosts.length,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function writeGrant(memento, documentKey, grant) {
|
|
182
|
+
const all = readAllGrants(memento);
|
|
183
|
+
const next = { ...all, [documentKey]: grant };
|
|
184
|
+
await memento.update(GRANTS_STORAGE_KEY, next);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A conservative hostname-shape check for what this module is willing to
|
|
188
|
+
* interpolate into a prompt string. `extractRunRequirements`
|
|
189
|
+
* (`./script-requirements.ts`) already derives every host in
|
|
190
|
+
* `RunRequirements.hosts` from `new URL(literal).hostname` — which can
|
|
191
|
+
* never itself contain whitespace or control characters — but this is a
|
|
192
|
+
* second, independent gate at the point of DISPLAY: defense in depth
|
|
193
|
+
* against a future host-extraction bug (or a deliberately different caller
|
|
194
|
+
* of this module) ever interpolating raw, unvalidated note text into a
|
|
195
|
+
* modal dialog. Accepts ordinary DNS names, bracketed IPv6 literals, and
|
|
196
|
+
* plain IPv4 literals; rejects everything else, including any control or
|
|
197
|
+
* whitespace character.
|
|
198
|
+
*/
|
|
199
|
+
const SAFE_HOSTNAME_PATTERN = /^(?:[a-z0-9]([a-z0-9-]*[a-z0-9])?)(?:\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$|^\[[0-9a-f:]+\]$|^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/i;
|
|
200
|
+
export function isSafeHostForPrompt(host) {
|
|
201
|
+
// SAFE_HOSTNAME_PATTERN is a full, anchored (^...$) match against a
|
|
202
|
+
// closed character set, so no whitespace or control character can ever
|
|
203
|
+
// pass it -- no separate check is needed.
|
|
204
|
+
return (typeof host === 'string' &&
|
|
205
|
+
host.length > 0 &&
|
|
206
|
+
host.length <= 253 &&
|
|
207
|
+
SAFE_HOSTNAME_PATTERN.test(host));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Splits `hosts` into the ones safe to render in a per-host prompt, and
|
|
211
|
+
* whether the "can't be listed in advance" prompt is needed — either
|
|
212
|
+
* because the static scan already said so (`hasUnknownHosts`), or because
|
|
213
|
+
* at least one extracted host failed `isSafeHostForPrompt` and must never
|
|
214
|
+
* be shown verbatim. Failing toward the broader prompt (never toward
|
|
215
|
+
* silently dropping a host with no prompt at all) mirrors
|
|
216
|
+
* `extractRunRequirements`'s own philosophy.
|
|
217
|
+
*/
|
|
218
|
+
function partitionHosts(hosts, hasUnknownHosts) {
|
|
219
|
+
const safeHosts = [];
|
|
220
|
+
let needsUnknownPrompt = hasUnknownHosts;
|
|
221
|
+
for (const host of hosts) {
|
|
222
|
+
if (isSafeHostForPrompt(host)) {
|
|
223
|
+
safeHosts.push(host);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
needsUnknownPrompt = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { safeHosts, needsUnknownPrompt };
|
|
230
|
+
}
|
|
231
|
+
function closureFrom(requirements) {
|
|
232
|
+
return {
|
|
233
|
+
scripts: [...requirements.grantScripts],
|
|
234
|
+
// F-1 fix: was unconditionally `{}` — see `GrantFlowRequirements.bundleModules`'s
|
|
235
|
+
// doc comment for why that let a bundle script's contents change under an
|
|
236
|
+
// unchanged grant key.
|
|
237
|
+
bundleModules: { ...(requirements.bundleModules ?? {}) },
|
|
238
|
+
vaultModules: {},
|
|
239
|
+
packs: [],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Runs the whole grant flow for one Run press: computes the closure's grant
|
|
244
|
+
* key, reuses a matching stored grant with NO prompting at all, or (on a
|
|
245
|
+
* miss — first run or any code change) prompts fresh for every host and the
|
|
246
|
+
* unknown-hosts gate, persists the result, and returns the allowlist
|
|
247
|
+
* `spawnRun` should use.
|
|
248
|
+
*/
|
|
249
|
+
export async function runGrantFlow(options) {
|
|
250
|
+
const { documentKey, requirements, memento, promptHost, promptUnknownHosts, promptManyHosts, } = options;
|
|
251
|
+
const declaredHosts = options.declaredHosts ?? [];
|
|
252
|
+
const key = await computeGrantKey(closureFrom(requirements));
|
|
253
|
+
const stored = readGrant(memento, documentKey);
|
|
254
|
+
// N-6: a matching key alone is not enough to reuse a stored grant with no
|
|
255
|
+
// prompting -- only take the fast path when EVERY stored host still
|
|
256
|
+
// passes today's safety re-check. If `readGrant` had to drop anything,
|
|
257
|
+
// the record is not trusted at all here; fall through to the ordinary
|
|
258
|
+
// prompt flow below, which re-derives from the note's CURRENT
|
|
259
|
+
// requirements and, once answered, overwrites the untrusted record via
|
|
260
|
+
// the normal `writeGrant` call at the end of this function.
|
|
261
|
+
if (stored && stored.grant.key === key && !stored.droppedUnsafeHost) {
|
|
262
|
+
return { allowedHosts: [...stored.grant.allowedHosts] };
|
|
263
|
+
}
|
|
264
|
+
const { safeHosts, needsUnknownPrompt } = partitionHosts(requirements.hosts, requirements.hasUnknownHosts);
|
|
265
|
+
let allowedHosts;
|
|
266
|
+
if (safeHosts.length > MAX_HOST_PROMPTS) {
|
|
267
|
+
// PROMPT-STORM guard: too many distinct hosts for the per-host loop --
|
|
268
|
+
// fold the whole set into one consolidated all-or-nothing gate instead
|
|
269
|
+
// of opening `safeHosts.length` modals in a row. See `MAX_HOST_PROMPTS`'s
|
|
270
|
+
// doc comment.
|
|
271
|
+
const allowAll = await promptManyHosts(safeHosts.length);
|
|
272
|
+
allowedHosts = allowAll ? [...safeHosts] : [];
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
allowedHosts = [];
|
|
276
|
+
for (const host of safeHosts) {
|
|
277
|
+
// Each prompt is modal; they must appear one at a time, never all at
|
|
278
|
+
// once, so this loop is deliberately sequential rather than
|
|
279
|
+
// `Promise.all`-ed.
|
|
280
|
+
const allowed = await promptHost(host, declaredHosts);
|
|
281
|
+
if (allowed)
|
|
282
|
+
allowedHosts.push(host);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
let finalAllowedHosts = allowedHosts;
|
|
286
|
+
// The gate governs the per-host answers just collected: declining
|
|
287
|
+
// withdraws them. With NO grantable host it governs nothing, and asking
|
|
288
|
+
// anyway produces a dialog the user cannot satisfy — Allow grants no
|
|
289
|
+
// host, Deny withdraws no host, and because a run that ends with an
|
|
290
|
+
// empty allowlist is deliberately never persisted (C-3 below), the same
|
|
291
|
+
// unanswerable dialog reopens on every single Run press. A real note hit
|
|
292
|
+
// this: every `net.*` call site built its URL from a variable, so the
|
|
293
|
+
// static scan resolved zero hosts, and the gate became an infinite loop
|
|
294
|
+
// around a decision with no effect.
|
|
295
|
+
//
|
|
296
|
+
// Skipping it here cannot widen anything, and the equivalence is exact
|
|
297
|
+
// rather than a judgement call: with `safeHosts` empty, `allowedHosts`
|
|
298
|
+
// is already `[]` at this point, so BOTH branches of the gate produce
|
|
299
|
+
// `[]`. The outcome is identical with the prompt and without it.
|
|
300
|
+
//
|
|
301
|
+
// The user is not left uninformed, per AGENTS.md's "clean is not
|
|
302
|
+
// silent": the request is still denied at execution time with the
|
|
303
|
+
// ordinary `'capability-denied'` kind, which shows the component's quiet
|
|
304
|
+
// failure marker and writes the full reason to the host's diagnostics
|
|
305
|
+
// surface — naming the host actually attempted, which is strictly more
|
|
306
|
+
// useful than this gate's "some address, somewhere" wording could be.
|
|
307
|
+
if (needsUnknownPrompt && safeHosts.length > 0) {
|
|
308
|
+
const allowed = await promptUnknownHosts();
|
|
309
|
+
if (!allowed) {
|
|
310
|
+
// Declining the "can't be listed in advance" gate withdraws every
|
|
311
|
+
// per-host answer already collected too — see this module's top doc
|
|
312
|
+
// comment.
|
|
313
|
+
finalAllowedHosts = [];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// C-3: a FULL decline (no host ended up allowed) is deliberately NOT
|
|
317
|
+
// persisted. Persisting `{key, allowedHosts: []}` would otherwise disable
|
|
318
|
+
// network for this note's exact current code FOREVER — a single
|
|
319
|
+
// mis-click on "Don't allow" (or declining the unknown-hosts gate) would
|
|
320
|
+
// silently and permanently suppress every future prompt, with no stored
|
|
321
|
+
// record even hinting that happened and no way to revoke it short of
|
|
322
|
+
// editing the code (which changes the grant key anyway). Only ever
|
|
323
|
+
// writing a grant with at least one allowed host means a full decline
|
|
324
|
+
// simply re-prompts on the next Run press, exactly like a first run — the
|
|
325
|
+
// ordinary, recoverable outcome. A PARTIAL grant (at least one host
|
|
326
|
+
// allowed) is still persisted normally, and `clearGrantForDocument` below
|
|
327
|
+
// gives an explicit way to revoke a persisted grant when the user wants a
|
|
328
|
+
// fresh prompt without changing the note's code.
|
|
329
|
+
if (finalAllowedHosts.length > 0) {
|
|
330
|
+
await writeGrant(memento, documentKey, {
|
|
331
|
+
key,
|
|
332
|
+
allowedHosts: finalAllowedHosts,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return { allowedHosts: finalAllowedHosts };
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The NON-INTERACTIVE half of `runGrantFlow`, for auto/scheduled runs
|
|
339
|
+
* (GitHub issue #11): resolves this note's effective net + bundle-fs
|
|
340
|
+
* allowlist from ONLY what is already persisted, and NEVER prompts, NEVER
|
|
341
|
+
* writes. It is exactly `runGrantFlow`'s stored-grant fast path in
|
|
342
|
+
* isolation.
|
|
343
|
+
*
|
|
344
|
+
* This is the code half of the security promise auto/scheduled execution
|
|
345
|
+
* makes (docs/security.md, docs/scripting.md): a run that was not triggered
|
|
346
|
+
* by a click must never open a modal on a timer, and must never widen what
|
|
347
|
+
* the note can reach beyond a grant the user already made by hand under the
|
|
348
|
+
* exact same executable closure. On ANY miss — no stored grant, a grant for
|
|
349
|
+
* different code (the key no longer matches), or a stored host that fails
|
|
350
|
+
* today's `isSafeHostForPrompt` re-check (N-6) — this returns an EMPTY
|
|
351
|
+
* allowlist rather than falling back to prompting. An ungranted host then
|
|
352
|
+
* fails at execution with the ordinary `'capability-denied'` kind, exactly
|
|
353
|
+
* as it would on a manual run the user declined, and the consuming
|
|
354
|
+
* component shows its quiet stale/empty state.
|
|
355
|
+
*
|
|
356
|
+
* The read-only tier (`tierForTrigger('auto'|'scheduled')` in
|
|
357
|
+
* `@markii/runtime`) is a SEPARATE, independent gate applied in the worker;
|
|
358
|
+
* this function governs only which hosts are reachable, that one governs
|
|
359
|
+
* which capabilities (no POST/PATCH/bundle-write) may run at all.
|
|
360
|
+
*/
|
|
361
|
+
export async function resolveStoredGrant(options) {
|
|
362
|
+
const empty = { allowedHosts: [] };
|
|
363
|
+
const stored = readGrant(options.memento, options.documentKey);
|
|
364
|
+
// N-6: a stored host that no longer passes today's safety re-check makes
|
|
365
|
+
// the whole record untrusted here, exactly as it does in `runGrantFlow` —
|
|
366
|
+
// but with no prompt to fall through to, an untrusted record simply yields
|
|
367
|
+
// the empty allowlist.
|
|
368
|
+
if (!stored || stored.droppedUnsafeHost)
|
|
369
|
+
return empty;
|
|
370
|
+
const key = await computeGrantKey(closureFrom(options.requirements));
|
|
371
|
+
if (stored.grant.key !== key)
|
|
372
|
+
return empty;
|
|
373
|
+
return { allowedHosts: [...stored.grant.allowedHosts] };
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Clears the persisted grant (if any) for `documentKey` — the
|
|
377
|
+
* `markii.resetScriptGrants` command's whole implementation (C-3): with no
|
|
378
|
+
* stored record, `runGrantFlow`'s next call for this document is
|
|
379
|
+
* indistinguishable from a first run, so it prompts fresh for every host
|
|
380
|
+
* again. A no-op, not an error, when there is nothing stored for this
|
|
381
|
+
* document.
|
|
382
|
+
*/
|
|
383
|
+
export async function clearGrantForDocument(memento, documentKey) {
|
|
384
|
+
const all = readAllGrants(memento);
|
|
385
|
+
if (!hasOwn(all, documentKey))
|
|
386
|
+
return;
|
|
387
|
+
const next = { ...all };
|
|
388
|
+
delete next[documentKey];
|
|
389
|
+
await memento.update(GRANTS_STORAGE_KEY, next);
|
|
390
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Address classification for the run path's network jail (GitHub issue #10).
|
|
3
|
+
*
|
|
4
|
+
* A network grant is a hostname string, so the host the user consented to and
|
|
5
|
+
* the address the request actually reaches are two different things. This
|
|
6
|
+
* module answers the second half: given an address, is it somewhere a note's
|
|
7
|
+
* script has any business reaching?
|
|
8
|
+
*
|
|
9
|
+
* Pure and dependency-free (the address validators are local), so it
|
|
10
|
+
* is exhaustively testable without a socket, a resolver, or a worker.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* True for a dotted-quad IPv4 literal: four decimal octets, 0 to 255, with
|
|
14
|
+
* no leading zeros (`010.0.0.1` is refused, since some SSRF filters
|
|
15
|
+
* mis-parse it as decimal) and no extra characters.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isIPv4(value: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* True for an IPv6 literal without a zone index: up to eight groups of one
|
|
20
|
+
* to four hex digits, at most one `::` run, and an optional trailing
|
|
21
|
+
* dotted-quad standing for the last two groups.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isIPv6(value: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Where an address lives. Everything that is not `'public'` is somewhere a
|
|
26
|
+
* script reaching it would be a surprise to the person who granted a
|
|
27
|
+
* hostname: their own machine, their own LAN, or a range that is not
|
|
28
|
+
* globally routable at all.
|
|
29
|
+
*/
|
|
30
|
+
export type AddressScope = 'public' | 'loopback' | 'private' | 'link-local' | 'unspecified' | 'multicast' | 'reserved';
|
|
31
|
+
/** Every scope except `'public'`: not reachable by default from a note's script. */
|
|
32
|
+
export declare function isRestrictedScope(scope: AddressScope): boolean;
|
|
33
|
+
/** Human wording for a denial or a grant prompt. Kept here so one vocabulary describes a scope everywhere. */
|
|
34
|
+
export declare function describeScope(scope: AddressScope): string;
|
|
35
|
+
/**
|
|
36
|
+
* Classifies `address`, or returns `undefined` when it is not an IP literal
|
|
37
|
+
* at all (a hostname, or anything malformed). The local `isIPv4`/`isIPv6`
|
|
38
|
+
* validators decide what counts as an IP; they reject the leading-zero octet forms
|
|
39
|
+
* (`010.0.0.1`) that some SSRF filters mis-parse as decimal.
|
|
40
|
+
*/
|
|
41
|
+
export declare function classifyIpAddress(address: string): AddressScope | undefined;
|