@markii/host 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +19 -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 +84 -4
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve-then-pin for the run path's network jail (GitHub issue #10).
|
|
3
|
+
*
|
|
4
|
+
* A network grant is a hostname string. Between granting the name and
|
|
5
|
+
* reaching it, two things can go wrong that a hostname check cannot see:
|
|
6
|
+
*
|
|
7
|
+
* - the name can resolve somewhere the user never intended (a public name
|
|
8
|
+
* answering with a private address, which is SSRF against their own
|
|
9
|
+
* machine or LAN, including the cloud metadata address);
|
|
10
|
+
* - the record can change between the check and the connection (DNS
|
|
11
|
+
* rebinding), so even a checked name can be connected to somewhere else.
|
|
12
|
+
*
|
|
13
|
+
* This module closes both by resolving a host ONCE, vetting every address the
|
|
14
|
+
* resolver returned, and handing back the single address the request must
|
|
15
|
+
* then be pinned to. `docs/security.md` documented these as accepted limits
|
|
16
|
+
* of a hostname allowlist; this is the code that closes them.
|
|
17
|
+
*
|
|
18
|
+
* Pure apart from the injected `lookup`, so every branch is testable without
|
|
19
|
+
* a resolver or a socket.
|
|
20
|
+
*/
|
|
21
|
+
import { type AddressScope } from './ip-address.js';
|
|
22
|
+
/** One address a resolver returned. Matches `dns.promises.lookup(host, { all: true })`'s shape. */
|
|
23
|
+
export interface ResolvedAddress {
|
|
24
|
+
address: string;
|
|
25
|
+
family: number;
|
|
26
|
+
}
|
|
27
|
+
/** Resolves a hostname to every address it has. Injected so tests never touch a real resolver. */
|
|
28
|
+
export type HostLookup = (hostname: string) => Promise<ResolvedAddress[]>;
|
|
29
|
+
/** The address a request is pinned to, and what kind of place it is. */
|
|
30
|
+
export interface PinnedAddress {
|
|
31
|
+
address: string;
|
|
32
|
+
family: 4 | 6;
|
|
33
|
+
scope: AddressScope;
|
|
34
|
+
/** True when the grant named an address (or `localhost`) directly, so no name-to-address surprise was possible. */
|
|
35
|
+
literal: boolean;
|
|
36
|
+
}
|
|
37
|
+
export type PinResult = {
|
|
38
|
+
ok: true;
|
|
39
|
+
pinned: PinnedAddress;
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
reason: string;
|
|
43
|
+
};
|
|
44
|
+
export interface PinPolicy {
|
|
45
|
+
/**
|
|
46
|
+
* The deployment opt-in from issue #10: allow a NAMED host to resolve into
|
|
47
|
+
* a private, loopback, or link-local range. Off by default, and off is the
|
|
48
|
+
* posture that closes the SSRF case. A deployment whose internal DNS
|
|
49
|
+
* legitimately points names at RFC 1918 space (a corporate `git.internal`)
|
|
50
|
+
* turns it on knowingly.
|
|
51
|
+
*/
|
|
52
|
+
allowRestrictedAddresses: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Vets `hostname` and returns the one address the caller must connect to.
|
|
56
|
+
*
|
|
57
|
+
* The rule, in one sentence: a name may only be reached at a public address,
|
|
58
|
+
* unless the grant itself named a literal address or `localhost` (in which
|
|
59
|
+
* case the user chose that address knowingly, and the prompt says so), or the
|
|
60
|
+
* deployment has opted in.
|
|
61
|
+
*
|
|
62
|
+
* A host whose resolver returns a mix of public and restricted addresses is
|
|
63
|
+
* refused OUTRIGHT rather than filtered down to the public ones. Picking the
|
|
64
|
+
* public address out of a mixed answer is precisely what a rebinding attacker
|
|
65
|
+
* wants: it keeps their name reachable while they wait for the pin to expire.
|
|
66
|
+
*/
|
|
67
|
+
export declare function pinHostAddress(hostname: string, policy: PinPolicy, lookup: HostLookup): Promise<PinResult>;
|
|
68
|
+
/**
|
|
69
|
+
* The `lookup` a pinned request installs on its own connection. Node calls it
|
|
70
|
+
* instead of the real resolver, so the socket can only ever go to
|
|
71
|
+
* `pinned.address` — the resolve-then-connect gap is closed by construction
|
|
72
|
+
* rather than by re-checking after the fact.
|
|
73
|
+
*
|
|
74
|
+
* The callback has two shapes and BOTH must be handled: Node's own
|
|
75
|
+
* `net.connect` calls this with `{ all: true }` and expects an array, while
|
|
76
|
+
* the single-address form is what a caller passing `{ all: false }` gets.
|
|
77
|
+
* Answering only the single form throws `ERR_INVALID_IP_ADDRESS` inside
|
|
78
|
+
* Node's connect path (verified against Node 22).
|
|
79
|
+
*/
|
|
80
|
+
export declare function pinnedLookup(pinned: PinnedAddress): (_hostname: string, options: {
|
|
81
|
+
all?: boolean;
|
|
82
|
+
}, callback: (err: NodeJS.ErrnoException | null, address: string | ResolvedAddress[], family?: number) => void) => void;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve-then-pin for the run path's network jail (GitHub issue #10).
|
|
3
|
+
*
|
|
4
|
+
* A network grant is a hostname string. Between granting the name and
|
|
5
|
+
* reaching it, two things can go wrong that a hostname check cannot see:
|
|
6
|
+
*
|
|
7
|
+
* - the name can resolve somewhere the user never intended (a public name
|
|
8
|
+
* answering with a private address, which is SSRF against their own
|
|
9
|
+
* machine or LAN, including the cloud metadata address);
|
|
10
|
+
* - the record can change between the check and the connection (DNS
|
|
11
|
+
* rebinding), so even a checked name can be connected to somewhere else.
|
|
12
|
+
*
|
|
13
|
+
* This module closes both by resolving a host ONCE, vetting every address the
|
|
14
|
+
* resolver returned, and handing back the single address the request must
|
|
15
|
+
* then be pinned to. `docs/security.md` documented these as accepted limits
|
|
16
|
+
* of a hostname allowlist; this is the code that closes them.
|
|
17
|
+
*
|
|
18
|
+
* Pure apart from the injected `lookup`, so every branch is testable without
|
|
19
|
+
* a resolver or a socket.
|
|
20
|
+
*/
|
|
21
|
+
import { classifyIpAddress, describeScope, isRestrictedScope, } from './ip-address.js';
|
|
22
|
+
/**
|
|
23
|
+
* `localhost` is special-cased alongside IP literals, per RFC 6761: it and
|
|
24
|
+
* anything under `.localhost` are defined to mean the loopback interface, so
|
|
25
|
+
* granting the name IS granting loopback. There is no surprise to protect the
|
|
26
|
+
* user from and no rebinding to prevent, since the name is not resolved on
|
|
27
|
+
* the network at all.
|
|
28
|
+
*/
|
|
29
|
+
function isLoopbackName(hostname) {
|
|
30
|
+
const lower = hostname.toLowerCase();
|
|
31
|
+
return lower === 'localhost' || lower.endsWith('.localhost');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Vets `hostname` and returns the one address the caller must connect to.
|
|
35
|
+
*
|
|
36
|
+
* The rule, in one sentence: a name may only be reached at a public address,
|
|
37
|
+
* unless the grant itself named a literal address or `localhost` (in which
|
|
38
|
+
* case the user chose that address knowingly, and the prompt says so), or the
|
|
39
|
+
* deployment has opted in.
|
|
40
|
+
*
|
|
41
|
+
* A host whose resolver returns a mix of public and restricted addresses is
|
|
42
|
+
* refused OUTRIGHT rather than filtered down to the public ones. Picking the
|
|
43
|
+
* public address out of a mixed answer is precisely what a rebinding attacker
|
|
44
|
+
* wants: it keeps their name reachable while they wait for the pin to expire.
|
|
45
|
+
*/
|
|
46
|
+
export async function pinHostAddress(hostname, policy, lookup) {
|
|
47
|
+
const literalScope = classifyIpAddress(hostname);
|
|
48
|
+
if (literalScope !== undefined) {
|
|
49
|
+
const bare = hostname.startsWith('[') && hostname.endsWith(']')
|
|
50
|
+
? hostname.slice(1, -1)
|
|
51
|
+
: hostname;
|
|
52
|
+
// The grant named this exact address: there is nothing to resolve, so
|
|
53
|
+
// there is nothing to pin against and no range to second-guess.
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
pinned: {
|
|
57
|
+
address: bare,
|
|
58
|
+
family: bare.includes(':') ? 6 : 4,
|
|
59
|
+
scope: literalScope,
|
|
60
|
+
literal: true,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (isLoopbackName(hostname)) {
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
pinned: {
|
|
68
|
+
address: '127.0.0.1',
|
|
69
|
+
family: 4,
|
|
70
|
+
scope: 'loopback',
|
|
71
|
+
literal: true,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
let addresses;
|
|
76
|
+
try {
|
|
77
|
+
addresses = await lookup(hostname);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Deliberately not the resolver's own message: it varies by platform and
|
|
81
|
+
// says nothing a note's author can act on.
|
|
82
|
+
return { ok: false, reason: `host "${hostname}" could not be resolved` };
|
|
83
|
+
}
|
|
84
|
+
if (addresses.length === 0) {
|
|
85
|
+
return { ok: false, reason: `host "${hostname}" resolved to no address` };
|
|
86
|
+
}
|
|
87
|
+
for (const candidate of addresses) {
|
|
88
|
+
const scope = classifyIpAddress(candidate.address);
|
|
89
|
+
if (scope === undefined) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: `host "${hostname}" resolved to an unusable address`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (isRestrictedScope(scope) && !policy.allowRestrictedAddresses) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
reason: `host "${hostname}" resolves to ${describeScope(scope)}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const chosen = addresses[0];
|
|
103
|
+
if (chosen === undefined) {
|
|
104
|
+
return { ok: false, reason: `host "${hostname}" resolved to no address` };
|
|
105
|
+
}
|
|
106
|
+
const scope = classifyIpAddress(chosen.address) ?? 'reserved';
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
pinned: {
|
|
110
|
+
address: chosen.address,
|
|
111
|
+
family: chosen.family === 6 ? 6 : 4,
|
|
112
|
+
scope,
|
|
113
|
+
literal: false,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The `lookup` a pinned request installs on its own connection. Node calls it
|
|
119
|
+
* instead of the real resolver, so the socket can only ever go to
|
|
120
|
+
* `pinned.address` — the resolve-then-connect gap is closed by construction
|
|
121
|
+
* rather than by re-checking after the fact.
|
|
122
|
+
*
|
|
123
|
+
* The callback has two shapes and BOTH must be handled: Node's own
|
|
124
|
+
* `net.connect` calls this with `{ all: true }` and expects an array, while
|
|
125
|
+
* the single-address form is what a caller passing `{ all: false }` gets.
|
|
126
|
+
* Answering only the single form throws `ERR_INVALID_IP_ADDRESS` inside
|
|
127
|
+
* Node's connect path (verified against Node 22).
|
|
128
|
+
*/
|
|
129
|
+
export function pinnedLookup(pinned) {
|
|
130
|
+
return (_hostname, options, callback) => {
|
|
131
|
+
if (options.all === true) {
|
|
132
|
+
callback(null, [{ address: pinned.address, family: pinned.family }]);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
callback(null, pinned.address, pinned.family);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { NetProvider } from '@markii/lua';
|
|
2
|
+
import { type HostLookup, type PinPolicy } from './net-pinning.js';
|
|
3
|
+
/**
|
|
4
|
+
* The worker's `NetProvider` (`@markii/lua`): built on `node:http`/
|
|
5
|
+
* `node:https` rather than global `fetch` (GitHub issue #10) — `fetch` has
|
|
6
|
+
* no way to pin where a request's socket connects short of installing an
|
|
7
|
+
* undici dispatcher, and adding a dependency for that is out of scope
|
|
8
|
+
* (AGENTS.md's Stack list). `node:http`/`node:https`'s own `lookup` request
|
|
9
|
+
* option does the same job without one — see `issueRequest`'s doc comment
|
|
10
|
+
* for what it does and does not affect.
|
|
11
|
+
*
|
|
12
|
+
* One allowlist governs `get`/`post`/`patch` alike (docs/security.md: "the
|
|
13
|
+
* per-host allowlist is the real boundary", not a GET/POST distinction).
|
|
14
|
+
* This is DEFENSE IN DEPTH, not the primary gate — the primary gate is
|
|
15
|
+
* `netGrants` passed to `createLuaExecutor` below, which `@markii/lua`'s
|
|
16
|
+
* `buildCapabilities` already enforces before this provider is ever called
|
|
17
|
+
* (a disallowed host never reaches here at all; it comes back as the
|
|
18
|
+
* standard `'capability-denied'` failure kind, recorded through
|
|
19
|
+
* `@markii/lua`'s non-spoofable denial-recording path — see
|
|
20
|
+
* `capabilities.ts`).
|
|
21
|
+
*
|
|
22
|
+
* Per hop of a redirect chain, in this order:
|
|
23
|
+
* 1. the scheme must be `http:` or `https:` — anything else is refused;
|
|
24
|
+
* 2. a URL carrying credentials (`https://user:pass@host/...`) is refused
|
|
25
|
+
* outright (N-4) — `fetch` used to give this for free by refusing to
|
|
26
|
+
* construct a credentialed `Request`; this loop checks it explicitly
|
|
27
|
+
* instead, on every hop, not only the first;
|
|
28
|
+
* 3. the hop's hostname is checked against `allowlist` — unchanged from
|
|
29
|
+
* before this port. An allowed host redirecting the request elsewhere
|
|
30
|
+
* is exactly the SSRF shape a host-string allowlist is meant to close,
|
|
31
|
+
* and `buildCapabilities` only ever sees the ORIGINAL request URL,
|
|
32
|
+
* never where a 3xx response actually sent the request. A hop landing
|
|
33
|
+
* on a non-allowed host is refused WITHOUT that hop's request ever
|
|
34
|
+
* being made (B-1);
|
|
35
|
+
* 4. `pinHostAddress` (`./net-pinning.ts`) resolves that hostname ONCE and
|
|
36
|
+
* vets every address it got back against `policy` — the new
|
|
37
|
+
* DNS-rebinding/private-range close from issue #10: a hostname
|
|
38
|
+
* allowlist alone cannot see where a name actually resolves, or that
|
|
39
|
+
* the record can change between the check and the connect;
|
|
40
|
+
* 5. the request is issued pinned to the vetted address (`issueRequest`),
|
|
41
|
+
* so the socket can only ever reach where step 4 vetted — the
|
|
42
|
+
* resolve-then-connect gap is closed by construction, not by
|
|
43
|
+
* re-checking after the fact.
|
|
44
|
+
*
|
|
45
|
+
* The response body is read bounded to `maxFetchBytes`, never buffered
|
|
46
|
+
* whole first — see `issueRequest` (B-2).
|
|
47
|
+
*/
|
|
48
|
+
export declare function createNetProvider(allowlist: readonly string[], maxFetchBytes: number, policy: PinPolicy,
|
|
49
|
+
/**
|
|
50
|
+
* How a POLICY refusal is built. The worker passes `@markii/lua`'s
|
|
51
|
+
* `netProviderDenial`, whose `Symbol` brand is what makes a refusal
|
|
52
|
+
* classify as `'capability-denied'` instead of a generic script error.
|
|
53
|
+
*
|
|
54
|
+
* It is injected rather than imported so this module's network half does
|
|
55
|
+
* not drag the Lua runtime in behind it. The Obsidian plugin runs this
|
|
56
|
+
* provider in its RENDERER, on behalf of a Web Worker isolate that has no
|
|
57
|
+
* `node:dns` to pin with, and that renderer bundle must not contain a
|
|
58
|
+
* WebAssembly Lua engine it never runs. The brand is not lost there: the
|
|
59
|
+
* host marks every refusal on the wire (`./net-bridge.ts`'s `denied`
|
|
60
|
+
* flag) and the worker rebuilds a properly branded error on arrival.
|
|
61
|
+
*/
|
|
62
|
+
denial: (message: string) => Error, lookup?: HostLookup): NetProvider;
|
|
63
|
+
/**
|
|
64
|
+
* An in-memory `CacheProvider` (`@markii/lua`) seeded from `snapshot` at
|
|
65
|
+
* construction and readable back out afterward via `.snapshot()` — the
|
|
66
|
+
* "snapshot-in/snapshot-out" design from the locked run-arc comment. No
|
|
67
|
+
* disk/IndexedDB access here: persisting the returned snapshot across runs
|
|
68
|
+
* is `run-host.ts`'s caller's job (extension storage, in the real
|
|
69
|
+
* extension).
|
|
70
|
+
*/
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pinned network provider: the code that actually performs a `net.*`
|
|
3
|
+
* request, follows redirects, re-checks every hop against the allowlist,
|
|
4
|
+
* and connects to an address it vetted itself (GitHub issue #10,
|
|
5
|
+
* docs/security.md).
|
|
6
|
+
*
|
|
7
|
+
* It lives in its own module, with NO `@markii/lua` runtime import, because
|
|
8
|
+
* it now runs in two very different places. In the VS Code extension it
|
|
9
|
+
* runs inside the worker thread, as it always has. In the Obsidian plugin
|
|
10
|
+
* it runs in the RENDERER, on behalf of a Web Worker isolate that has no
|
|
11
|
+
* `node:dns` to pin with and therefore cannot do this for itself (see
|
|
12
|
+
* `./browser-isolate.ts`). That renderer bundle must not contain a
|
|
13
|
+
* WebAssembly Lua engine it never runs, so the one value this file used to
|
|
14
|
+
* import from `@markii/lua` — the brand that marks a policy refusal — is
|
|
15
|
+
* injected as `denial` instead. Types are `import type` and erase.
|
|
16
|
+
*
|
|
17
|
+
* The security properties are unchanged by the move, and one improves: an
|
|
18
|
+
* isolate that has no network stack cannot bypass the allowlist enforced
|
|
19
|
+
* here, because it has nothing to bypass it with.
|
|
20
|
+
*/
|
|
21
|
+
import * as http from 'node:http';
|
|
22
|
+
import * as https from 'node:https';
|
|
23
|
+
import * as dns from 'node:dns';
|
|
24
|
+
import { pinHostAddress, pinnedLookup, } from './net-pinning.js';
|
|
25
|
+
function describeThrown(err) {
|
|
26
|
+
return err instanceof Error ? err.message : String(err);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A same-hop, same-host redirect chain is capped at this many hops (B-1):
|
|
30
|
+
* an allowed host is free to redirect a handful of times (a login/CDN
|
|
31
|
+
* bounce is common), but an unbounded chain is itself a resource-abuse
|
|
32
|
+
* shape worth refusing outright rather than following forever.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_REDIRECTS = 5;
|
|
35
|
+
/**
|
|
36
|
+
* The resolver `createNetProvider` pins against when the host does not
|
|
37
|
+
* inject one (GitHub issue #10): every address a real DNS lookup returns
|
|
38
|
+
* for a hostname, in the shape `pinHostAddress` (`./net-pinning.ts`)
|
|
39
|
+
* expects. `all: true` is what makes this the FULL set of addresses a name
|
|
40
|
+
* resolves to, rather than just the one Node's default resolution would
|
|
41
|
+
* pick — vetting only the address Node happened to choose is exactly the
|
|
42
|
+
* incomplete check a rebinding attacker relies on. `verbatim: true` asks
|
|
43
|
+
* Node not to reorder that set for connection-preference reasons, which
|
|
44
|
+
* have nothing to do with vetting it.
|
|
45
|
+
*/
|
|
46
|
+
const defaultHostLookup = async (hostname) => {
|
|
47
|
+
const results = await dns.promises.lookup(hostname, {
|
|
48
|
+
all: true,
|
|
49
|
+
verbatim: true,
|
|
50
|
+
});
|
|
51
|
+
return results.map((entry) => ({
|
|
52
|
+
address: entry.address,
|
|
53
|
+
family: entry.family,
|
|
54
|
+
}));
|
|
55
|
+
};
|
|
56
|
+
function bareHostname(hostname) {
|
|
57
|
+
return hostname.startsWith('[') && hostname.endsWith(']')
|
|
58
|
+
? hostname.slice(1, -1)
|
|
59
|
+
: hostname;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Issues exactly one HTTP(S) request, pinned to `pinned.address` via
|
|
63
|
+
* `pinnedLookup` (`./net-pinning.ts`, GitHub issue #10) — Node's `lookup`
|
|
64
|
+
* request option is what makes this possible without `fetch` (which has no
|
|
65
|
+
* such hook short of an undici dispatcher, and adding a dependency for that
|
|
66
|
+
* is out of scope; see `createNetProvider`'s doc comment). The socket can
|
|
67
|
+
* only ever connect to `pinned.address`, while `url.hostname` still drives
|
|
68
|
+
* the `Host` header and, for `https:`, the TLS certificate check and SNI —
|
|
69
|
+
* `rejectUnauthorized` is never touched here, so a certificate that does
|
|
70
|
+
* not cover the real hostname still fails the handshake (verified
|
|
71
|
+
* empirically against Node 22). `agent: false` so no pooled connection is
|
|
72
|
+
* ever reused across hosts or hops.
|
|
73
|
+
*
|
|
74
|
+
* The response body is read bounded to `maxFetchBytes` (B-2): a
|
|
75
|
+
* `content-length` header over the cap is rejected WITHOUT reading
|
|
76
|
+
* anything, and otherwise the read is aborted — the response socket is
|
|
77
|
+
* destroyed — the moment the running byte total exceeds the cap. The whole
|
|
78
|
+
* body is never buffered before that check runs; `chunks` only ever holds
|
|
79
|
+
* bytes already known to be within the cap.
|
|
80
|
+
*/
|
|
81
|
+
function issueRequest(url, method, body, pinned, maxFetchBytes,
|
|
82
|
+
/** See `createNetProvider`'s own `denial` parameter — injected for the same reason. */
|
|
83
|
+
denial) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
86
|
+
const bodyBuffer = body !== undefined ? Buffer.from(body, 'utf8') : undefined;
|
|
87
|
+
// The headers `fetch` used to add on its own, restored explicitly now
|
|
88
|
+
// that this path builds the request itself. They are not cosmetic: with
|
|
89
|
+
// no `user-agent`, api.github.com answers 403 ("Please make sure your
|
|
90
|
+
// request has a User-Agent"), which would break every note fetching the
|
|
91
|
+
// GitHub API while every local-server test still passed. The values are
|
|
92
|
+
// exactly what Node's `fetch` sent for a string body, so this port
|
|
93
|
+
// changes nothing that leaves the machine.
|
|
94
|
+
const headers = {
|
|
95
|
+
'user-agent': 'node',
|
|
96
|
+
accept: '*/*',
|
|
97
|
+
};
|
|
98
|
+
if (bodyBuffer) {
|
|
99
|
+
headers['content-length'] = String(bodyBuffer.byteLength);
|
|
100
|
+
headers['content-type'] = 'text/plain;charset=UTF-8';
|
|
101
|
+
}
|
|
102
|
+
// Exactly-once settlement: several of the events below (`res`'s
|
|
103
|
+
// 'error' after a deliberate `destroy()`, `req`'s 'error' after the
|
|
104
|
+
// response already resolved) can fire in close succession once
|
|
105
|
+
// something has been torn down early. Every later one is a no-op.
|
|
106
|
+
let settled = false;
|
|
107
|
+
const settleResolve = (value) => {
|
|
108
|
+
if (settled)
|
|
109
|
+
return;
|
|
110
|
+
settled = true;
|
|
111
|
+
resolve(value);
|
|
112
|
+
};
|
|
113
|
+
const settleReject = (error) => {
|
|
114
|
+
if (settled)
|
|
115
|
+
return;
|
|
116
|
+
settled = true;
|
|
117
|
+
reject(error);
|
|
118
|
+
};
|
|
119
|
+
const req = mod.request({
|
|
120
|
+
protocol: url.protocol,
|
|
121
|
+
hostname: bareHostname(url.hostname),
|
|
122
|
+
port: url.port === '' ? undefined : Number(url.port),
|
|
123
|
+
path: `${url.pathname}${url.search}`,
|
|
124
|
+
method,
|
|
125
|
+
headers,
|
|
126
|
+
agent: false,
|
|
127
|
+
// Pins the socket to the vetted address (issue #10) without
|
|
128
|
+
// touching the `Host` header or the TLS certificate check — both
|
|
129
|
+
// are still driven by `hostname` above.
|
|
130
|
+
lookup: pinnedLookup(pinned),
|
|
131
|
+
}, (res) => {
|
|
132
|
+
const declaredLength = res.headers['content-length'];
|
|
133
|
+
if (declaredLength !== undefined) {
|
|
134
|
+
const declared = Number(declaredLength);
|
|
135
|
+
if (Number.isFinite(declared) && declared > maxFetchBytes) {
|
|
136
|
+
res.destroy();
|
|
137
|
+
settleReject(denial(`response declares ${declared} bytes, exceeding the ${maxFetchBytes}-byte cap`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let total = 0;
|
|
142
|
+
const chunks = [];
|
|
143
|
+
res.on('data', (chunk) => {
|
|
144
|
+
if (settled)
|
|
145
|
+
return;
|
|
146
|
+
total += chunk.byteLength;
|
|
147
|
+
if (total > maxFetchBytes) {
|
|
148
|
+
res.destroy();
|
|
149
|
+
settleReject(denial(`response exceeds the ${maxFetchBytes}-byte cap`));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
chunks.push(chunk);
|
|
153
|
+
});
|
|
154
|
+
res.on('end', () => {
|
|
155
|
+
settleResolve({
|
|
156
|
+
status: res.statusCode ?? 0,
|
|
157
|
+
location: res.headers.location,
|
|
158
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
res.on('error', (err) => settleReject(err));
|
|
162
|
+
});
|
|
163
|
+
req.on('error', (err) => {
|
|
164
|
+
settleReject(denial(`request failed: ${describeThrown(err)}`));
|
|
165
|
+
});
|
|
166
|
+
if (bodyBuffer)
|
|
167
|
+
req.write(bodyBuffer);
|
|
168
|
+
req.end();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The worker's `NetProvider` (`@markii/lua`): built on `node:http`/
|
|
173
|
+
* `node:https` rather than global `fetch` (GitHub issue #10) — `fetch` has
|
|
174
|
+
* no way to pin where a request's socket connects short of installing an
|
|
175
|
+
* undici dispatcher, and adding a dependency for that is out of scope
|
|
176
|
+
* (AGENTS.md's Stack list). `node:http`/`node:https`'s own `lookup` request
|
|
177
|
+
* option does the same job without one — see `issueRequest`'s doc comment
|
|
178
|
+
* for what it does and does not affect.
|
|
179
|
+
*
|
|
180
|
+
* One allowlist governs `get`/`post`/`patch` alike (docs/security.md: "the
|
|
181
|
+
* per-host allowlist is the real boundary", not a GET/POST distinction).
|
|
182
|
+
* This is DEFENSE IN DEPTH, not the primary gate — the primary gate is
|
|
183
|
+
* `netGrants` passed to `createLuaExecutor` below, which `@markii/lua`'s
|
|
184
|
+
* `buildCapabilities` already enforces before this provider is ever called
|
|
185
|
+
* (a disallowed host never reaches here at all; it comes back as the
|
|
186
|
+
* standard `'capability-denied'` failure kind, recorded through
|
|
187
|
+
* `@markii/lua`'s non-spoofable denial-recording path — see
|
|
188
|
+
* `capabilities.ts`).
|
|
189
|
+
*
|
|
190
|
+
* Per hop of a redirect chain, in this order:
|
|
191
|
+
* 1. the scheme must be `http:` or `https:` — anything else is refused;
|
|
192
|
+
* 2. a URL carrying credentials (`https://user:pass@host/...`) is refused
|
|
193
|
+
* outright (N-4) — `fetch` used to give this for free by refusing to
|
|
194
|
+
* construct a credentialed `Request`; this loop checks it explicitly
|
|
195
|
+
* instead, on every hop, not only the first;
|
|
196
|
+
* 3. the hop's hostname is checked against `allowlist` — unchanged from
|
|
197
|
+
* before this port. An allowed host redirecting the request elsewhere
|
|
198
|
+
* is exactly the SSRF shape a host-string allowlist is meant to close,
|
|
199
|
+
* and `buildCapabilities` only ever sees the ORIGINAL request URL,
|
|
200
|
+
* never where a 3xx response actually sent the request. A hop landing
|
|
201
|
+
* on a non-allowed host is refused WITHOUT that hop's request ever
|
|
202
|
+
* being made (B-1);
|
|
203
|
+
* 4. `pinHostAddress` (`./net-pinning.ts`) resolves that hostname ONCE and
|
|
204
|
+
* vets every address it got back against `policy` — the new
|
|
205
|
+
* DNS-rebinding/private-range close from issue #10: a hostname
|
|
206
|
+
* allowlist alone cannot see where a name actually resolves, or that
|
|
207
|
+
* the record can change between the check and the connect;
|
|
208
|
+
* 5. the request is issued pinned to the vetted address (`issueRequest`),
|
|
209
|
+
* so the socket can only ever reach where step 4 vetted — the
|
|
210
|
+
* resolve-then-connect gap is closed by construction, not by
|
|
211
|
+
* re-checking after the fact.
|
|
212
|
+
*
|
|
213
|
+
* The response body is read bounded to `maxFetchBytes`, never buffered
|
|
214
|
+
* whole first — see `issueRequest` (B-2).
|
|
215
|
+
*/
|
|
216
|
+
export function createNetProvider(allowlist, maxFetchBytes, policy,
|
|
217
|
+
/**
|
|
218
|
+
* How a POLICY refusal is built. The worker passes `@markii/lua`'s
|
|
219
|
+
* `netProviderDenial`, whose `Symbol` brand is what makes a refusal
|
|
220
|
+
* classify as `'capability-denied'` instead of a generic script error.
|
|
221
|
+
*
|
|
222
|
+
* It is injected rather than imported so this module's network half does
|
|
223
|
+
* not drag the Lua runtime in behind it. The Obsidian plugin runs this
|
|
224
|
+
* provider in its RENDERER, on behalf of a Web Worker isolate that has no
|
|
225
|
+
* `node:dns` to pin with, and that renderer bundle must not contain a
|
|
226
|
+
* WebAssembly Lua engine it never runs. The brand is not lost there: the
|
|
227
|
+
* host marks every refusal on the wire (`./net-bridge.ts`'s `denied`
|
|
228
|
+
* flag) and the worker rebuilds a properly branded error on arrival.
|
|
229
|
+
*/
|
|
230
|
+
denial, lookup = defaultHostLookup) {
|
|
231
|
+
const allowed = new Set(allowlist.map((host) => host.toLowerCase()));
|
|
232
|
+
async function fetchAllowed(startUrl, method, body) {
|
|
233
|
+
let currentUrl = startUrl;
|
|
234
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
235
|
+
let parsed;
|
|
236
|
+
try {
|
|
237
|
+
parsed = new URL(currentUrl);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
throw denial(`"${currentUrl}" is not a valid URL`);
|
|
241
|
+
}
|
|
242
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
243
|
+
throw denial(`scheme "${parsed.protocol}" is not allowed`);
|
|
244
|
+
}
|
|
245
|
+
// N-4: a URL carrying credentials is refused outright, on every hop.
|
|
246
|
+
if (parsed.username !== '' || parsed.password !== '') {
|
|
247
|
+
throw denial(`URL "${currentUrl}" embeds credentials, which is not allowed`);
|
|
248
|
+
}
|
|
249
|
+
const host = parsed.hostname.toLowerCase();
|
|
250
|
+
if (!allowed.has(host)) {
|
|
251
|
+
throw denial(`host "${host}" is not on the run's allowlist`);
|
|
252
|
+
}
|
|
253
|
+
// Issue #10: resolve-then-pin, closing the DNS-rebinding / private-
|
|
254
|
+
// range SSRF gap a hostname allowlist alone cannot see.
|
|
255
|
+
const pinResult = await pinHostAddress(host, policy, lookup);
|
|
256
|
+
if (!pinResult.ok) {
|
|
257
|
+
throw denial(pinResult.reason);
|
|
258
|
+
}
|
|
259
|
+
const response = await issueRequest(parsed, method, body, pinResult.pinned, maxFetchBytes, denial);
|
|
260
|
+
if (response.status >= 300 && response.status < 400) {
|
|
261
|
+
if (!response.location) {
|
|
262
|
+
throw denial('redirect response carried no Location header');
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
currentUrl = new URL(response.location, currentUrl).toString();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
throw denial('redirect response carried an unparseable Location header');
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
return { status: response.status, body: response.body };
|
|
273
|
+
}
|
|
274
|
+
throw denial(`exceeded ${MAX_REDIRECTS} redirects`);
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
get: (url) => fetchAllowed(url, 'GET', undefined),
|
|
278
|
+
post: (url, requestBody) => fetchAllowed(url, 'POST', requestBody),
|
|
279
|
+
patch: (url, requestBody) => fetchAllowed(url, 'PATCH', requestBody),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* An in-memory `CacheProvider` (`@markii/lua`) seeded from `snapshot` at
|
|
284
|
+
* construction and readable back out afterward via `.snapshot()` — the
|
|
285
|
+
* "snapshot-in/snapshot-out" design from the locked run-arc comment. No
|
|
286
|
+
* disk/IndexedDB access here: persisting the returned snapshot across runs
|
|
287
|
+
* is `run-host.ts`'s caller's job (extension storage, in the real
|
|
288
|
+
* extension).
|
|
289
|
+
*/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IsolateSpawner } from './isolate.js';
|
|
2
|
+
import type { SpawnRunOptions } from './run-host.js';
|
|
3
|
+
import type { RunResult } from './worker-entry.js';
|
|
4
|
+
/**
|
|
5
|
+
* The ONLY fallback this shared package supplies for an unset `workerPath`:
|
|
6
|
+
* the sibling `worker-entry.ts`, run straight from source via `tsx`
|
|
7
|
+
* (`TSX_DEV_EXEC_ARGV`, above). That is correct for this package's own
|
|
8
|
+
* dev/Vitest runs — `__dirname` at that point is this real source
|
|
9
|
+
* directory — but it says nothing about where a packaged host's bundled
|
|
10
|
+
* worker lives, because this package does not know that host's `dist/`
|
|
11
|
+
* layout. A packaged host (e.g. `apps/vscode`) MUST resolve its own bundled
|
|
12
|
+
* worker path itself and pass it as `SpawnRunOptions.workerPath`; see
|
|
13
|
+
* `apps/vscode/src/worker-path.ts` for that host's resolution and
|
|
14
|
+
* `apps/vscode/esbuild.config.mjs`'s worker build for where it bundles to.
|
|
15
|
+
*/
|
|
16
|
+
export declare function defaultWorkerPath(): string;
|
|
17
|
+
/**
|
|
18
|
+
* The default isolate: a `node:worker_threads` worker, which is what every
|
|
19
|
+
* Node host (the VS Code extension host, this package's own tests) uses. It
|
|
20
|
+
* is the only kind that accepts `resourceLimits`, so it is also the only
|
|
21
|
+
* kind whose V8 heap is bounded — see `./isolate.ts` for why a host might
|
|
22
|
+
* still have to supply the other one.
|
|
23
|
+
*/
|
|
24
|
+
export declare const workerThreadIsolate: IsolateSpawner;
|
|
25
|
+
/**
|
|
26
|
+
* `spawnRun` with the Node defaults filled in: the worker-thread isolate and
|
|
27
|
+
* the dev/Vitest worker entry. For this package's own tests and any Node
|
|
28
|
+
* caller that wants them; a packaged host passes its own `workerPath`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function spawnRunInThread(options: Omit<SpawnRunOptions, 'spawnIsolate' | 'workerPath'> & Partial<Pick<SpawnRunOptions, 'spawnIsolate' | 'workerPath'>>): Promise<RunResult>;
|