@markii/host 0.17.0-next.2 → 0.17.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +4 -2
  2. package/dist/browser-worker.js +36 -36
  3. package/dist/browser.d.ts +6 -2
  4. package/dist/browser.js +7 -2
  5. package/dist/host/adapter.d.ts +18 -23
  6. package/dist/host/conformance-runner.d.ts +4 -1
  7. package/dist/host/conformance-runner.js +4 -6
  8. package/dist/host/create-host.d.ts +4 -6
  9. package/dist/host/editor-behavior.d.ts +2 -6
  10. package/dist/host/editor-behavior.js +5 -9
  11. package/dist/host/export-behavior.d.ts +4 -6
  12. package/dist/host/labels.d.ts +1 -2
  13. package/dist/host/pack-archive.js +2 -8
  14. package/dist/host/pack-install.d.ts +1 -2
  15. package/dist/host/pack-install.js +4 -7
  16. package/dist/host/pack-load.d.ts +1 -2
  17. package/dist/host/pack-trust-behavior.js +1 -1
  18. package/dist/host/refresh-interval.d.ts +1 -3
  19. package/dist/host/refresh-interval.js +1 -3
  20. package/dist/host/run-behavior.d.ts +3 -4
  21. package/dist/host/run-behavior.js +23 -2
  22. package/dist/host/script-execution.d.ts +1 -1
  23. package/dist/index.js +1 -2
  24. package/dist/node-worker.js +20657 -0
  25. package/dist/node.d.ts +2 -1
  26. package/dist/node.js +1 -1
  27. package/dist/packs/discover.js +1 -1
  28. package/dist/packs/pack-diagnostics.d.ts +6 -8
  29. package/dist/packs/pack-diagnostics.js +6 -8
  30. package/dist/packs/pack-paths.js +1 -2
  31. package/dist/packs/pack-render-registry.d.ts +9 -10
  32. package/dist/run/browser-isolate.d.ts +5 -5
  33. package/dist/run/browser-isolate.js +5 -1
  34. package/dist/run/isolate.d.ts +3 -1
  35. package/dist/run/net-bridge.d.ts +5 -5
  36. package/dist/run/net-provider.d.ts +3 -1
  37. package/dist/run/net-provider.js +20 -6
  38. package/dist/run/node-isolate.d.ts +13 -21
  39. package/dist/run/node-isolate.js +31 -80
  40. package/dist/run/run-flow.d.ts +1 -2
  41. package/dist/run/run-host.d.ts +3 -3
  42. package/dist/run/worker-entry-browser.d.ts +24 -4
  43. package/dist/run/worker-entry-browser.js +99 -4
  44. package/dist/run/worker-entry.js +2 -0
  45. package/package.json +14 -11
package/dist/node.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { SpawnRunOptions } from './run/run-host.js';
2
- export { defaultWorkerPath, spawnRunInThread, workerThreadIsolate, } from './run/node-isolate.js';
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
5
  export { createNodeArchiveExtractFs, createNodeDirectoryLister, createNodeFileReader, createNodePackDiscoveryIo, createNodeReader as createNodePackScriptsReader, } from './node-fs.js';
5
6
  export { createHtmlExportEngine } from './html-engine.js';
package/dist/node.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // - the `@markii/html`-backed static export engine;
8
8
  //
9
9
  // A Node host imports from both entries. No browser bundle imports this one.
10
- export { defaultWorkerPath, spawnRunInThread, workerThreadIsolate, } from './run/node-isolate.js';
10
+ export { createNodeHostIsolate, workerThreadIsolate, } from './run/node-isolate.js';
11
11
  export { createNetProvider } from './run/net-provider.js';
12
12
  export { createNodeArchiveExtractFs, createNodeDirectoryLister, createNodeFileReader, createNodePackDiscoveryIo, createNodeReader as createNodePackScriptsReader, } from './node-fs.js';
13
13
  export { createHtmlExportEngine } from './html-engine.js';
@@ -131,7 +131,7 @@ async function tryLoadPackAt(folder, readFile) {
131
131
  */
132
132
  /**
133
133
  * When a `ManifestAttempt` succeeded (`kind: 'found'`) but the manifest's
134
- * `engine` does not match `supportedEngine` (batch 7 #46), downgrades it to
134
+ * `engine` does not match `supportedEngine`, downgrades it to
135
135
  * a `SkippedPackFolder` instead of a `DiscoveredPack` — same effect
136
136
  * `@markii/react`'s `loadPack` already has for an unsupported engine
137
137
  * (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`, batch 7 #46). Omitted (or
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 — survey finding A9:
66
- * `apps/vscode/src/packs/pack-diagnostics.ts` and
67
- * `apps/obsidian/src/packs/pack-diagnostics.ts` each defined this exact
68
- * sentence shape, differing only in the tail clause naming how a user
69
- * would rebuild the prebuilt artifact from those sources (VS Code's own
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 (batch 7 #46) — `[]` when there was none. */
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 — survey finding A9:
91
- * `apps/vscode/src/packs/pack-diagnostics.ts` and
92
- * `apps/obsidian/src/packs/pack-diagnostics.ts` each defined this exact
93
- * sentence shape, differing only in the tail clause naming how a user
94
- * would rebuild the prebuilt artifact from those sources (VS Code's own
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
@@ -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 (per orchestrator direction, carried over from the original VS
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, RegistryEntry>): {
11
- registry: 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, RegistryEntry>): Registry;
18
- mergeRegistries(...registries: Registry[]): Registry;
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: 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 (batch 7 #46). Before
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: Registry, engine: PackRegistryEngine): BuildRenderRegistryResult;
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`, which is
17
- * the same pinned one a worker-thread isolate runs. The isolate has no
18
- * network stack of its own, so the allowlist cannot be bypassed from
19
- * inside it.
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) => {
@@ -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 (see `./run-host.ts`'s `defaultWorkerPath`). */
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
  }
@@ -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
- * 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.
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,
@@ -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): NetProvider;
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
@@ -14,9 +14,9 @@
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
- * 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.
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';
@@ -80,7 +80,7 @@ function bareHostname(hostname) {
80
80
  */
81
81
  function issueRequest(url, method, body, pinned, maxFetchBytes,
82
82
  /** See `createNetProvider`'s own `denial` parameter — injected for the same reason. */
83
- denial) {
83
+ denial, signal) {
84
84
  return new Promise((resolve, reject) => {
85
85
  const mod = url.protocol === 'https:' ? https : http;
86
86
  const bodyBuffer = body !== undefined ? Buffer.from(body, 'utf8') : undefined;
@@ -163,6 +163,16 @@ denial) {
163
163
  req.on('error', (err) => {
164
164
  settleReject(denial(`request failed: ${describeThrown(err)}`));
165
165
  });
166
+ if (signal !== undefined) {
167
+ const onAbort = () => {
168
+ req.destroy();
169
+ settleReject(denial('the run has ended'));
170
+ };
171
+ if (signal.aborted)
172
+ onAbort();
173
+ else
174
+ signal.addEventListener('abort', onAbort, { once: true });
175
+ }
166
176
  if (bodyBuffer)
167
177
  req.write(bodyBuffer);
168
178
  req.end();
@@ -227,7 +237,9 @@ export function createNetProvider(allowlist, maxFetchBytes, policy,
227
237
  * host marks every refusal on the wire (`./net-bridge.ts`'s `denied`
228
238
  * flag) and the worker rebuilds a properly branded error on arrival.
229
239
  */
230
- denial, lookup = defaultHostLookup) {
240
+ denial, lookup = defaultHostLookup,
241
+ /** Aborted when the run ends: an in-flight request is destroyed and no further hop is made. */
242
+ signal) {
231
243
  const allowed = new Set(allowlist.map((host) => host.toLowerCase()));
232
244
  async function fetchAllowed(startUrl, method, body) {
233
245
  let currentUrl = startUrl;
@@ -246,6 +258,8 @@ denial, lookup = defaultHostLookup) {
246
258
  if (parsed.username !== '' || parsed.password !== '') {
247
259
  throw denial(`URL "${currentUrl}" embeds credentials, which is not allowed`);
248
260
  }
261
+ if (signal?.aborted)
262
+ throw denial('the run has ended');
249
263
  const host = parsed.hostname.toLowerCase();
250
264
  if (!allowed.has(host)) {
251
265
  throw denial(`host "${host}" is not on the run's allowlist`);
@@ -256,7 +270,7 @@ denial, lookup = defaultHostLookup) {
256
270
  if (!pinResult.ok) {
257
271
  throw denial(pinResult.reason);
258
272
  }
259
- const response = await issueRequest(parsed, method, body, pinResult.pinned, maxFetchBytes, denial);
273
+ const response = await issueRequest(parsed, method, body, pinResult.pinned, maxFetchBytes, denial, signal);
260
274
  if (response.status >= 300 && response.status < 400) {
261
275
  if (!response.location) {
262
276
  throw denial('redirect response carried no Location header');
@@ -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 (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
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
- * `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`.
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 spawnRunInThread(options: Omit<SpawnRunOptions, 'spawnIsolate' | 'workerPath'> & Partial<Pick<SpawnRunOptions, 'spawnIsolate' | 'workerPath'>>): Promise<RunResult>;
20
+ export declare function createNodeHostIsolate(options?: NodeHostIsolateOptions): Extract<HostIsolate, {
21
+ kind: 'node';
22
+ }>;
@@ -1,81 +1,23 @@
1
1
  /**
2
2
  * The Node half of the Run path's isolate seam: a `node:worker_threads`
3
- * worker, plus the dev/Vitest worker-entry fallback. Split out of
4
- * `./run-host.ts` so the watchdog, the exactly-once settlement and the
5
- * never-rejects contract stay in a module with no `node:*` import and run
6
- * unchanged for every host kind. Reached only through `@markii/host/node`.
3
+ * worker. Split out of `./run-host.ts` so the watchdog, the exactly-once
4
+ * settlement and the never-rejects contract stay in a module with no
5
+ * `node:*` import and run unchanged for every host kind. Reached only
6
+ * through `@markii/host/node`.
7
7
  */
8
8
  import { Worker } from 'node:worker_threads';
9
9
  import { existsSync } from 'node:fs';
10
- import * as path from 'node:path';
11
- import { spawnRun } from './run-host.js';
12
- /** `execArgv` needed to run a `.ts` worker entry directly (dev/Vitest only) — see `./worker-entry.ts`'s doc comment on why a plain `node` `.ts` import can't resolve this repo's `@markii/*` bare specifiers or its `./foo.js` -> `./foo.ts` import convention on its own. `--require tsx/cjs` (rather than `tsx/esm`) is deliberate: empirically, the ESM loader hook deadlocks Node's CJS/ESM interop translator for a worker thread requiring `.js`-suffixed relative imports that resolve to `.ts` files (this repo's convention throughout `@markii/*`), while the CJS `require` hook has no such issue. */
13
- const TSX_DEV_EXEC_ARGV = ['--require', 'tsx/cjs'];
14
- /**
15
- * The ONLY fallback this shared package supplies for an unset `workerPath`:
16
- * the sibling `worker-entry.ts`, run straight from source via `tsx`
17
- * (`TSX_DEV_EXEC_ARGV`, above). That is correct for this package's own
18
- * dev/Vitest runs — `__dirname` at that point is this real source
19
- * directory — but it says nothing about where a packaged host's bundled
20
- * worker lives, because this package does not know that host's `dist/`
21
- * layout. A packaged host (e.g. `apps/vscode`) MUST resolve its own bundled
22
- * worker path itself and pass it as `SpawnRunOptions.workerPath`; see
23
- * `apps/vscode/src/worker-path.ts` for that host's resolution and
24
- * `apps/vscode/esbuild.config.mjs`'s worker build for where it bundles to.
25
- */
26
- export function defaultWorkerPath() {
27
- // The built package is ESM, where `__dirname` does not exist: a packaged
28
- // host always passes its own bundled worker, so say so instead of
29
- // throwing a ReferenceError.
30
- const devSource = typeof __dirname === 'undefined'
31
- ? undefined
32
- : path.join(__dirname, 'worker-entry.ts');
33
- if (devSource !== undefined && existsSync(devSource))
34
- return devSource;
35
- throw new Error('spawnRun: could not resolve the worker entry file (looked for ' +
36
- `${devSource ?? 'worker-entry.ts beside the package source'}) — pass workerPath explicitly`);
37
- }
38
- function execArgvFor(workerPath) {
39
- return workerPath.endsWith('.ts') ? TSX_DEV_EXEC_ARGV : undefined;
40
- }
41
- /**
42
- * Dev/Vitest only: point `tsx` at the repo's `tsconfig.dev.json` so a
43
- * `.ts` worker entry resolves `@markii/*` to each package's SOURCE.
44
- *
45
- * Without this the worker is a plain Node resolution: a bare `@markii/lua`
46
- * goes through `node_modules` to that package's `exports`, which name the
47
- * built `dist/`. `npm run build` only type-checks (`tsc --noEmit`); the
48
- * emitting script is `build:dist`, which runs at release. So `dist/` is
49
- * whatever a past release left behind, and a real-isolate probe would
50
- * silently exercise that stale code instead of the source it is written to
51
- * test. A capability added since the last `build:dist` simply would not
52
- * exist inside the worker.
53
- *
54
- * Gated on a `.ts` entry, the same signal `execArgvFor` already uses for
55
- * the tsx hook, so a packaged host passing its own bundled `.js` worker is
56
- * never touched by this.
57
- */
58
- function devTsconfigEnv(entryPath) {
59
- if (!entryPath.endsWith('.ts') || typeof __dirname === 'undefined') {
60
- return undefined;
61
- }
62
- const tsconfig = path.resolve(__dirname, '../../../..', 'tsconfig.dev.json');
63
- if (!existsSync(tsconfig))
64
- return undefined;
65
- return { env: { ...process.env, TSX_TSCONFIG_PATH: tsconfig } };
66
- }
10
+ import { fileURLToPath } from 'node:url';
67
11
  /**
68
12
  * The default isolate: a `node:worker_threads` worker, which is what every
69
- * Node host (the VS Code extension host, this package's own tests) uses. It
70
- * is the only kind that accepts `resourceLimits`, so it is also the only
13
+ * Node host uses. It is the only kind that accepts `resourceLimits`, so it is also the only
71
14
  * kind whose V8 heap is bounded — see `./isolate.ts` for why a host might
72
15
  * still have to supply the other one.
73
16
  */
74
17
  export const workerThreadIsolate = (options) => {
75
- const execArgv = options.execArgv ?? execArgvFor(options.entryPath);
76
18
  const worker = new Worker(options.entryPath, {
77
- ...(execArgv ? { execArgv } : {}),
78
- ...(devTsconfigEnv(options.entryPath) ?? {}),
19
+ ...(options.execArgv ? { execArgv: options.execArgv } : {}),
20
+ ...(options.env ? { env: options.env } : {}),
79
21
  resourceLimits: {
80
22
  maxOldGenerationSizeMb: options.maxOldGenerationSizeMb,
81
23
  },
@@ -88,11 +30,11 @@ export const workerThreadIsolate = (options) => {
88
30
  void worker.terminate();
89
31
  },
90
32
  onMessage: (listener) => {
91
- // `on`, not `once`: a run now sends one progress message per script
92
- // (GitHub issue #35) ahead of its single result message, and
93
- // `spawnRun` is what tells the two apart. Messages arriving after
94
- // the run settles are harmless — `settled` guards every path — and
95
- // the worker is terminated on settle anyway.
33
+ // `on`, not `once`: a run sends one progress message per script
34
+ // ahead of its single result message, and `spawnRun` tells the two
35
+ // apart. Messages arriving after the run settles are harmless
36
+ // (`settled` guards every path) and the worker is terminated on
37
+ // settle anyway.
96
38
  worker.on('message', listener);
97
39
  },
98
40
  onError: (listener) => {
@@ -104,14 +46,23 @@ export const workerThreadIsolate = (options) => {
104
46
  };
105
47
  };
106
48
  /**
107
- * `spawnRun` with the Node defaults filled in: the worker-thread isolate and
108
- * the dev/Vitest worker entry. For this package's own tests and any Node
109
- * caller that wants them; a packaged host passes its own `workerPath`.
49
+ * A `HostIsolate` of kind `node`: each run starts a `worker_threads` worker
50
+ * from the packaged, self-contained `node-worker.js`. A host that bundles
51
+ * the worker itself passes its own `workerPath`. The default is resolved
52
+ * when a run asks for it, so constructing the isolate never throws.
110
53
  */
111
- export function spawnRunInThread(options) {
112
- return spawnRun({
113
- ...options,
114
- workerPath: options.workerPath ?? defaultWorkerPath(),
115
- spawnIsolate: options.spawnIsolate ?? workerThreadIsolate,
116
- });
54
+ export function createNodeHostIsolate(options = {}) {
55
+ return {
56
+ kind: 'node',
57
+ workerPath: () => {
58
+ if (options.workerPath !== undefined)
59
+ return options.workerPath;
60
+ const packaged = fileURLToPath(new URL('../node-worker.js', import.meta.url));
61
+ if (!existsSync(packaged)) {
62
+ throw new Error(`the packaged worker was not found at ${packaged}: pass workerPath`);
63
+ }
64
+ return packaged;
65
+ },
66
+ spawnIsolate: workerThreadIsolate,
67
+ };
117
68
  }
@@ -163,8 +163,7 @@ export interface RunOnceResult {
163
163
  /**
164
164
  * Consent unification (SECURITY-RELEVANT): lines describing any mismatch
165
165
  * between what is DECLARED (a bundle manifest's `permissions.net`, and/or
166
- * an individual script's own `permissions` fence attribute — batch 7
167
- * #47) and the hosts this run's scripts actually reach, from
166
+ * an individual script's own `permissions` fence attribute) and the hosts this run's scripts actually reach, from
168
167
  * `./bundle-run.ts`'s `netDeclarationDiagnostics`. `[]` when there is
169
168
  * nothing to compare against (no bundle manifest and no script declares
170
169
  * `permissions`) or every declared host agrees with the scan. For the
@@ -85,9 +85,9 @@ export interface SpawnRunOptions {
85
85
  /**
86
86
  * The entry the isolate loads. This package cannot know how a given host
87
87
  * bundles or lays out its own `dist/` (see e.g.
88
- * `apps/vscode/src/worker-path.ts`), so the host always passes it. The
89
- * Node-only dev/Vitest fallback lives in `@markii/host/node`
90
- * (`defaultWorkerPath`).
88
+ * `apps/vscode/src/worker-path.ts`), so the host always passes it.
89
+ * `createNodeHostIsolate` from `@markii/host/node` supplies the packaged
90
+ * worker's path.
91
91
  */
92
92
  workerPath: string;
93
93
  }
@@ -10,10 +10,11 @@
10
10
  * the failure classification cannot drift between hosts. This file supplies
11
11
  * only the two things a Web Worker has to do differently:
12
12
  *
13
- * 1. Network goes through `./net-bridge.ts` to the host, which still has
14
- * Node and runs the same pinned provider as before. The isolate itself
15
- * has no network stack, which makes the allowlist unbypassable from
16
- * inside it.
13
+ * 1. Network goes through `./net-bridge.ts` to the host, which runs the
14
+ * same pinned provider as before. A Web Worker does have network
15
+ * globals, so `lockdownWorkerGlobals` (below) removes them before any
16
+ * script runs. Lua cannot reach JavaScript at all; the removal is a
17
+ * second wall for the case where an interpreter escape does.
17
18
  * 2. wasmoon's `glue.wasm` arrives as a URL on the job, because there is
18
19
  * no `__dirname` here to find it beside the bundle.
19
20
  *
@@ -29,6 +30,25 @@ interface WorkerGlobal {
29
30
  data: unknown;
30
31
  }) => void) => void;
31
32
  }
33
+ /**
34
+ * Removes the worker's own ways to reach a network, once, before the first
35
+ * script runs.
36
+ *
37
+ * - Every name in `REMOVED_GLOBALS` (the connection and storage
38
+ * constructors, and `Worker`, which would start an unlocked scope) is
39
+ * shadowed with `undefined`.
40
+ * - `importScripts` becomes a function that throws. It stays a function
41
+ * because the Lua engine's start-up detects a worker by
42
+ * `typeof importScripts`.
43
+ * - `fetch` keeps working for exactly one URL, the `glue.wasm` the job
44
+ * named, because the engine loads its WebAssembly through it for every
45
+ * script. Any other URL is refused.
46
+ *
47
+ * Net traffic is unaffected: it goes to the host with `postMessage`.
48
+ * Idempotent. Covered by `worker-entry-browser.test.ts` and, in a real
49
+ * browser, by the e2e suite.
50
+ */
51
+ export declare function lockdownWorkerGlobals(scope: object, wasmUri: string | undefined): void;
32
52
  /**
33
53
  * A job for this entry additionally carries where `glue.wasm` can be
34
54
  * fetched from — a blob URL the host minted from the file on disk. Absent,