@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.
- package/README.md +18 -2
- package/dist/browser-worker.js +639 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +167 -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 +223 -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 +156 -0
- package/dist/host/conformance-runner.js +634 -0
- package/dist/host/create-host.d.ts +253 -0
- package/dist/host/create-host.js +329 -0
- package/dist/host/editor-behavior.d.ts +157 -0
- package/dist/host/editor-behavior.js +227 -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 +113 -0
- package/dist/host/export-behavior.js +257 -0
- package/dist/host/labels.d.ts +24 -0
- package/dist/host/labels.js +32 -0
- package/dist/host/pack-archive.d.ts +30 -0
- package/dist/host/pack-archive.js +68 -0
- package/dist/host/pack-install.d.ts +88 -0
- package/dist/host/pack-install.js +189 -0
- package/dist/host/pack-load.d.ts +72 -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 +39 -0
- package/dist/host/refresh-interval.js +61 -0
- package/dist/host/run-behavior.d.ts +89 -0
- package/dist/host/run-behavior.js +168 -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 +70 -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.d.ts +5 -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 +81 -0
- package/dist/packs/pack-diagnostics.js +105 -0
- package/dist/packs/pack-paths.d.ts +53 -0
- package/dist/packs/pack-paths.js +106 -0
- package/dist/packs/pack-render-registry.d.ts +83 -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 +81 -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 +57 -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 +70 -0
- package/dist/run/net-provider.js +289 -0
- package/dist/run/node-isolate.d.ts +30 -0
- package/dist/run/node-isolate.js +117 -0
- package/dist/run/run-flow.d.ts +187 -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 +42 -0
- package/dist/run/worker-entry-browser.js +77 -0
- package/dist/run/worker-entry.d.ts +1 -0
- package/dist/run/worker-entry.js +96 -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 +77 -4
|
@@ -0,0 +1,255 @@
|
|
|
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 function isIPv4(value) {
|
|
18
|
+
const parts = value.split('.');
|
|
19
|
+
if (parts.length !== 4)
|
|
20
|
+
return false;
|
|
21
|
+
return parts.every((part) => /^(0|[1-9][0-9]{0,2})$/.test(part) && Number.parseInt(part, 10) <= 255);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* True for an IPv6 literal without a zone index: up to eight groups of one
|
|
25
|
+
* to four hex digits, at most one `::` run, and an optional trailing
|
|
26
|
+
* dotted-quad standing for the last two groups.
|
|
27
|
+
*/
|
|
28
|
+
export function isIPv6(value) {
|
|
29
|
+
if (value === '' || value.includes('%') || !value.includes(':'))
|
|
30
|
+
return false;
|
|
31
|
+
const doubleColon = value.indexOf('::');
|
|
32
|
+
if (doubleColon !== -1 && value.indexOf('::', doubleColon + 1) !== -1) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
// ':::' is two overlapping '::' runs.
|
|
36
|
+
if (value.includes(':::'))
|
|
37
|
+
return false;
|
|
38
|
+
const head = doubleColon === -1 ? value : value.slice(0, doubleColon);
|
|
39
|
+
const tail = doubleColon === -1 ? '' : value.slice(doubleColon + 2);
|
|
40
|
+
const headGroups = head === '' ? [] : head.split(':');
|
|
41
|
+
const tailGroups = tail === '' ? [] : tail.split(':');
|
|
42
|
+
const groups = [...headGroups, ...tailGroups];
|
|
43
|
+
let count = 0;
|
|
44
|
+
for (let i = 0; i < groups.length; i += 1) {
|
|
45
|
+
const group = groups[i] ?? '';
|
|
46
|
+
const isLast = i === groups.length - 1;
|
|
47
|
+
if (group.includes('.')) {
|
|
48
|
+
// Only the final group may be a dotted-quad, and it counts as two.
|
|
49
|
+
if (!isLast || !isIPv4(group))
|
|
50
|
+
return false;
|
|
51
|
+
count += 2;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(group))
|
|
55
|
+
return false;
|
|
56
|
+
count += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return doubleColon === -1 ? count === 8 : count <= 7;
|
|
60
|
+
}
|
|
61
|
+
/** Every scope except `'public'`: not reachable by default from a note's script. */
|
|
62
|
+
export function isRestrictedScope(scope) {
|
|
63
|
+
return scope !== 'public';
|
|
64
|
+
}
|
|
65
|
+
/** Human wording for a denial or a grant prompt. Kept here so one vocabulary describes a scope everywhere. */
|
|
66
|
+
export function describeScope(scope) {
|
|
67
|
+
switch (scope) {
|
|
68
|
+
case 'public':
|
|
69
|
+
return 'a public address';
|
|
70
|
+
case 'loopback':
|
|
71
|
+
return 'an address on this machine';
|
|
72
|
+
case 'private':
|
|
73
|
+
return 'an address on this private network';
|
|
74
|
+
case 'link-local':
|
|
75
|
+
return 'a link-local address';
|
|
76
|
+
case 'unspecified':
|
|
77
|
+
return 'the unspecified address';
|
|
78
|
+
case 'multicast':
|
|
79
|
+
return 'a multicast address';
|
|
80
|
+
case 'reserved':
|
|
81
|
+
return 'a reserved address';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Classifies `address`, or returns `undefined` when it is not an IP literal
|
|
86
|
+
* at all (a hostname, or anything malformed). The local `isIPv4`/`isIPv6`
|
|
87
|
+
* validators decide what counts as an IP; they reject the leading-zero octet forms
|
|
88
|
+
* (`010.0.0.1`) that some SSRF filters mis-parse as decimal.
|
|
89
|
+
*/
|
|
90
|
+
export function classifyIpAddress(address) {
|
|
91
|
+
const trimmed = address.trim();
|
|
92
|
+
// A bracketed literal is how an IPv6 address appears in a URL authority;
|
|
93
|
+
// `URL.hostname` hands it back still bracketed.
|
|
94
|
+
const bare = trimmed.startsWith('[') && trimmed.endsWith(']')
|
|
95
|
+
? trimmed.slice(1, -1)
|
|
96
|
+
: trimmed;
|
|
97
|
+
if (isIPv4(bare))
|
|
98
|
+
return classifyIpv4(bare);
|
|
99
|
+
if (isIPv6(bare))
|
|
100
|
+
return classifyIpv6(bare);
|
|
101
|
+
// An IPv6 literal may carry a zone index (`fe80::1%eth0`), which the
|
|
102
|
+
// validator rejects; the address before the `%` is what matters.
|
|
103
|
+
const zoneSplit = bare.indexOf('%');
|
|
104
|
+
if (zoneSplit > 0) {
|
|
105
|
+
const withoutZone = bare.slice(0, zoneSplit);
|
|
106
|
+
if (isIPv6(withoutZone))
|
|
107
|
+
return classifyIpv6(withoutZone);
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function classifyIpv4(address) {
|
|
112
|
+
const octets = address.split('.').map((part) => Number(part));
|
|
113
|
+
return classifyIpv4Octets(octets[0], octets[1], octets[2], octets[3]);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The IPv4 special-purpose ranges, in the order a classifier must read them
|
|
117
|
+
* (narrower ranges before the wider ones they sit inside). Every range that
|
|
118
|
+
* is not globally routable is restricted, not only the three RFC 1918
|
|
119
|
+
* blocks: a filter that stops at 10/8, 172.16/12 and 192.168/16 is the
|
|
120
|
+
* classic incomplete SSRF filter, walked around with 169.254.169.254 (the
|
|
121
|
+
* cloud metadata address) or a CGNAT address.
|
|
122
|
+
*/
|
|
123
|
+
function classifyIpv4Octets(a = 0, b = 0, c = 0, d = 0) {
|
|
124
|
+
if (a === 0)
|
|
125
|
+
return b === 0 && c === 0 && d === 0 ? 'unspecified' : 'reserved';
|
|
126
|
+
if (a === 127)
|
|
127
|
+
return 'loopback';
|
|
128
|
+
if (a === 10)
|
|
129
|
+
return 'private';
|
|
130
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
131
|
+
return 'private';
|
|
132
|
+
if (a === 192 && b === 168)
|
|
133
|
+
return 'private';
|
|
134
|
+
// Carrier-grade NAT (RFC 6598): a real LAN range on many networks.
|
|
135
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
136
|
+
return 'private';
|
|
137
|
+
// Link-local, which contains the cloud metadata address 169.254.169.254.
|
|
138
|
+
if (a === 169 && b === 254)
|
|
139
|
+
return 'link-local';
|
|
140
|
+
// IETF protocol assignments, and the benchmarking range.
|
|
141
|
+
if (a === 192 && b === 0 && c === 0)
|
|
142
|
+
return 'reserved';
|
|
143
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
144
|
+
return 'reserved';
|
|
145
|
+
// Documentation ranges (RFC 5737).
|
|
146
|
+
if (a === 192 && b === 0 && c === 2)
|
|
147
|
+
return 'reserved';
|
|
148
|
+
if (a === 198 && b === 51 && c === 100)
|
|
149
|
+
return 'reserved';
|
|
150
|
+
if (a === 203 && b === 0 && c === 113)
|
|
151
|
+
return 'reserved';
|
|
152
|
+
if (a >= 224 && a <= 239)
|
|
153
|
+
return 'multicast';
|
|
154
|
+
if (a >= 240)
|
|
155
|
+
return 'reserved';
|
|
156
|
+
return 'public';
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Expands `address` to its sixteen bytes. The validator has already accepted
|
|
160
|
+
* it, so this handles the two shapes that survive: groups of hex separated by
|
|
161
|
+
* `:` with at most one `::` run, and a trailing dotted-quad.
|
|
162
|
+
*/
|
|
163
|
+
function ipv6Bytes(address) {
|
|
164
|
+
let head = address;
|
|
165
|
+
let tail = '';
|
|
166
|
+
const doubleColon = address.indexOf('::');
|
|
167
|
+
if (doubleColon !== -1) {
|
|
168
|
+
head = address.slice(0, doubleColon);
|
|
169
|
+
tail = address.slice(doubleColon + 2);
|
|
170
|
+
}
|
|
171
|
+
const bytes = [];
|
|
172
|
+
const pushGroup = (group) => {
|
|
173
|
+
// A trailing dotted-quad stands for the last four bytes.
|
|
174
|
+
if (group.includes('.')) {
|
|
175
|
+
if (!isIPv4(group))
|
|
176
|
+
return false;
|
|
177
|
+
for (const part of group.split('.'))
|
|
178
|
+
bytes.push(Number(part));
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
const value = Number.parseInt(group, 16);
|
|
182
|
+
if (!Number.isFinite(value))
|
|
183
|
+
return false;
|
|
184
|
+
bytes.push((value >> 8) & 0xff, value & 0xff);
|
|
185
|
+
return true;
|
|
186
|
+
};
|
|
187
|
+
const headGroups = head === '' ? [] : head.split(':');
|
|
188
|
+
const tailGroups = tail === '' ? [] : tail.split(':');
|
|
189
|
+
for (const group of headGroups)
|
|
190
|
+
if (!pushGroup(group))
|
|
191
|
+
return undefined;
|
|
192
|
+
const tailBytes = [];
|
|
193
|
+
const savedLength = bytes.length;
|
|
194
|
+
for (const group of tailGroups)
|
|
195
|
+
if (!pushGroup(group))
|
|
196
|
+
return undefined;
|
|
197
|
+
tailBytes.push(...bytes.splice(savedLength));
|
|
198
|
+
if (doubleColon === -1) {
|
|
199
|
+
return bytes.length === 16 ? bytes : undefined;
|
|
200
|
+
}
|
|
201
|
+
const gap = 16 - bytes.length - tailBytes.length;
|
|
202
|
+
if (gap < 0)
|
|
203
|
+
return undefined;
|
|
204
|
+
return [...bytes, ...new Array(gap).fill(0), ...tailBytes];
|
|
205
|
+
}
|
|
206
|
+
function classifyIpv6(address) {
|
|
207
|
+
const bytes = ipv6Bytes(address.toLowerCase());
|
|
208
|
+
// Unparseable after the validator accepted it should be impossible; treat
|
|
209
|
+
// it as restricted anyway rather than letting an unknown shape through.
|
|
210
|
+
if (!bytes)
|
|
211
|
+
return 'reserved';
|
|
212
|
+
/** Byte `index`, or 0 past the end. `bytes` is always sixteen long here; this keeps the reads total under `noUncheckedIndexedAccess`. */
|
|
213
|
+
const at = (index) => bytes[index] ?? 0;
|
|
214
|
+
const allZero = (from, to) => bytes.slice(from, to).every((byte) => byte === 0);
|
|
215
|
+
if (allZero(0, 16))
|
|
216
|
+
return 'unspecified';
|
|
217
|
+
if (allZero(0, 15) && at(15) === 1)
|
|
218
|
+
return 'loopback';
|
|
219
|
+
// The three shapes that carry an IPv4 address inside an IPv6 one. Each is a
|
|
220
|
+
// documented SSRF bypass when a filter classifies the wrapper instead of
|
|
221
|
+
// the address it actually reaches, so each is classified by its payload.
|
|
222
|
+
// IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible (::a.b.c.d).
|
|
223
|
+
if (allZero(0, 10) && at(10) === 0xff && at(11) === 0xff) {
|
|
224
|
+
return classifyIpv4Octets(at(12), at(13), at(14), at(15));
|
|
225
|
+
}
|
|
226
|
+
if (allZero(0, 12)) {
|
|
227
|
+
return classifyIpv4Octets(at(12), at(13), at(14), at(15));
|
|
228
|
+
}
|
|
229
|
+
// 6to4 (2002::/16) embeds the IPv4 address in the next four bytes.
|
|
230
|
+
if (at(0) === 0x20 && at(1) === 0x02) {
|
|
231
|
+
return classifyIpv4Octets(at(2), at(3), at(4), at(5));
|
|
232
|
+
}
|
|
233
|
+
// NAT64 well-known prefix (64:ff9b::/96) embeds it in the last four.
|
|
234
|
+
if (at(0) === 0x00 &&
|
|
235
|
+
at(1) === 0x64 &&
|
|
236
|
+
at(2) === 0xff &&
|
|
237
|
+
at(3) === 0x9b &&
|
|
238
|
+
allZero(4, 12)) {
|
|
239
|
+
return classifyIpv4Octets(at(12), at(13), at(14), at(15));
|
|
240
|
+
}
|
|
241
|
+
// Unique-local (fc00::/7).
|
|
242
|
+
if ((at(0) & 0xfe) === 0xfc)
|
|
243
|
+
return 'private';
|
|
244
|
+
// Link-local (fe80::/10).
|
|
245
|
+
if (at(0) === 0xfe && (at(1) & 0xc0) === 0x80)
|
|
246
|
+
return 'link-local';
|
|
247
|
+
// Multicast (ff00::/8).
|
|
248
|
+
if (at(0) === 0xff)
|
|
249
|
+
return 'multicast';
|
|
250
|
+
// Documentation (2001:db8::/32) and the IETF protocol block (2001::/23).
|
|
251
|
+
if (at(0) === 0x20 && at(1) === 0x01 && at(2) === 0x0d && at(3) === 0xb8) {
|
|
252
|
+
return 'reserved';
|
|
253
|
+
}
|
|
254
|
+
return 'public';
|
|
255
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam between `spawnRun`'s watchdog logic and the KIND of isolate a
|
|
3
|
+
* host can actually create.
|
|
4
|
+
*
|
|
5
|
+
* `./run-host.ts` owns the part that must never differ between hosts: the
|
|
6
|
+
* external wall-clock watchdog, exactly-once settlement, and the
|
|
7
|
+
* never-rejects contract. What differs is what the runtime can spawn at
|
|
8
|
+
* all. A Node host (the VS Code extension's extension host) uses
|
|
9
|
+
* `node:worker_threads`. An Electron RENDERER, which is what an Obsidian
|
|
10
|
+
* plugin runs in, supports neither: `new Worker()` from `worker_threads`
|
|
11
|
+
* throws "The V8 platform used by this instance of Node does not support
|
|
12
|
+
* creating Workers", and forking a Node child via `ELECTRON_RUN_AS_NODE`
|
|
13
|
+
* was measured failing there too (no IPC round-trip; the child exited on
|
|
14
|
+
* SIGTRAP). A Web Worker is what remains, and it works.
|
|
15
|
+
*
|
|
16
|
+
* Both satisfy the normative requirement in docs/security.md: a dedicated
|
|
17
|
+
* isolate that an external watchdog can terminate from outside. They do
|
|
18
|
+
* NOT have equal capabilities, and the difference is documented rather than
|
|
19
|
+
* papered over: `maxOldGenerationSizeMb` bounds a worker thread's V8 heap,
|
|
20
|
+
* and a Web Worker accepts no such cap.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* One spawned isolate, reduced to what the watchdog needs. Deliberately
|
|
24
|
+
* event-callback shaped rather than promise-shaped: `spawnRun` settles on
|
|
25
|
+
* whichever of message/error/exit/watchdog fires FIRST, which a promise
|
|
26
|
+
* per event would make harder to express, not easier.
|
|
27
|
+
*
|
|
28
|
+
* Every implementation must guarantee: `kill()` is idempotent and safe on
|
|
29
|
+
* an isolate that already exited, and no callback fires after `kill()`
|
|
30
|
+
* resolves the run (late events are harmless anyway, since `spawnRun`
|
|
31
|
+
* guards settlement, but an implementation should not rely on that).
|
|
32
|
+
*/
|
|
33
|
+
export interface RunIsolate {
|
|
34
|
+
/**
|
|
35
|
+
* Hands the job to the isolate. MAY THROW synchronously — a structured
|
|
36
|
+
* clone of an uncloneable payload does exactly that — and `spawnRun`
|
|
37
|
+
* catches it, so an implementation must not swallow it into a silent
|
|
38
|
+
* no-op that would leave the run hanging until the watchdog.
|
|
39
|
+
*/
|
|
40
|
+
send(job: unknown): void;
|
|
41
|
+
/** Terminates the isolate. Called by the watchdog and again on settle; must tolerate both. */
|
|
42
|
+
kill(): void;
|
|
43
|
+
onMessage(listener: (message: unknown) => void): void;
|
|
44
|
+
onError(listener: (error: unknown) => void): void;
|
|
45
|
+
/** `code` is `null` where the runtime does not report one (a Web Worker never does). */
|
|
46
|
+
onExit(listener: (code: number | null) => void): void;
|
|
47
|
+
}
|
|
48
|
+
export interface SpawnIsolateOptions {
|
|
49
|
+
/** The entry the isolate loads — resolved by the host, never guessed here (see `./run-host.ts`'s `defaultWorkerPath`). */
|
|
50
|
+
entryPath: string;
|
|
51
|
+
/** Node `execArgv` for a worker-thread isolate; meaningless to a Web Worker, which ignores it. */
|
|
52
|
+
execArgv?: string[];
|
|
53
|
+
/** V8 old-space cap in MB. A worker thread honors it; a Web Worker cannot, and says so in its own doc comment. */
|
|
54
|
+
maxOldGenerationSizeMb: number;
|
|
55
|
+
}
|
|
56
|
+
/** Creates one isolate per run. Injected via `SpawnRunOptions.spawnIsolate`; defaults to the worker-thread implementation. */
|
|
57
|
+
export type IsolateSpawner = (options: SpawnIsolateOptions) => RunIsolate;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam between `spawnRun`'s watchdog logic and the KIND of isolate a
|
|
3
|
+
* host can actually create.
|
|
4
|
+
*
|
|
5
|
+
* `./run-host.ts` owns the part that must never differ between hosts: the
|
|
6
|
+
* external wall-clock watchdog, exactly-once settlement, and the
|
|
7
|
+
* never-rejects contract. What differs is what the runtime can spawn at
|
|
8
|
+
* all. A Node host (the VS Code extension's extension host) uses
|
|
9
|
+
* `node:worker_threads`. An Electron RENDERER, which is what an Obsidian
|
|
10
|
+
* plugin runs in, supports neither: `new Worker()` from `worker_threads`
|
|
11
|
+
* throws "The V8 platform used by this instance of Node does not support
|
|
12
|
+
* creating Workers", and forking a Node child via `ELECTRON_RUN_AS_NODE`
|
|
13
|
+
* was measured failing there too (no IPC round-trip; the child exited on
|
|
14
|
+
* SIGTRAP). A Web Worker is what remains, and it works.
|
|
15
|
+
*
|
|
16
|
+
* Both satisfy the normative requirement in docs/security.md: a dedicated
|
|
17
|
+
* isolate that an external watchdog can terminate from outside. They do
|
|
18
|
+
* NOT have equal capabilities, and the difference is documented rather than
|
|
19
|
+
* papered over: `maxOldGenerationSizeMb` bounds a worker thread's V8 heap,
|
|
20
|
+
* and a Web Worker accepts no such cap.
|
|
21
|
+
*/
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PackModuleResolver } from '@markii/lua';
|
|
2
|
+
/**
|
|
3
|
+
* Pack namespace -> (bundle-jail-normalized module path, always ending in
|
|
4
|
+
* `.lua`) -> Lua source text. Produced by `./pack-scripts.ts` on the
|
|
5
|
+
* extension host and carried into the worker as `RunJob.packModules`.
|
|
6
|
+
*/
|
|
7
|
+
export type PackModulesMap = Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
8
|
+
/**
|
|
9
|
+
* Builds a `PackModuleResolver` (`@markii/lua`) backed by a pre-loaded
|
|
10
|
+
* `PackModulesMap`. Every lookup is read with `Object.hasOwn` only (never
|
|
11
|
+
* bare indexing) so a pack namespace or module path shaped like
|
|
12
|
+
* `__proto__`/`constructor` can never resolve through the prototype chain,
|
|
13
|
+
* matching this codebase's hostile-map discipline throughout
|
|
14
|
+
* `@markii/pack`/`@markii/bundle`.
|
|
15
|
+
*
|
|
16
|
+
* Returns `undefined` (a clean "no such module", per `PackModuleResolver`'s
|
|
17
|
+
* own contract) for: an unconfigured pack namespace, a module path the
|
|
18
|
+
* bundle-jail rejects (traversal, absolute path, null byte, backslash), or
|
|
19
|
+
* a module path not present in the map. Never throws.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createPackModuleResolver(modules: PackModulesMap): PackModuleResolver;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure half of the Run path's `PackModuleResolver` (`@markii/lua`'s
|
|
3
|
+
* `require.ts`): a synchronous, in-memory lookup over a pre-loaded map of
|
|
4
|
+
* pack Lua source text. `vscode`-free and dependency-free beyond
|
|
5
|
+
* `@markii/bundle`'s path jail, so it is unit-testable directly.
|
|
6
|
+
*
|
|
7
|
+
* WHY pre-loaded rather than reading files live: the resolver runs on the
|
|
8
|
+
* `worker_thread` the Run path spawns per run (`run/worker-entry.ts`),
|
|
9
|
+
* which has no filesystem access of its own — by design, the worker's only
|
|
10
|
+
* inputs are the serializable `RunJob` message the extension host posts to
|
|
11
|
+
* it (docs/security.md's isolate model treats the worker as running
|
|
12
|
+
* untrusted script content). So the extension host reads every pack's
|
|
13
|
+
* `scripts/*.lua` files ahead of time (`./pack-scripts.ts`), and this
|
|
14
|
+
* module's map is exactly that pre-read content, structurally cloned into
|
|
15
|
+
* the job. `require "packName/modulePath"` then resolves with no I/O at
|
|
16
|
+
* all inside the worker.
|
|
17
|
+
*/
|
|
18
|
+
import { normalizeBundlePath } from '@markii/bundle';
|
|
19
|
+
/**
|
|
20
|
+
* Normalizes a `require "packName/modulePath"` module path the same way
|
|
21
|
+
* `./pack-scripts.ts` normalized it while building the map: reject any
|
|
22
|
+
* path-jail violation (`..`, an absolute path, a null byte, a backslash —
|
|
23
|
+
* see `@markii/bundle`'s `normalizeBundlePath`) and append `.lua` when the
|
|
24
|
+
* caller didn't already write one, matching `@markii/lua`'s own bundle-local
|
|
25
|
+
* `require` convention. Returns `undefined` for anything the jail rejects —
|
|
26
|
+
* never throws, so a hostile or malformed module path degrades to the same
|
|
27
|
+
* "no such module" outcome an ordinary miss gets.
|
|
28
|
+
*/
|
|
29
|
+
function normalizeModulePath(modulePath) {
|
|
30
|
+
const withExtension = modulePath.endsWith('.lua')
|
|
31
|
+
? modulePath
|
|
32
|
+
: `${modulePath}.lua`;
|
|
33
|
+
const result = normalizeBundlePath(withExtension);
|
|
34
|
+
return result.ok ? result.path : undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds a `PackModuleResolver` (`@markii/lua`) backed by a pre-loaded
|
|
38
|
+
* `PackModulesMap`. Every lookup is read with `Object.hasOwn` only (never
|
|
39
|
+
* bare indexing) so a pack namespace or module path shaped like
|
|
40
|
+
* `__proto__`/`constructor` can never resolve through the prototype chain,
|
|
41
|
+
* matching this codebase's hostile-map discipline throughout
|
|
42
|
+
* `@markii/pack`/`@markii/bundle`.
|
|
43
|
+
*
|
|
44
|
+
* Returns `undefined` (a clean "no such module", per `PackModuleResolver`'s
|
|
45
|
+
* own contract) for: an unconfigured pack namespace, a module path the
|
|
46
|
+
* bundle-jail rejects (traversal, absolute path, null byte, backslash), or
|
|
47
|
+
* a module path not present in the map. Never throws.
|
|
48
|
+
*/
|
|
49
|
+
export function createPackModuleResolver(modules) {
|
|
50
|
+
return (packName, modulePath) => {
|
|
51
|
+
if (!Object.hasOwn(modules, packName))
|
|
52
|
+
return undefined;
|
|
53
|
+
const packModules = modules[packName];
|
|
54
|
+
if (packModules == null)
|
|
55
|
+
return undefined;
|
|
56
|
+
const normalized = normalizeModulePath(modulePath);
|
|
57
|
+
if (normalized === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (!Object.hasOwn(packModules, normalized))
|
|
60
|
+
return undefined;
|
|
61
|
+
return packModules[normalized];
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WORKER half of the net bridge (see `./net-bridge.ts` for the
|
|
3
|
+
* protocol and the host half).
|
|
4
|
+
*
|
|
5
|
+
* Separate from its other half for one reason: it needs
|
|
6
|
+
* `@markii/lua`'s `netProviderDenial` to rebrand a host-side refusal, and
|
|
7
|
+
* that import pulls the whole WebAssembly Lua engine in behind it. The
|
|
8
|
+
* worker bundle contains that engine anyway; a host bundle must not.
|
|
9
|
+
*/
|
|
10
|
+
import { type NetProvider } from '@markii/lua';
|
|
11
|
+
import { type NetBridgeRequest } from './net-bridge.js';
|
|
12
|
+
/**
|
|
13
|
+
* The worker-side `NetProvider`: every call becomes one request message and
|
|
14
|
+
* resolves when its reply arrives. `post` on the returned object is what
|
|
15
|
+
* the worker uses to send; the caller routes incoming replies through
|
|
16
|
+
* `handleMessage`.
|
|
17
|
+
*
|
|
18
|
+
* There is deliberately NO timeout here. The run's wall-clock budget is
|
|
19
|
+
* already enforced from outside the isolate by `run-host.ts`'s watchdog,
|
|
20
|
+
* which terminates the whole worker; a second, inner timeout would only
|
|
21
|
+
* add a way for the two to disagree about whether a run is still alive.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createNetBridge(post: (message: NetBridgeRequest) => void): {
|
|
24
|
+
provider: NetProvider;
|
|
25
|
+
handleMessage: (message: unknown) => void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WORKER half of the net bridge (see `./net-bridge.ts` for the
|
|
3
|
+
* protocol and the host half).
|
|
4
|
+
*
|
|
5
|
+
* Separate from its other half for one reason: it needs
|
|
6
|
+
* `@markii/lua`'s `netProviderDenial` to rebrand a host-side refusal, and
|
|
7
|
+
* that import pulls the whole WebAssembly Lua engine in behind it. The
|
|
8
|
+
* worker bundle contains that engine anyway; a host bundle must not.
|
|
9
|
+
*/
|
|
10
|
+
import { netProviderDenial } from '@markii/lua';
|
|
11
|
+
import { isNetBridgeReply } from './net-bridge.js';
|
|
12
|
+
/**
|
|
13
|
+
* The worker-side `NetProvider`: every call becomes one request message and
|
|
14
|
+
* resolves when its reply arrives. `post` on the returned object is what
|
|
15
|
+
* the worker uses to send; the caller routes incoming replies through
|
|
16
|
+
* `handleMessage`.
|
|
17
|
+
*
|
|
18
|
+
* There is deliberately NO timeout here. The run's wall-clock budget is
|
|
19
|
+
* already enforced from outside the isolate by `run-host.ts`'s watchdog,
|
|
20
|
+
* which terminates the whole worker; a second, inner timeout would only
|
|
21
|
+
* add a way for the two to disagree about whether a run is still alive.
|
|
22
|
+
*/
|
|
23
|
+
export function createNetBridge(post) {
|
|
24
|
+
let nextId = 1;
|
|
25
|
+
const pending = new Map();
|
|
26
|
+
const request = (method, url, body) => new Promise((resolve, reject) => {
|
|
27
|
+
const id = nextId++;
|
|
28
|
+
pending.set(id, { resolve, reject });
|
|
29
|
+
try {
|
|
30
|
+
post({
|
|
31
|
+
kind: 'markii:net-request',
|
|
32
|
+
id,
|
|
33
|
+
method,
|
|
34
|
+
url,
|
|
35
|
+
...(body !== undefined ? { body } : {}),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
pending.delete(id);
|
|
40
|
+
reject(error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
provider: {
|
|
45
|
+
get: (url) => request('get', url),
|
|
46
|
+
post: (url, body) => request('post', url, body),
|
|
47
|
+
patch: (url, body) => request('patch', url, body),
|
|
48
|
+
},
|
|
49
|
+
handleMessage: (message) => {
|
|
50
|
+
if (!isNetBridgeReply(message))
|
|
51
|
+
return;
|
|
52
|
+
const waiter = pending.get(message.id);
|
|
53
|
+
if (!waiter)
|
|
54
|
+
return;
|
|
55
|
+
pending.delete(message.id);
|
|
56
|
+
if (message.ok && message.response) {
|
|
57
|
+
waiter.resolve(message.response);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const text = message.error ?? 'network request failed';
|
|
61
|
+
// Rebrand a host-side policy refusal so `@markii/lua` classifies it
|
|
62
|
+
// exactly as it would have inside a Node worker.
|
|
63
|
+
waiter.reject(message.denied ? netProviderDenial(text) : new Error(text));
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The network half of a Web Worker isolate.
|
|
3
|
+
*
|
|
4
|
+
* A Web Worker has no `node:https` and no `node:dns`, so the pinned
|
|
5
|
+
* request that closes the DNS-rebinding gap (issue #10, docs/security.md)
|
|
6
|
+
* cannot run inside one. Rather than drop pinning for hosts that only have
|
|
7
|
+
* Web Workers, the request moves OUT of the isolate: the worker asks its
|
|
8
|
+
* host to perform it, and the host, which still has Node, runs exactly the
|
|
9
|
+
* same `createNetProvider` the worker-thread isolate always used.
|
|
10
|
+
*
|
|
11
|
+
* This is a security improvement, not a compromise. The allowlist and the
|
|
12
|
+
* pinning policy now live somewhere the sandboxed code cannot reach at
|
|
13
|
+
* all: an isolate with no network stack has nothing to bypass them with.
|
|
14
|
+
* What the worker can do is ask, and every ask is answered by the host's
|
|
15
|
+
* own provider, against the host's own allowlist.
|
|
16
|
+
*
|
|
17
|
+
* This file is the protocol and the HOST half. The worker half lives in
|
|
18
|
+
* `./net-bridge-worker.ts` because it needs `@markii/lua`'s denial brand,
|
|
19
|
+
* and a host bundle must not drag a WebAssembly Lua engine in behind a
|
|
20
|
+
* message type — see `./net-provider.ts` for the same reasoning applied to
|
|
21
|
+
* the pinned request itself.
|
|
22
|
+
*/
|
|
23
|
+
import type { NetProvider, NetResponse } from '@markii/lua';
|
|
24
|
+
/** Worker -> host. `id` correlates the reply; `method` selects the provider call. */
|
|
25
|
+
export interface NetBridgeRequest {
|
|
26
|
+
kind: 'markii:net-request';
|
|
27
|
+
id: number;
|
|
28
|
+
method: 'get' | 'post' | 'patch';
|
|
29
|
+
url: string;
|
|
30
|
+
body?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Host -> worker. A failure crosses as a MESSAGE plus a `denied` flag, never
|
|
34
|
+
* as a structured-cloned `Error`: the branded denial `@markii/lua` checks
|
|
35
|
+
* for is a JS `Symbol` on an Error object, and a `Symbol` does not survive
|
|
36
|
+
* structured cloning. The flag is what lets the worker side rebuild a
|
|
37
|
+
* properly branded denial, so a host-side policy refusal still classifies
|
|
38
|
+
* as `'capability-denied'` rather than degrading to a generic script error.
|
|
39
|
+
*/
|
|
40
|
+
export interface NetBridgeReply {
|
|
41
|
+
kind: 'markii:net-reply';
|
|
42
|
+
id: number;
|
|
43
|
+
ok: boolean;
|
|
44
|
+
response?: NetResponse;
|
|
45
|
+
error?: string;
|
|
46
|
+
denied?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare function isNetBridgeRequest(value: unknown): value is NetBridgeRequest;
|
|
49
|
+
export declare function isNetBridgeReply(value: unknown): value is NetBridgeReply;
|
|
50
|
+
/**
|
|
51
|
+
* The host-side half: answers one request message using `provider`, and
|
|
52
|
+
* hands the reply to `post`. Never throws — a failure becomes a reply
|
|
53
|
+
* carrying the message, because a worker waiting on a reply that never
|
|
54
|
+
* comes would hang until the watchdog kills it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function serveNetRequest(request: NetBridgeRequest, provider: NetProvider, post: (reply: NetBridgeReply) => void): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function isNetBridgeRequest(value) {
|
|
2
|
+
if (typeof value !== 'object' || value === null)
|
|
3
|
+
return false;
|
|
4
|
+
const v = value;
|
|
5
|
+
return (v.kind === 'markii:net-request' &&
|
|
6
|
+
typeof v.id === 'number' &&
|
|
7
|
+
typeof v.url === 'string' &&
|
|
8
|
+
(v.method === 'get' || v.method === 'post' || v.method === 'patch'));
|
|
9
|
+
}
|
|
10
|
+
export function isNetBridgeReply(value) {
|
|
11
|
+
if (typeof value !== 'object' || value === null)
|
|
12
|
+
return false;
|
|
13
|
+
const v = value;
|
|
14
|
+
return v.kind === 'markii:net-reply' && typeof v.id === 'number';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The host-side half: answers one request message using `provider`, and
|
|
18
|
+
* hands the reply to `post`. Never throws — a failure becomes a reply
|
|
19
|
+
* carrying the message, because a worker waiting on a reply that never
|
|
20
|
+
* comes would hang until the watchdog kills it.
|
|
21
|
+
*/
|
|
22
|
+
export async function serveNetRequest(request, provider, post) {
|
|
23
|
+
try {
|
|
24
|
+
const call = request.method === 'get'
|
|
25
|
+
? provider.get(request.url)
|
|
26
|
+
: request.method === 'post'
|
|
27
|
+
? provider.post?.(request.url, request.body ?? '')
|
|
28
|
+
: provider.patch?.(request.url, request.body ?? '');
|
|
29
|
+
if (!call) {
|
|
30
|
+
post({
|
|
31
|
+
kind: 'markii:net-reply',
|
|
32
|
+
id: request.id,
|
|
33
|
+
ok: false,
|
|
34
|
+
error: `${request.method} is not available`,
|
|
35
|
+
denied: true,
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
post({
|
|
40
|
+
kind: 'markii:net-reply',
|
|
41
|
+
id: request.id,
|
|
42
|
+
ok: true,
|
|
43
|
+
response: await call,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
post({
|
|
48
|
+
kind: 'markii:net-reply',
|
|
49
|
+
id: request.id,
|
|
50
|
+
ok: false,
|
|
51
|
+
error: error instanceof Error ? error.message : String(error),
|
|
52
|
+
// Everything the host's provider refuses IS a policy denial: the
|
|
53
|
+
// provider's own failures are allowlist, redirect, size, and pinning
|
|
54
|
+
// refusals. Marking them so keeps the worker's classification
|
|
55
|
+
// identical to the in-worker case.
|
|
56
|
+
denied: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|