@markii/host 0.17.0-next.2 → 0.17.0-next.4
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 +10 -2
- package/dist/browser-worker.classic.js +640 -0
- package/dist/browser-worker.js +40 -40
- package/dist/browser.d.ts +7 -3
- package/dist/browser.js +16 -4
- package/dist/conformance/host/05-export-html-pack-directive/expected.json +2 -0
- package/dist/conformance/host/06-install-pack-then-render/expected.json +6 -1
- package/dist/conformance/host/README.md +4 -2
- package/dist/export/note-export.d.ts +1 -1
- package/dist/host/adapter.d.ts +22 -25
- package/dist/host/conformance-runner.d.ts +1 -1
- package/dist/host/conformance-runner.js +21 -11
- package/dist/host/create-host.d.ts +29 -11
- package/dist/host/create-host.js +34 -23
- package/dist/host/editor-behavior.d.ts +2 -6
- package/dist/host/editor-behavior.js +5 -9
- package/dist/host/export-behavior.d.ts +6 -7
- package/dist/host/export-behavior.js +1 -1
- package/dist/host/labels.d.ts +5 -15
- package/dist/host/labels.js +8 -22
- package/dist/host/pack-archive.d.ts +4 -10
- package/dist/host/pack-archive.js +12 -21
- package/dist/host/pack-install.d.ts +11 -23
- package/dist/host/pack-install.js +26 -23
- package/dist/host/pack-load.d.ts +1 -2
- package/dist/host/pack-trust-behavior.js +1 -1
- package/dist/host/refresh-interval.d.ts +1 -3
- package/dist/host/refresh-interval.js +1 -3
- package/dist/host/run-behavior.d.ts +3 -4
- package/dist/host/run-behavior.js +23 -2
- package/dist/host/script-execution.d.ts +1 -1
- package/dist/html-engine.js +4 -4
- package/dist/index.d.ts +15 -80
- package/dist/index.js +18 -63
- package/dist/internal.d.ts +30 -0
- package/dist/internal.js +31 -0
- package/dist/node-fs.d.ts +0 -3
- package/dist/node-fs.js +1 -21
- package/dist/node-worker.d.ts +8 -0
- package/dist/node-worker.js +20678 -0
- package/dist/node.d.ts +3 -3
- package/dist/node.js +5 -4
- package/dist/packs/discover.d.ts +2 -0
- package/dist/packs/discover.js +2 -1
- package/dist/packs/pack-diagnostics.d.ts +6 -8
- package/dist/packs/pack-diagnostics.js +6 -8
- package/dist/packs/pack-paths.js +1 -2
- package/dist/packs/pack-render-registry.d.ts +9 -10
- package/dist/run/browser-isolate.d.ts +5 -5
- package/dist/run/browser-isolate.js +5 -1
- package/dist/run/isolate.d.ts +3 -1
- package/dist/run/net-bridge.d.ts +5 -5
- package/dist/run/net-bridge.js +7 -5
- package/dist/run/net-pinning.d.ts +2 -0
- package/dist/run/net-pinning.js +17 -3
- package/dist/run/net-provider.d.ts +3 -1
- package/dist/run/net-provider.js +40 -15
- package/dist/run/net-refusal.d.ts +4 -0
- package/dist/run/net-refusal.js +24 -0
- package/dist/run/node-isolate.d.ts +13 -21
- package/dist/run/node-isolate.js +31 -80
- package/dist/run/run-flow.d.ts +1 -2
- package/dist/run/run-host.d.ts +3 -3
- package/dist/run/worker-entry-browser.d.ts +24 -4
- package/dist/run/worker-entry-browser.js +99 -4
- package/dist/run/worker-entry.js +2 -0
- package/package.json +31 -12
package/dist/node.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { SpawnRunOptions } from './run/run-host.js';
|
|
2
|
-
export {
|
|
2
|
+
export type { NodeHostIsolateOptions } from './run/node-isolate.js';
|
|
3
|
+
export { createNodeHostIsolate, workerThreadIsolate, } from './run/node-isolate.js';
|
|
3
4
|
export { createNetProvider } from './run/net-provider.js';
|
|
4
|
-
export {
|
|
5
|
-
export { createHtmlExportEngine } from './html-engine.js';
|
|
5
|
+
export { createNodeDirectoryLister, createNodeFileReader, createNodePackDiscoveryIo, createNodeReader as createNodePackScriptsReader, } from './node-fs.js';
|
package/dist/node.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// The main entry (`./index.ts`) is environment-neutral. This entry adds:
|
|
5
5
|
// - the worker_threads isolate and the pinned network provider (`node:*`);
|
|
6
6
|
// - the Node-backed file-system seams the neutral code takes by injection;
|
|
7
|
-
//
|
|
7
|
+
//
|
|
8
|
+
// The `@markii/html`-backed static export engine is its own entry,
|
|
9
|
+
// `@markii/host/html-engine`, so importing this one needs no optional peer.
|
|
8
10
|
//
|
|
9
11
|
// A Node host imports from both entries. No browser bundle imports this one.
|
|
10
|
-
export {
|
|
12
|
+
export { createNodeHostIsolate, workerThreadIsolate, } from './run/node-isolate.js';
|
|
11
13
|
export { createNetProvider } from './run/net-provider.js';
|
|
12
|
-
export {
|
|
13
|
-
export { createHtmlExportEngine } from './html-engine.js';
|
|
14
|
+
export { createNodeDirectoryLister, createNodeFileReader, createNodePackDiscoveryIo, createNodeReader as createNodePackScriptsReader, } from './node-fs.js';
|
package/dist/packs/discover.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface DiscoveredPack {
|
|
|
30
30
|
/** The configured folder this pack was discovered in (absolute path, as given). */
|
|
31
31
|
readonly folder: string;
|
|
32
32
|
readonly manifest: PackManifest;
|
|
33
|
+
/** The exact text of the pack's `pack.json`, as read. A compiled script registers this text unchanged. Discovery always sets it; it is absent only on a pack a host assembled by hand. */
|
|
34
|
+
readonly manifestText?: string;
|
|
33
35
|
/** Local component name -> absolute path to its declared source file (`manifest.components[name]`, resolved against `folder`). Informational: what a host consumes them for (bundling, `import()`, or locating the pack's prebuilt registration script) is that host's own concern. */
|
|
34
36
|
readonly componentPaths: Readonly<Record<string, string>>;
|
|
35
37
|
/** Engine id -> (local component name -> absolute source path), for every engine any component declares a source for. `componentPaths` is the `react` slice of this. A terminal build (`ansi`) reads its slice here. */
|
package/dist/packs/discover.js
CHANGED
|
@@ -102,6 +102,7 @@ async function tryLoadPackAt(folder, readFile) {
|
|
|
102
102
|
pack: {
|
|
103
103
|
folder,
|
|
104
104
|
manifest: result.manifest,
|
|
105
|
+
manifestText: text,
|
|
105
106
|
componentPaths,
|
|
106
107
|
componentPathsByEngine,
|
|
107
108
|
scriptsDir: joinPath(folder, 'scripts'),
|
|
@@ -131,7 +132,7 @@ async function tryLoadPackAt(folder, readFile) {
|
|
|
131
132
|
*/
|
|
132
133
|
/**
|
|
133
134
|
* When a `ManifestAttempt` succeeded (`kind: 'found'`) but the manifest's
|
|
134
|
-
* `engine` does not match `supportedEngine
|
|
135
|
+
* `engine` does not match `supportedEngine`, downgrades it to
|
|
135
136
|
* a `SkippedPackFolder` instead of a `DiscoveredPack` — same effect
|
|
136
137
|
* `@markii/react`'s `loadPack` already has for an unsupported engine
|
|
137
138
|
* (nothing registered for that namespace, unknown-component fallback for
|
|
@@ -34,7 +34,7 @@ export interface PackDiagnosticsContext {
|
|
|
34
34
|
/**
|
|
35
35
|
* One entry per validated, non-colliding pack whose declared engine
|
|
36
36
|
* `@markii/react`'s `loadPack` cannot run (`./pack-render-registry.ts`'s
|
|
37
|
-
* `BuildRenderRegistryResult.droppedEngines
|
|
37
|
+
* `BuildRenderRegistryResult.droppedEngines`). Omitted (or
|
|
38
38
|
* empty) contributes nothing — the ordinary case where every registered
|
|
39
39
|
* pack's engine is supported.
|
|
40
40
|
*/
|
|
@@ -62,13 +62,11 @@ export declare function skippedPackCount(context: {
|
|
|
62
62
|
}): number;
|
|
63
63
|
/**
|
|
64
64
|
* One informational line for a pack whose prebuilt `webview.js` shadows
|
|
65
|
-
* component sources still present in the same folder
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* Export Pack command versus, from Obsidian, VS Code's Export Pack command
|
|
71
|
-
* by name, since Obsidian has no build command of its own).
|
|
65
|
+
* component sources still present in the same folder.
|
|
66
|
+
* The tail clause names how a user would rebuild the prebuilt artifact
|
|
67
|
+
* from those sources: VS Code's own Export Pack command, or, from
|
|
68
|
+
* Obsidian, VS Code's Export Pack command by name, since Obsidian has no
|
|
69
|
+
* build command of its own.
|
|
72
70
|
*
|
|
73
71
|
* Never a failure: shipping both the built artifact and its sources is a
|
|
74
72
|
* supported distribution shape. `rebuildInstruction` is the one thing that
|
|
@@ -53,7 +53,7 @@ function duplicateComposedNameLines(context) {
|
|
|
53
53
|
const duplicates = context.duplicateComposedNames ?? [];
|
|
54
54
|
return duplicates.map((duplicate) => `Pack "${duplicate.skippedPack}"'s component composed to the directive name "${duplicate.composedName}", already claimed by pack "${duplicate.keptPack}"; the later component was skipped.`);
|
|
55
55
|
}
|
|
56
|
-
/** One line per pack dropped because this renderer cannot run its declared engine
|
|
56
|
+
/** One line per pack dropped because this renderer cannot run its declared engine — `[]` when there was none. */
|
|
57
57
|
function droppedEngineLines(context) {
|
|
58
58
|
const dropped = context.droppedEngines ?? [];
|
|
59
59
|
return dropped.map((entry) => `Pack "${entry.name}" declares engine "${entry.engine}", which this renderer cannot run; it installed with zero components (every directive under its namespace falls back to the unknown-component box).`);
|
|
@@ -87,13 +87,11 @@ export function skippedPackCount(context) {
|
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
89
|
* One informational line for a pack whose prebuilt `webview.js` shadows
|
|
90
|
-
* component sources still present in the same folder
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* Export Pack command versus, from Obsidian, VS Code's Export Pack command
|
|
96
|
-
* by name, since Obsidian has no build command of its own).
|
|
90
|
+
* component sources still present in the same folder.
|
|
91
|
+
* The tail clause names how a user would rebuild the prebuilt artifact
|
|
92
|
+
* from those sources: VS Code's own Export Pack command, or, from
|
|
93
|
+
* Obsidian, VS Code's Export Pack command by name, since Obsidian has no
|
|
94
|
+
* build command of its own.
|
|
97
95
|
*
|
|
98
96
|
* Never a failure: shipping both the built artifact and its sources is a
|
|
99
97
|
* supported distribution shape. `rebuildInstruction` is the one thing that
|
package/dist/packs/pack-paths.js
CHANGED
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* host concern — reading the setting itself, and naming what "relative"
|
|
13
13
|
* means in a diagnostic message, stays in each host's own app code.
|
|
14
14
|
*
|
|
15
|
-
* DECISION
|
|
16
|
-
* Code module this was hoisted from): a relative entry is resolved against
|
|
15
|
+
* DECISION: a relative entry is resolved against
|
|
17
16
|
* `baseDir` as given by the caller; an absolute entry is used as-is. This
|
|
18
17
|
* matches how a user reads the setting's own description: "this pack
|
|
19
18
|
* folder, relative to my project/vault".
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import type { PackManifest } from '@markii/pack';
|
|
2
|
-
import type { Registry, RegistryEntry } from '@markii/react';
|
|
3
2
|
/**
|
|
4
3
|
* The renderer-specific half of building a pack-aware registry: the
|
|
5
4
|
* renderer's own `loadPack`, `createRegistry` and `mergeRegistries`. A
|
|
6
5
|
* React host passes `@markii/react`'s `reactPackEngine`; the host layer
|
|
7
6
|
* itself names no renderer.
|
|
8
7
|
*/
|
|
9
|
-
export interface PackRegistryEngine {
|
|
10
|
-
loadPack(manifest: PackManifest, componentModules: Record<string,
|
|
11
|
-
registry:
|
|
8
|
+
export interface PackRegistryEngine<TRegistry extends object = object, TEntry = unknown> {
|
|
9
|
+
loadPack(manifest: PackManifest, componentModules: Record<string, TEntry>): {
|
|
10
|
+
registry: TRegistry;
|
|
12
11
|
dropped?: {
|
|
13
12
|
name: string;
|
|
14
13
|
engine: string;
|
|
15
14
|
};
|
|
16
15
|
};
|
|
17
|
-
createRegistry(entries?: Record<string,
|
|
18
|
-
mergeRegistries(...registries:
|
|
16
|
+
createRegistry(entries?: Record<string, TEntry>): TRegistry;
|
|
17
|
+
mergeRegistries(...registries: TRegistry[]): TRegistry;
|
|
19
18
|
}
|
|
20
19
|
/** One registration a compiled pack script queued by calling `window.__markiiRegisterPack(manifestJson, componentModules)` (see `./pack-build.ts`'s top doc comment for the full registration convention). Structurally identical across every host that satisfies the convention. */
|
|
21
20
|
export interface QueuedPackRegistration {
|
|
@@ -42,8 +41,8 @@ export interface DuplicateComposedName {
|
|
|
42
41
|
readonly skippedPack: string;
|
|
43
42
|
}
|
|
44
43
|
/** What `buildRenderRegistry` reports alongside the merged registry — developer-facing diagnostics for a caller to fold into its own surface, never shown as page content. */
|
|
45
|
-
export interface BuildRenderRegistryResult {
|
|
46
|
-
readonly registry:
|
|
44
|
+
export interface BuildRenderRegistryResult<TRegistry extends object = object> {
|
|
45
|
+
readonly registry: TRegistry;
|
|
47
46
|
/** One line per malformed registration, dropped rather than installed. */
|
|
48
47
|
readonly invalidReasons: readonly string[];
|
|
49
48
|
/** Non-empty only when two or more validated registrations shared a namespace — the WHOLE install was then rejected (docs/packs.md's install-time rejection rule) and `registry` falls back to `defaultRegistry` alone. */
|
|
@@ -52,7 +51,7 @@ export interface BuildRenderRegistryResult {
|
|
|
52
51
|
readonly duplicateComposedNames: readonly DuplicateComposedName[];
|
|
53
52
|
/**
|
|
54
53
|
* One entry per validated, non-colliding pack whose manifest declares an
|
|
55
|
-
* engine `@markii/react`'s `loadPack` cannot run
|
|
54
|
+
* engine `@markii/react`'s `loadPack` cannot run. Before
|
|
56
55
|
* this field existed, such a pack installed as an empty contribution
|
|
57
56
|
* with no record anywhere that it had even been attempted — every
|
|
58
57
|
* directive under its namespace fell through to the ordinary
|
|
@@ -80,4 +79,4 @@ export interface BuildRenderRegistryResult {
|
|
|
80
79
|
* (see `DuplicateComposedName`) drops only the later claimant's
|
|
81
80
|
* component, not the whole pack.
|
|
82
81
|
*/
|
|
83
|
-
export declare function buildRenderRegistry(queued: readonly QueuedPackRegistration[], defaultRegistry:
|
|
82
|
+
export declare function buildRenderRegistry<TRegistry extends object, TEntry>(queued: readonly QueuedPackRegistration[], defaultRegistry: TRegistry, engine: PackRegistryEngine<TRegistry, TEntry>): BuildRenderRegistryResult<TRegistry>;
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* there is no `__dirname` inside a worker to find it beside the bundle.
|
|
14
14
|
*
|
|
15
15
|
* Network is served by the HOST, not the worker: `serveNetRequests` wires
|
|
16
|
-
* every `markii:net-request` to the caller's own `NetProvider
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* every `markii:net-request` to the caller's own `NetProvider`. Lua cannot
|
|
17
|
+
* reach JavaScript, and the worker entry removes its own network globals
|
|
18
|
+
* before the first script runs (`./worker-entry-browser.ts`), so the
|
|
19
|
+
* provider's allowlist is the only path to the network.
|
|
20
20
|
*
|
|
21
21
|
* What this CANNOT do, stated plainly because it is a real difference: a
|
|
22
22
|
* Web Worker takes no `maxOldGenerationSizeMb`, so `options`'s heap cap is
|
|
@@ -37,7 +37,7 @@ export interface BrowserIsolateOptions {
|
|
|
37
37
|
/** Creates the worker. Injected so a test never needs a real one, and so a host can decide how it mints the URL. */
|
|
38
38
|
createWorker: (entryPath: string) => WorkerLike;
|
|
39
39
|
/** The host's own pinned provider. Every `net.*` call the script makes is answered by this, in the host, never in the isolate. */
|
|
40
|
-
netProvider: (netAllowlist: string[], maxFetchBytes: number, netPolicy: unknown) => NetProvider;
|
|
40
|
+
netProvider: (netAllowlist: string[], maxFetchBytes: number, netPolicy: unknown, signal?: AbortSignal) => NetProvider;
|
|
41
41
|
/** Where the worker should fetch wasmoon's `glue.wasm`; sent along with the job. */
|
|
42
42
|
wasmUri?: string;
|
|
43
43
|
}
|
|
@@ -19,6 +19,9 @@ export function createBrowserIsolate(options) {
|
|
|
19
19
|
const worker = options.createWorker(spawn.entryPath);
|
|
20
20
|
let provider;
|
|
21
21
|
let alive = true;
|
|
22
|
+
// Aborted by `kill()`: a request still in flight when the run ends is
|
|
23
|
+
// cancelled rather than left running in the host.
|
|
24
|
+
const requests = new AbortController();
|
|
22
25
|
const messageListeners = [];
|
|
23
26
|
const errorListeners = [];
|
|
24
27
|
worker.addEventListener('message', (event) => {
|
|
@@ -52,7 +55,7 @@ export function createBrowserIsolate(options) {
|
|
|
52
55
|
return {
|
|
53
56
|
send: (job) => {
|
|
54
57
|
const { allowlist, maxFetchBytes, policy } = netArgsFrom(job);
|
|
55
|
-
provider = options.netProvider(allowlist, maxFetchBytes, policy);
|
|
58
|
+
provider = options.netProvider(allowlist, maxFetchBytes, policy, requests.signal);
|
|
56
59
|
worker.postMessage({
|
|
57
60
|
...job,
|
|
58
61
|
...(options.wasmUri !== undefined
|
|
@@ -64,6 +67,7 @@ export function createBrowserIsolate(options) {
|
|
|
64
67
|
if (!alive)
|
|
65
68
|
return;
|
|
66
69
|
alive = false;
|
|
70
|
+
requests.abort();
|
|
67
71
|
worker.terminate();
|
|
68
72
|
},
|
|
69
73
|
onMessage: (listener) => {
|
package/dist/run/isolate.d.ts
CHANGED
|
@@ -46,10 +46,12 @@ export interface RunIsolate {
|
|
|
46
46
|
onExit(listener: (code: number | null) => void): void;
|
|
47
47
|
}
|
|
48
48
|
export interface SpawnIsolateOptions {
|
|
49
|
-
/** The entry the isolate loads — resolved by the host, never guessed here (
|
|
49
|
+
/** The entry the isolate loads — resolved by the host, never guessed here (`createNodeHostIsolate` supplies the packaged one). */
|
|
50
50
|
entryPath: string;
|
|
51
51
|
/** Node `execArgv` for a worker-thread isolate; meaningless to a Web Worker, which ignores it. */
|
|
52
52
|
execArgv?: string[];
|
|
53
|
+
/** Environment for a worker-thread isolate; a Web Worker ignores it. */
|
|
54
|
+
env?: Record<string, string | undefined>;
|
|
53
55
|
/** V8 old-space cap in MB. A worker thread honors it; a Web Worker cannot, and says so in its own doc comment. */
|
|
54
56
|
maxOldGenerationSizeMb: number;
|
|
55
57
|
}
|
package/dist/run/net-bridge.d.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* host to perform it, and the host, which still has Node, runs exactly the
|
|
9
9
|
* same `createNetProvider` the worker-thread isolate always used.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* The allowlist and the pinning policy live somewhere the sandboxed code
|
|
12
|
+
* cannot reach: Lua has no path to JavaScript, and the worker entry removes
|
|
13
|
+
* its network globals after startup (`./worker-entry-browser.ts`). What the
|
|
14
|
+
* worker can do is ask, and every ask is answered by the host's own
|
|
15
|
+
* provider, against the host's own allowlist.
|
|
16
16
|
*
|
|
17
17
|
* This file is the protocol and the HOST half. The worker half lives in
|
|
18
18
|
* `./net-bridge-worker.ts` because it needs `@markii/lua`'s denial brand,
|
package/dist/run/net-bridge.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isNetRefusal } from './net-refusal.js';
|
|
1
2
|
export function isNetBridgeRequest(value) {
|
|
2
3
|
if (typeof value !== 'object' || value === null)
|
|
3
4
|
return false;
|
|
@@ -49,11 +50,12 @@ export async function serveNetRequest(request, provider, post) {
|
|
|
49
50
|
id: request.id,
|
|
50
51
|
ok: false,
|
|
51
52
|
error: error instanceof Error ? error.message : String(error),
|
|
52
|
-
//
|
|
53
|
-
// provider
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
53
|
+
// A policy refusal (allowlist, redirect, size, address range) is
|
|
54
|
+
// marked by the provider and crosses as a denial, so the worker
|
|
55
|
+
// classifies it exactly as it would in-worker. A transport failure
|
|
56
|
+
// (DNS, connection, timeout, abort) is unmarked and crosses as an
|
|
57
|
+
// ordinary failure with its reason. Both reach the script as errors.
|
|
58
|
+
denied: isNetRefusal(error),
|
|
57
59
|
});
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -40,6 +40,8 @@ export type PinResult = {
|
|
|
40
40
|
} | {
|
|
41
41
|
ok: false;
|
|
42
42
|
reason: string;
|
|
43
|
+
/** `unresolved`: the name did not resolve (a transport failure). `refused`: the address policy refused it. */
|
|
44
|
+
kind: 'unresolved' | 'refused';
|
|
43
45
|
};
|
|
44
46
|
export interface PinPolicy {
|
|
45
47
|
/**
|
package/dist/run/net-pinning.js
CHANGED
|
@@ -79,29 +79,43 @@ export async function pinHostAddress(hostname, policy, lookup) {
|
|
|
79
79
|
catch {
|
|
80
80
|
// Deliberately not the resolver's own message: it varies by platform and
|
|
81
81
|
// says nothing a note's author can act on.
|
|
82
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
kind: 'unresolved',
|
|
85
|
+
reason: `host "${hostname}" could not be resolved`,
|
|
86
|
+
};
|
|
83
87
|
}
|
|
84
88
|
if (addresses.length === 0) {
|
|
85
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
kind: 'unresolved',
|
|
92
|
+
reason: `host "${hostname}" resolved to no address`,
|
|
93
|
+
};
|
|
86
94
|
}
|
|
87
95
|
for (const candidate of addresses) {
|
|
88
96
|
const scope = classifyIpAddress(candidate.address);
|
|
89
97
|
if (scope === undefined) {
|
|
90
98
|
return {
|
|
91
99
|
ok: false,
|
|
100
|
+
kind: 'refused',
|
|
92
101
|
reason: `host "${hostname}" resolved to an unusable address`,
|
|
93
102
|
};
|
|
94
103
|
}
|
|
95
104
|
if (isRestrictedScope(scope) && !policy.allowRestrictedAddresses) {
|
|
96
105
|
return {
|
|
97
106
|
ok: false,
|
|
107
|
+
kind: 'refused',
|
|
98
108
|
reason: `host "${hostname}" resolves to ${describeScope(scope)}`,
|
|
99
109
|
};
|
|
100
110
|
}
|
|
101
111
|
}
|
|
102
112
|
const chosen = addresses[0];
|
|
103
113
|
if (chosen === undefined) {
|
|
104
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
kind: 'unresolved',
|
|
117
|
+
reason: `host "${hostname}" resolved to no address`,
|
|
118
|
+
};
|
|
105
119
|
}
|
|
106
120
|
const scope = classifyIpAddress(chosen.address) ?? 'reserved';
|
|
107
121
|
return {
|
|
@@ -59,7 +59,9 @@ export declare function createNetProvider(allowlist: readonly string[], maxFetch
|
|
|
59
59
|
* host marks every refusal on the wire (`./net-bridge.ts`'s `denied`
|
|
60
60
|
* flag) and the worker rebuilds a properly branded error on arrival.
|
|
61
61
|
*/
|
|
62
|
-
denial: (message: string) => Error, lookup?: HostLookup
|
|
62
|
+
denial: (message: string) => Error, lookup?: HostLookup,
|
|
63
|
+
/** Aborted when the run ends: an in-flight request is destroyed and no further hop is made. */
|
|
64
|
+
signal?: AbortSignal): NetProvider;
|
|
63
65
|
/**
|
|
64
66
|
* An in-memory `CacheProvider` (`@markii/lua`) seeded from `snapshot` at
|
|
65
67
|
* construction and readable back out afterward via `.snapshot()` — the
|
package/dist/run/net-provider.js
CHANGED
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
* import from `@markii/lua` — the brand that marks a policy refusal — is
|
|
15
15
|
* injected as `denial` instead. Types are `import type` and erase.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* here
|
|
17
|
+
* A Web Worker isolate asks this provider through the net bridge. The
|
|
18
|
+
* worker entry removes its own network globals after startup, so the
|
|
19
|
+
* allowlist enforced here is the only path to the network.
|
|
20
20
|
*/
|
|
21
21
|
import * as http from 'node:http';
|
|
22
22
|
import * as https from 'node:https';
|
|
23
23
|
import * as dns from 'node:dns';
|
|
24
|
+
import { netRefusal } from './net-refusal.js';
|
|
24
25
|
import { pinHostAddress, pinnedLookup, } from './net-pinning.js';
|
|
25
26
|
function describeThrown(err) {
|
|
26
27
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -80,7 +81,7 @@ function bareHostname(hostname) {
|
|
|
80
81
|
*/
|
|
81
82
|
function issueRequest(url, method, body, pinned, maxFetchBytes,
|
|
82
83
|
/** See `createNetProvider`'s own `denial` parameter — injected for the same reason. */
|
|
83
|
-
denial) {
|
|
84
|
+
denial, signal) {
|
|
84
85
|
return new Promise((resolve, reject) => {
|
|
85
86
|
const mod = url.protocol === 'https:' ? https : http;
|
|
86
87
|
const bodyBuffer = body !== undefined ? Buffer.from(body, 'utf8') : undefined;
|
|
@@ -161,8 +162,20 @@ denial) {
|
|
|
161
162
|
res.on('error', (err) => settleReject(err));
|
|
162
163
|
});
|
|
163
164
|
req.on('error', (err) => {
|
|
164
|
-
|
|
165
|
+
// A transport failure (connection refused, reset, timeout): an
|
|
166
|
+
// ordinary runtime failure with the reason, not a policy refusal.
|
|
167
|
+
settleReject(new Error(`request failed: ${describeThrown(err)}`));
|
|
165
168
|
});
|
|
169
|
+
if (signal !== undefined) {
|
|
170
|
+
const onAbort = () => {
|
|
171
|
+
req.destroy();
|
|
172
|
+
settleReject(new Error('the run has ended'));
|
|
173
|
+
};
|
|
174
|
+
if (signal.aborted)
|
|
175
|
+
onAbort();
|
|
176
|
+
else
|
|
177
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
178
|
+
}
|
|
166
179
|
if (bodyBuffer)
|
|
167
180
|
req.write(bodyBuffer);
|
|
168
181
|
req.end();
|
|
@@ -227,8 +240,14 @@ export function createNetProvider(allowlist, maxFetchBytes, policy,
|
|
|
227
240
|
* host marks every refusal on the wire (`./net-bridge.ts`'s `denied`
|
|
228
241
|
* flag) and the worker rebuilds a properly branded error on arrival.
|
|
229
242
|
*/
|
|
230
|
-
denial, lookup = defaultHostLookup
|
|
243
|
+
denial, lookup = defaultHostLookup,
|
|
244
|
+
/** Aborted when the run ends: an in-flight request is destroyed and no further hop is made. */
|
|
245
|
+
signal) {
|
|
231
246
|
const allowed = new Set(allowlist.map((host) => host.toLowerCase()));
|
|
247
|
+
// Every policy refusal is built by the injected `denial` and then marked,
|
|
248
|
+
// so the net bridge can tell it from a transport failure without importing
|
|
249
|
+
// the Lua runtime. A transport failure is a plain `Error`.
|
|
250
|
+
const refuse = (message) => netRefusal(denial(message));
|
|
232
251
|
async function fetchAllowed(startUrl, method, body) {
|
|
233
252
|
let currentUrl = startUrl;
|
|
234
253
|
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
@@ -237,41 +256,47 @@ denial, lookup = defaultHostLookup) {
|
|
|
237
256
|
parsed = new URL(currentUrl);
|
|
238
257
|
}
|
|
239
258
|
catch {
|
|
240
|
-
throw
|
|
259
|
+
throw refuse(`"${currentUrl}" is not a valid URL`);
|
|
241
260
|
}
|
|
242
261
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
243
|
-
throw
|
|
262
|
+
throw refuse(`scheme "${parsed.protocol}" is not allowed`);
|
|
244
263
|
}
|
|
245
264
|
// N-4: a URL carrying credentials is refused outright, on every hop.
|
|
246
265
|
if (parsed.username !== '' || parsed.password !== '') {
|
|
247
|
-
throw
|
|
266
|
+
throw refuse(`URL "${currentUrl}" embeds credentials, which is not allowed`);
|
|
248
267
|
}
|
|
268
|
+
if (signal?.aborted)
|
|
269
|
+
throw new Error('the run has ended');
|
|
249
270
|
const host = parsed.hostname.toLowerCase();
|
|
250
271
|
if (!allowed.has(host)) {
|
|
251
|
-
throw
|
|
272
|
+
throw refuse(`host "${host}" is not on the run's allowlist`);
|
|
252
273
|
}
|
|
253
274
|
// Issue #10: resolve-then-pin, closing the DNS-rebinding / private-
|
|
254
275
|
// range SSRF gap a hostname allowlist alone cannot see.
|
|
255
276
|
const pinResult = await pinHostAddress(host, policy, lookup);
|
|
256
277
|
if (!pinResult.ok) {
|
|
257
|
-
|
|
278
|
+
// A name that does not resolve is a transport failure; an address
|
|
279
|
+
// the policy refuses is a policy refusal.
|
|
280
|
+
throw pinResult.kind === 'unresolved'
|
|
281
|
+
? new Error(pinResult.reason)
|
|
282
|
+
: refuse(pinResult.reason);
|
|
258
283
|
}
|
|
259
|
-
const response = await issueRequest(parsed, method, body, pinResult.pinned, maxFetchBytes,
|
|
284
|
+
const response = await issueRequest(parsed, method, body, pinResult.pinned, maxFetchBytes, refuse, signal);
|
|
260
285
|
if (response.status >= 300 && response.status < 400) {
|
|
261
286
|
if (!response.location) {
|
|
262
|
-
throw
|
|
287
|
+
throw refuse('redirect response carried no Location header');
|
|
263
288
|
}
|
|
264
289
|
try {
|
|
265
290
|
currentUrl = new URL(response.location, currentUrl).toString();
|
|
266
291
|
}
|
|
267
292
|
catch {
|
|
268
|
-
throw
|
|
293
|
+
throw refuse('redirect response carried an unparseable Location header');
|
|
269
294
|
}
|
|
270
295
|
continue;
|
|
271
296
|
}
|
|
272
297
|
return { status: response.status, body: response.body };
|
|
273
298
|
}
|
|
274
|
-
throw
|
|
299
|
+
throw refuse(`exceeded ${MAX_REDIRECTS} redirects`);
|
|
275
300
|
}
|
|
276
301
|
return {
|
|
277
302
|
get: (url) => fetchAllowed(url, 'GET', undefined),
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Returns `error` marked as a policy refusal. A network provider throws one for every request it refuses on policy grounds. */
|
|
2
|
+
export declare function netRefusal(error: Error): Error;
|
|
3
|
+
/** True when `error` was marked by {@link netRefusal}. */
|
|
4
|
+
export declare function isNetRefusal(error: unknown): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks an `Error` as a network POLICY refusal (an unlisted host, a refused
|
|
3
|
+
* address range, an over-size body) as opposed to a transport failure (DNS,
|
|
4
|
+
* connection, timeout, abort).
|
|
5
|
+
*
|
|
6
|
+
* The brand is a global `Symbol.for` key on the error object, so it needs no
|
|
7
|
+
* runtime import and survives being created in one bundle and read in
|
|
8
|
+
* another. The net bridge reads it to decide whether a failure crosses to
|
|
9
|
+
* the worker as a capability denial or as an ordinary runtime failure. A
|
|
10
|
+
* refusal is always an error: a script can never read one as a success.
|
|
11
|
+
*
|
|
12
|
+
* It is a plain property, not a classification the Lua side trusts: the
|
|
13
|
+
* worker rebuilds its own branded denial from the `denied` flag on the wire.
|
|
14
|
+
*/
|
|
15
|
+
const NET_REFUSAL = Symbol.for('markii.net.refusal');
|
|
16
|
+
/** Returns `error` marked as a policy refusal. A network provider throws one for every request it refuses on policy grounds. */
|
|
17
|
+
export function netRefusal(error) {
|
|
18
|
+
return Object.assign(error, { [NET_REFUSAL]: true });
|
|
19
|
+
}
|
|
20
|
+
/** True when `error` was marked by {@link netRefusal}. */
|
|
21
|
+
export function isNetRefusal(error) {
|
|
22
|
+
return (error instanceof Error &&
|
|
23
|
+
error[NET_REFUSAL] === true);
|
|
24
|
+
}
|
|
@@ -1,30 +1,22 @@
|
|
|
1
|
+
import type { HostIsolate } from '../host/adapter.js';
|
|
1
2
|
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
3
|
/**
|
|
18
4
|
* The default isolate: a `node:worker_threads` worker, which is what every
|
|
19
|
-
* Node host
|
|
20
|
-
* is the only kind that accepts `resourceLimits`, so it is also the only
|
|
5
|
+
* Node host uses. It is the only kind that accepts `resourceLimits`, so it is also the only
|
|
21
6
|
* kind whose V8 heap is bounded — see `./isolate.ts` for why a host might
|
|
22
7
|
* still have to supply the other one.
|
|
23
8
|
*/
|
|
24
9
|
export declare const workerThreadIsolate: IsolateSpawner;
|
|
10
|
+
export interface NodeHostIsolateOptions {
|
|
11
|
+
/** The worker entry on disk. Defaults to `node-worker.js` in the package's `dist/`. */
|
|
12
|
+
workerPath?: string;
|
|
13
|
+
}
|
|
25
14
|
/**
|
|
26
|
-
* `
|
|
27
|
-
* the
|
|
28
|
-
*
|
|
15
|
+
* A `HostIsolate` of kind `node`: each run starts a `worker_threads` worker
|
|
16
|
+
* from the packaged, self-contained `node-worker.js`. A host that bundles
|
|
17
|
+
* the worker itself passes its own `workerPath`. The default is resolved
|
|
18
|
+
* when a run asks for it, so constructing the isolate never throws.
|
|
29
19
|
*/
|
|
30
|
-
export declare function
|
|
20
|
+
export declare function createNodeHostIsolate(options?: NodeHostIsolateOptions): Extract<HostIsolate, {
|
|
21
|
+
kind: 'node';
|
|
22
|
+
}>;
|