@markii/host 0.17.0-next.5 → 0.17.0-next.6

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 (39) hide show
  1. package/dist/browser-worker.classic.js +43 -43
  2. package/dist/browser-worker.js +43 -43
  3. package/dist/conformance/host/18-last-run-and-grants/expected.json +42 -0
  4. package/dist/conformance/host/18-last-run-and-grants/note.mk.md +5 -0
  5. package/dist/conformance/host/18-last-run-and-grants/scenario.json +27 -0
  6. package/dist/conformance/host/README.md +11 -4
  7. package/dist/conformance.d.ts +1 -1
  8. package/dist/conformance.js +1 -1
  9. package/dist/host/adapter.d.ts +21 -7
  10. package/dist/host/cascade-export-behavior.d.ts +3 -1
  11. package/dist/host/cascade-export-behavior.js +7 -4
  12. package/dist/host/conformance-runner.d.ts +8 -0
  13. package/dist/host/conformance-runner.js +40 -2
  14. package/dist/host/create-host.d.ts +231 -62
  15. package/dist/host/create-host.js +204 -98
  16. package/dist/host/export-behavior.d.ts +2 -0
  17. package/dist/host/export-behavior.js +10 -4
  18. package/dist/host/pack-archive.d.ts +7 -6
  19. package/dist/host/pack-archive.js +11 -7
  20. package/dist/host/pack-install.js +9 -7
  21. package/dist/host/pack-remove.d.ts +7 -18
  22. package/dist/host/pack-remove.js +2 -0
  23. package/dist/host/pack-trust-behavior.d.ts +13 -36
  24. package/dist/host/pack-trust-behavior.js +22 -1
  25. package/dist/host/run-behavior.d.ts +21 -34
  26. package/dist/host/run-behavior.js +23 -5
  27. package/dist/index.d.ts +5 -10
  28. package/dist/index.js +0 -1
  29. package/dist/internal.d.ts +9 -2
  30. package/dist/internal.js +3 -2
  31. package/dist/node-worker.js +3 -1
  32. package/dist/node.d.ts +1 -2
  33. package/dist/node.js +1 -1
  34. package/dist/run/node-isolate.d.ts +6 -2
  35. package/dist/run/node-isolate.js +2 -0
  36. package/dist/run/run-host.d.ts +2 -2
  37. package/dist/run/worker-entry-browser.d.ts +6 -1
  38. package/dist/run/worker-entry-browser.js +62 -5
  39. package/package.json +11 -11
@@ -1,5 +1,6 @@
1
1
  import type { PackTrustList } from '../packs/pack-trust.js';
2
2
  import type { HostAdapter } from './adapter.js';
3
+ import type { DisablePackOutcome, EnablePackOutcome } from './create-host.js';
3
4
  /** One installed pack this device has enabled. */
4
5
  export interface EnabledPack {
5
6
  readonly namespace: string;
@@ -33,47 +34,23 @@ export declare function trustedPackFolders(adapter: HostAdapter): Promise<readon
33
34
  * Never throws; a refused write is reported on the diagnostics surface.
34
35
  */
35
36
  export declare function trustInstalledPack(adapter: HostAdapter, namespace: string, installedDir: string): Promise<void>;
36
- export type EnablePackOutcome = {
37
- readonly kind: 'enabled';
38
- readonly namespace: string;
39
- } | {
40
- readonly kind: 'declined';
41
- readonly namespace: string;
42
- } | {
43
- readonly kind: 'not-installed';
44
- readonly namespace: string;
45
- } | {
46
- readonly kind: 'reserved';
47
- readonly namespace: string;
48
- } | {
49
- readonly kind: 'invalid';
50
- readonly reason: string;
51
- } | {
52
- readonly kind: 'failed';
53
- readonly reason: string;
54
- };
55
- export type DisablePackOutcome = {
56
- readonly kind: 'disabled';
57
- readonly namespace: string;
58
- } | {
59
- readonly kind: 'not-enabled';
60
- readonly namespace: string;
61
- } | {
62
- readonly kind: 'invalid';
63
- readonly reason: string;
64
- } | {
65
- readonly kind: 'failed';
66
- readonly reason: string;
67
- };
37
+ /** What one enable attempt produced, before the host adds `unsupported`. */
38
+ export type EnablePackAttempt = Exclude<EnablePackOutcome, {
39
+ readonly kind: 'unsupported';
40
+ }>;
41
+ /** What one disable attempt produced, before the host adds `unsupported`. */
42
+ export type DisablePackAttempt = Exclude<DisablePackOutcome, {
43
+ readonly kind: 'unsupported';
44
+ }>;
68
45
  /** Never throws. The caller checks `usesPackTrust` first. */
69
- export declare function enablePackViaAdapter(adapter: HostAdapter, namespace: string): Promise<EnablePackOutcome>;
46
+ export declare function enablePackViaAdapter(adapter: HostAdapter, namespace: string): Promise<EnablePackAttempt>;
70
47
  /** Never throws. The caller checks `usesPackTrust` first. The folder stays on disk. */
71
- export declare function disablePackViaAdapter(adapter: HostAdapter, namespace: string): Promise<DisablePackOutcome>;
48
+ export declare function disablePackViaAdapter(adapter: HostAdapter, namespace: string): Promise<DisablePackAttempt>;
72
49
  /** Withdraws a namespace's trust entry after a removal. A no-op for a host without trust. Never throws. */
73
50
  export declare function forgetPackTrust(adapter: HostAdapter, namespace: string): Promise<void>;
74
51
  export declare function enablePackMessage(outcome: EnablePackOutcome): string;
75
- export declare function enablePackDiagnosticLine(outcome: EnablePackOutcome): string;
52
+ export declare function enablePackDiagnosticLine(outcome: EnablePackAttempt): string;
76
53
  export declare function disablePackMessage(outcome: DisablePackOutcome): string;
77
- export declare function disablePackDiagnosticLine(outcome: DisablePackOutcome): string;
54
+ export declare function disablePackDiagnosticLine(outcome: DisablePackAttempt): string;
78
55
  /** One line per folder that is present in the install root but not enabled on this device. Informational, not a failure. */
79
56
  export declare function notEnabledPackDiagnosticLine(namespace: string, deviceNoun: string): string;
@@ -68,8 +68,25 @@ export async function listInstalledPacks(adapter) {
68
68
  .map((entry) => entry.name)
69
69
  .sort();
70
70
  }
71
- catch {
71
+ catch (error) {
72
72
  names = [];
73
+ // A missing install root is the ordinary state before the first
74
+ // install. Any other failure is reported, so it is not mute.
75
+ let rootExists = false;
76
+ try {
77
+ rootExists = await adapter.exists(installRoot);
78
+ }
79
+ catch {
80
+ rootExists = true;
81
+ }
82
+ if (rootExists) {
83
+ try {
84
+ adapter.diagnostics(`could not list installed packs: ${error instanceof Error ? error.message : String(error)}`);
85
+ }
86
+ catch {
87
+ // the diagnostics surface itself is what failed
88
+ }
89
+ }
73
90
  }
74
91
  return selectEnabledPacks(installRoot, names, readPackTrust(adapter));
75
92
  }
@@ -202,6 +219,8 @@ export async function forgetPackTrust(adapter, namespace) {
202
219
  // --- wording ---------------------------------------------------------------
203
220
  export function enablePackMessage(outcome) {
204
221
  switch (outcome.kind) {
222
+ case 'unsupported':
223
+ return 'Markii: enabling packs is not available on this host. Nothing was enabled.';
205
224
  case 'enabled':
206
225
  return `Markii: enabled the pack ${outcome.namespace}.`;
207
226
  case 'declined':
@@ -234,6 +253,8 @@ export function enablePackDiagnosticLine(outcome) {
234
253
  }
235
254
  export function disablePackMessage(outcome) {
236
255
  switch (outcome.kind) {
256
+ case 'unsupported':
257
+ return 'Markii: disabling packs is not available on this host. Nothing changed.';
237
258
  case 'disabled':
238
259
  return `Markii: disabled the pack ${outcome.namespace}. Its folder was left in place.`;
239
260
  case 'not-enabled':
@@ -2,34 +2,25 @@
2
2
  * The `run` behavior's host-shaped seams, built from a `HostAdapter`:
3
3
  * the grant prompts and the isolate spawn live here once for every host.
4
4
  *
5
- * A1: `apps/vscode/src/preview-panel.ts`'s `promptHostAdapter` /
6
- * `promptUnknownHostsAdapter` / `promptManyHostsAdapter`,
7
- * `apps/obsidian/src/run-modals.ts`'s `promptHostModal` /
8
- * `promptUnknownHostsModal` / `promptManyHostsModal`, and
9
- * `apps/cli/src/prompts.ts`'s three closures all did the same thing: call
10
- * one of `@markii/host`'s exported message builders and turn the answer
11
- * into a `boolean` through a host-specific UI primitive. `promptsFromAdapter`
12
- * below collapses all nine into three calls to the ONE `adapter.prompt`
13
- * method every `HostAdapter` already has.
5
+ * `promptsFromAdapter` routes the three grant prompts through the one
6
+ * `adapter.prompt` method every `HostAdapter` has, so a host writes no
7
+ * per-prompt code. Each prompt carries the wording the grant flow's message
8
+ * builders produce.
14
9
  *
15
- * A6: `apps/vscode/src/preview-panel.ts:spawnRun` and
16
- * `apps/cli/src/run-note.ts:spawnRun` were the same five-line function —
17
- * the CLI's own doc comment said outright it was "mirroring
18
- * apps/vscode/src/preview-panel.ts's identical function". `spawnRunViaAdapter`
19
- * below is that function, written once, dispatching on `HostAdapter.isolate`'s
20
- * discriminated union instead of assuming a Node worker path.
10
+ * `spawnRunViaAdapter` starts the run in the isolate the adapter names. It
11
+ * dispatches on `HostIsolate`'s discriminated union, so a Node worker path
12
+ * and a browser spawner go through the same call.
21
13
  *
22
- * `runViaAdapter` composes both of these around `../run/run-flow.ts`'s
23
- * `runOnce` (the existing, already-shared grant-flow-plus-spawn
24
- * orchestration) and writes the `RunTrace` through `adapter.now()` rather
25
- * than `Date.now()`, so a fixed clock in a test fake makes the trace
26
- * byte-stable.
14
+ * `runViaAdapter` composes both around `../run/run-flow.ts`'s `runOnce`,
15
+ * the shared grant-flow-plus-spawn orchestration. It writes the `RunTrace`
16
+ * through `adapter.now()` rather than `Date.now()`, so a fixed clock in a
17
+ * test fake makes the trace byte-stable.
27
18
  */
28
- import type { RunTrigger } from '@markii/runtime';
29
19
  import { type PromptHost, type PromptManyHosts, type PromptUnknownHosts } from '../run/grant-flow.js';
30
20
  import { type RunResult, type SpawnRunRequest } from '../run/run-host.js';
31
- import { type RunOnceOptions, type RunOnceResult } from '../run/run-flow.js';
21
+ import { type RunOnceResult } from '../run/run-flow.js';
32
22
  import { type RunTrace } from '../run/run-trace.js';
23
+ import type { RunRequest } from './create-host.js';
33
24
  import type { HostAdapter, HostIsolate } from './adapter.js';
34
25
  /**
35
26
  * External wall-clock budget for one run, matching the figure both
@@ -60,17 +51,6 @@ export declare function promptsFromAdapter(adapter: HostAdapter): {
60
51
  * disposed once this run's spawner has been used.
61
52
  */
62
53
  export declare function spawnRunViaAdapter(isolate: HostIsolate | undefined, options: SpawnRunRequest): Promise<RunResult>;
63
- export interface RunViaAdapterRequest {
64
- /** Stable identity for the note this run belongs to. */
65
- readonly documentKey: string;
66
- readonly text: string;
67
- readonly trigger: RunTrigger;
68
- readonly netPolicy?: RunOnceOptions['netPolicy'];
69
- readonly bundle?: RunOnceOptions['bundle'];
70
- readonly packModules?: RunOnceOptions['packModules'];
71
- readonly onValue?: RunOnceOptions['onValue'];
72
- readonly timeoutMs?: number;
73
- }
74
54
  /** A run's result plus the `RunTrace` it just recorded, so a host can show its in-note marker from the same value the shared path persisted. */
75
55
  export type RunViaAdapterResult = RunOnceResult & {
76
56
  readonly trace: RunTrace;
@@ -79,6 +59,13 @@ export type RunViaAdapterResult = RunOnceResult & {
79
59
  /** How many of this run's values came back with the value store's `error` status. */
80
60
  readonly errorCount: number;
81
61
  };
62
+ /** What one run produced: the result, or the reason it could not complete. Never thrown. */
63
+ export type RunAttemptOutcome = ({
64
+ readonly kind: 'ran';
65
+ } & RunViaAdapterResult) | {
66
+ readonly kind: 'failed';
67
+ readonly reason: string;
68
+ };
82
69
  /**
83
70
  * Runs one manual, auto, or scheduled pass of `request.text`'s scripts,
84
71
  * exactly as `runOnce` already does (the grant flow prompts only on a
@@ -89,4 +76,4 @@ export type RunViaAdapterResult = RunOnceResult & {
89
76
  * `Date.now()` directly, so a fixed clock in a test fake makes the trace
90
77
  * byte-stable.
91
78
  */
92
- export declare function runViaAdapter(adapter: HostAdapter, request: RunViaAdapterRequest, overrideSpawnRun?: (options: SpawnRunRequest) => Promise<RunResult>): Promise<RunViaAdapterResult>;
79
+ export declare function runViaAdapter(adapter: HostAdapter, request: RunRequest, overrideSpawnRun?: (options: SpawnRunRequest) => Promise<RunResult>): Promise<RunAttemptOutcome>;
@@ -1,6 +1,24 @@
1
+ /**
2
+ * The `run` behavior's host-shaped seams, built from a `HostAdapter`:
3
+ * the grant prompts and the isolate spawn live here once for every host.
4
+ *
5
+ * `promptsFromAdapter` routes the three grant prompts through the one
6
+ * `adapter.prompt` method every `HostAdapter` has, so a host writes no
7
+ * per-prompt code. Each prompt carries the wording the grant flow's message
8
+ * builders produce.
9
+ *
10
+ * `spawnRunViaAdapter` starts the run in the isolate the adapter names. It
11
+ * dispatches on `HostIsolate`'s discriminated union, so a Node worker path
12
+ * and a browser spawner go through the same call.
13
+ *
14
+ * `runViaAdapter` composes both around `../run/run-flow.ts`'s `runOnce`,
15
+ * the shared grant-flow-plus-spawn orchestration. It writes the `RunTrace`
16
+ * through `adapter.now()` rather than `Date.now()`, so a fixed clock in a
17
+ * test fake makes the trace byte-stable.
18
+ */
1
19
  import { ALLOW_LABEL, DONT_ALLOW_LABEL, UNKNOWN_HOSTS_PROMPT_MESSAGE, hostPromptMessage, manyHostsPromptMessage, } from '../run/grant-flow.js';
2
20
  import { spawnRun as spawnRunHost, } from '../run/run-host.js';
3
- import { runOnce, } from '../run/run-flow.js';
21
+ import { runOnce } from '../run/run-flow.js';
4
22
  import { writeLastRunTrace } from '../run/run-trace.js';
5
23
  import { runCrashDiagnosticLine, runDiagnosticLines, } from './run-diagnostics.js';
6
24
  import { BROWSER_ISOLATE_ENTRY } from './adapter.js';
@@ -153,8 +171,8 @@ export async function runViaAdapter(adapter, request, overrideSpawnRun) {
153
171
  }
154
172
  catch (err) {
155
173
  // A run that threw is still a failure with two homes: the diagnostics
156
- // line and a failed trace for the in-note marker. The caller keeps its
157
- // own visible surface (a notice), so the error is rethrown.
174
+ // line and a failed trace for the in-note marker. The caller gets the
175
+ // reason as a `failed` outcome and keeps its own visible surface.
158
176
  const detail = err instanceof Error ? err.message : String(err);
159
177
  adapter.diagnostics(runCrashDiagnosticLine(request.trigger, detail));
160
178
  await writeLastRunTrace(adapter.memento, request.documentKey, {
@@ -163,7 +181,7 @@ export async function runViaAdapter(adapter, request, overrideSpawnRun) {
163
181
  ok: false,
164
182
  reason: detail,
165
183
  }).then(undefined, () => { });
166
- throw err;
184
+ return { kind: 'failed', reason: detail };
167
185
  }
168
186
  // The failure's second home (AGENTS.md, "clean is not silent"): one line
169
187
  // per failed script and per declaration mismatch, written here once so no
@@ -193,5 +211,5 @@ export async function runViaAdapter(adapter, request, overrideSpawnRun) {
193
211
  else if (value.status === 'error')
194
212
  errorCount += 1;
195
213
  }
196
- return { ...result, trace, freshCount, errorCount };
214
+ return { kind: 'ran', ...result, trace, freshCount, errorCount };
197
215
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  export type { ValuesFailure } from './values-failure.js';
2
- export type { RunJob, RunResult, RunFailure, SpawnRunOptions, SpawnRunRequest, } from './run/run-host.js';
3
2
  export type { IsolateSpawner, RunIsolate, SpawnIsolateOptions, } from './run/isolate.js';
4
- export type { RunOnceOptions, RunOnceResult } from './run/run-flow.js';
5
- export type { GrantMemento, PromptHost, PromptManyHosts, PromptUnknownHosts, Thenable, } from './run/grant-flow.js';
3
+ export type { GrantMemento, Thenable } from './run/grant-flow.js';
6
4
  export type { BuildBundleSnapshotOptions, BundleSnapshotResult, } from './run/bundle-run.js';
7
5
  export { buildBundleSnapshot } from './run/bundle-run.js';
8
6
  export type { RunTrace } from './run/run-trace.js';
@@ -10,24 +8,21 @@ export type { HtmlExportEngine, ExportBodyRenderer, ExportBodyResult, ExportPack
10
8
  export type { EmbeddedImageReport, ExportImageReader, ExportImageResult, ImageSkipReason, SkippedImage, } from './export/image-embed.js';
11
9
  export type { PinPolicy } from './run/net-pinning.js';
12
10
  export type { PackModulesMap } from './run/lua-resolver.js';
13
- export type { DiscoveredPack, DiscoverPacksResult, EngineUnsupportedPack, PackDirectoryLister, SkippedPackFolder, } from './packs/discover.js';
14
- export { discoverPacks } from './packs/discover.js';
11
+ export type { DiscoveredPack, EngineUnsupportedPack, SkippedPackFolder, } from './packs/discover.js';
15
12
  export type { BuildRenderRegistryResult, DuplicateComposedName, PackRegistryEngine, QueuedPackRegistration, } from './packs/pack-render-registry.js';
16
13
  export { buildRenderRegistry } from './packs/pack-render-registry.js';
17
14
  export { buildComponentCatalog } from './insert/component-catalog.js';
18
15
  export type { HostAdapter, HostDirEntry, HostEditor, HostExportCapabilities, HostExportFormat, HostIsolate, HostLabels, HostPackSource, HostPromptRequest, HostTextEdit, } from './host/adapter.js';
19
- export type { RunViaAdapterRequest, RunViaAdapterResult, } from './host/run-behavior.js';
20
16
  export { installPackMessage } from './host/pack-install.js';
21
- export type { PackDiscoveryIo } from './host/pack-load.js';
22
17
  export type { TerminalPackScript } from './host/terminal-packs.js';
23
- export type { RemovePackOutcome } from './host/pack-remove.js';
24
- export type { DisablePackOutcome, EnabledPack, EnablePackOutcome, InstalledPacksListing, } from './host/pack-trust-behavior.js';
18
+ export type { EnabledPack, InstalledPacksListing, } from './host/pack-trust-behavior.js';
25
19
  export { disablePackMessage, enablePackMessage, } from './host/pack-trust-behavior.js';
26
20
  export { removePackMessage } from './host/pack-remove.js';
27
21
  export { exportResultMessage } from './host/export-behavior.js';
28
22
  export { cascadeResultMessage } from './host/cascade-export-behavior.js';
29
23
  export type { CascadeLinkResolver, CascadeTruncation, } from './export/cascade.js';
24
+ export type { NoteLink } from './export/note-links.js';
30
25
  export type { CascadeExportedNote, UnreadableCascadeNote, } from './host/cascade-export-behavior.js';
31
26
  export { createMarkiiHost } from './host/create-host.js';
32
- export type { CompleteOutcome, EditorPositionRequest, ExportLinkedSetOutcome, ExportLinkedSetRequest, ExportOutcome, ExportRequest, HoverOutcome, InsertComponentRequest, InstallPackOutcome, InstallPackRequest, LoadPacksOutcome, LoadTerminalPacksOutcome, MarkiiHost, MarkiiHostOptions, OpenOutcome, OpenRequest, RemovePackResult, DisablePackResult, EnablePackResult, InstalledPacksOutcome, RunOutcome, RunRequest, Unsupported, } from './host/create-host.js';
27
+ export type { CompleteOutcome, DisablePackOutcome, EditorPositionRequest, EnablePackOutcome, ExportLinkedSetOutcome, ExportLinkedSetRequest, ExportOutcome, ExportRenderOptions, ExportRequest, HoverOutcome, InsertComponentRequest, InsertOutcome, InstallPackOutcome, InstallPackRequest, InstalledPacksOutcome, LastRunOutcome, LoadPacksOutcome, LoadTerminalPacksOutcome, MarkiiHost, MarkiiHostOptions, OpenOutcome, OpenRequest, RemovePackOutcome, RunOutcome, RunRequest, Unsupported, } from './host/create-host.js';
33
28
  export { netRefusal } from './run/net-refusal.js';
package/dist/index.js CHANGED
@@ -14,7 +14,6 @@
14
14
  // address pinning, the terminatable-isolate watchdog): one copy lives here
15
15
  // so no app grows a drifting second one.
16
16
  export { buildBundleSnapshot } from './run/bundle-run.js';
17
- export { discoverPacks } from './packs/discover.js';
18
17
  export { buildRenderRegistry } from './packs/pack-render-registry.js';
19
18
  export { buildComponentCatalog } from './insert/component-catalog.js';
20
19
  export { installPackMessage } from './host/pack-install.js';
@@ -1,12 +1,19 @@
1
+ export { createMarkiiHostWithSeams } from './host/create-host.js';
2
+ export type { MarkiiHostSeams } from './host/create-host.js';
3
+ export type { RunJob, RunResult, RunFailure, SpawnRunOptions, SpawnRunRequest, } from './run/run-host.js';
4
+ export type { RunOnceOptions, RunOnceResult } from './run/run-flow.js';
5
+ export type { PromptHost, PromptManyHosts, PromptUnknownHosts, } from './run/grant-flow.js';
6
+ export type { DiscoverPacksResult, PackDirectoryLister, } from './packs/discover.js';
7
+ export { discoverPacks } from './packs/discover.js';
8
+ export type { PackDiscoveryIo } from './host/pack-load.js';
1
9
  export { mergeArrivingValue } from './values-merge.js';
2
10
  export { isRunProgress } from './run/run-progress.js';
3
11
  export { isNetBridgeRequest, serveNetRequest } from './run/net-bridge.js';
4
12
  export type { NetBridgeReply } from './run/net-bridge.js';
5
13
  export { MAX_VALUES_SNAPSHOT_BYTES, readPersistedValues, runOnce, valuesStorageKeyFor, } from './run/run-flow.js';
6
- export { ALLOW_LABEL, DONT_ALLOW_LABEL, clearGrantForDocument, } from './run/grant-flow.js';
14
+ export { ALLOW_LABEL, DONT_ALLOW_LABEL } from './run/grant-flow.js';
7
15
  export { decodeBundleCacheFromStorage, encodeBundleCacheForStorage, withPersistedCache, } from './run/bundle-run.js';
8
16
  export { isBundleName, resolveBundleDocumentPath, } from './run/bundle-document.js';
9
- export { readLastRunTrace } from './run/run-trace.js';
10
17
  export { staleValuesForRehydration } from './run/stale-values.js';
11
18
  export { MARK_EXTENSION, buildNoteHtmlExport, composeNoteHtmlExport, noteHasScripts, exportBaseName, exportedSiblingPath, } from './export/note-export.js';
12
19
  export { EMPTY_IMAGE_REPORT, MAX_EMBEDDED_IMAGE_BYTES, embedImagesInHtml, } from './export/image-embed.js';
package/dist/internal.js CHANGED
@@ -3,14 +3,15 @@
3
3
  // here can change or disappear in any release. An outside host should use
4
4
  // the main entry. Environment-neutral like the main entry
5
5
  // (`src/neutral-entries.probe.test.ts` bundles it for a browser).
6
+ export { createMarkiiHostWithSeams } from './host/create-host.js';
7
+ export { discoverPacks } from './packs/discover.js';
6
8
  export { mergeArrivingValue } from './values-merge.js';
7
9
  export { isRunProgress } from './run/run-progress.js';
8
10
  export { isNetBridgeRequest, serveNetRequest } from './run/net-bridge.js';
9
11
  export { MAX_VALUES_SNAPSHOT_BYTES, readPersistedValues, runOnce, valuesStorageKeyFor, } from './run/run-flow.js';
10
- export { ALLOW_LABEL, DONT_ALLOW_LABEL, clearGrantForDocument, } from './run/grant-flow.js';
12
+ export { ALLOW_LABEL, DONT_ALLOW_LABEL } from './run/grant-flow.js';
11
13
  export { decodeBundleCacheFromStorage, encodeBundleCacheForStorage, withPersistedCache, } from './run/bundle-run.js';
12
14
  export { isBundleName, resolveBundleDocumentPath, } from './run/bundle-document.js';
13
- export { readLastRunTrace } from './run/run-trace.js';
14
15
  export { staleValuesForRehydration } from './run/stale-values.js';
15
16
  export { MARK_EXTENSION, buildNoteHtmlExport, composeNoteHtmlExport, noteHasScripts, exportBaseName, exportedSiblingPath, } from './export/note-export.js';
16
17
  export { EMPTY_IMAGE_REPORT, MAX_EMBEDDED_IMAGE_BYTES, embedImagesInHtml, } from './export/image-embed.js';
@@ -5466,6 +5466,9 @@ bundle.write = function(path, data) return __smd_bundle_write_blocked(path, data
5466
5466
  };
5467
5467
  }
5468
5468
 
5469
+ // ../markii-lua/src/sandbox.ts
5470
+ var import_wasmoon4 = __toESM(require_dist(), 1);
5471
+
5469
5472
  // ../markii-lua/src/doc.ts
5470
5473
  function buildDoc(config) {
5471
5474
  const view = config.doc;
@@ -5790,7 +5793,6 @@ end
5790
5793
  }
5791
5794
 
5792
5795
  // ../markii-lua/src/sandbox.ts
5793
- var import_wasmoon4 = __toESM(require_dist(), 1);
5794
5796
  var WALL_CLOCK_GUARD_SLACK_MS = 250;
5795
5797
  function describeError(err) {
5796
5798
  if (err instanceof Error) return err.message;
package/dist/node.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- export type { SpawnRunOptions } from './run/run-host.js';
2
1
  export type { NodeHostIsolateOptions } from './run/node-isolate.js';
3
- export { createNodeHostIsolate, workerThreadIsolate, } from './run/node-isolate.js';
2
+ export { createNodeHostIsolate } from './run/node-isolate.js';
4
3
  export { createNetProvider } from './run/net-provider.js';
5
4
  export { createNodeDirectoryLister, createNodeFileReader, createNodePackDiscoveryIo, createNodeReader as createNodePackScriptsReader, } from './node-fs.js';
package/dist/node.js CHANGED
@@ -9,6 +9,6 @@
9
9
  // `@markii/host/html-engine`, so importing this one needs no optional peer.
10
10
  //
11
11
  // A Node host imports from both entries. No browser bundle imports this one.
12
- export { createNodeHostIsolate, workerThreadIsolate, } from './run/node-isolate.js';
12
+ export { createNodeHostIsolate } from './run/node-isolate.js';
13
13
  export { createNetProvider } from './run/net-provider.js';
14
14
  export { createNodeDirectoryLister, createNodeFileReader, createNodePackDiscoveryIo, createNodeReader as createNodePackScriptsReader, } from './node-fs.js';
@@ -8,8 +8,12 @@ import type { IsolateSpawner } from './isolate.js';
8
8
  */
9
9
  export declare const workerThreadIsolate: IsolateSpawner;
10
10
  export interface NodeHostIsolateOptions {
11
- /** The worker entry on disk. Defaults to `node-worker.js` in the package's `dist/`. */
12
- workerPath?: string;
11
+ /**
12
+ * The worker entry on disk, or a function that finds it when a run asks.
13
+ * A function may throw, and that run then reports the worker as missing.
14
+ * Defaults to `node-worker.js` in the package's `dist/`.
15
+ */
16
+ workerPath?: string | (() => string);
13
17
  }
14
18
  /**
15
19
  * A `HostIsolate` of kind `node`: each run starts a `worker_threads` worker
@@ -55,6 +55,8 @@ export function createNodeHostIsolate(options = {}) {
55
55
  return {
56
56
  kind: 'node',
57
57
  workerPath: () => {
58
+ if (typeof options.workerPath === 'function')
59
+ return options.workerPath();
58
60
  if (options.workerPath !== undefined)
59
61
  return options.workerPath;
60
62
  const packaged = fileURLToPath(new URL('../node-worker.js', import.meta.url));
@@ -57,8 +57,8 @@ export interface SpawnRunOptions {
57
57
  /**
58
58
  * The KIND of isolate this run spawns. Required: this module has no
59
59
  * default so it stays free of `node:*`. A Node host passes
60
- * `workerThreadIsolate` from `@markii/host/node` (the only kind that
61
- * accepts a V8 heap cap); a host whose runtime cannot create worker
60
+ * the worker-thread isolate that `createNodeHostIsolate()` from
61
+ * `@markii/host/node` carries (the only kind that accepts a V8 heap cap); a host whose runtime cannot create worker
62
62
  * threads, such as an Electron renderer, supplies a Web Worker one. See
63
63
  * `./isolate.ts` for the constraints an implementation must meet, and
64
64
  * for what a Web Worker cannot bound that a worker thread can.
@@ -40,13 +40,18 @@ interface WorkerGlobal {
40
40
  * - `importScripts` becomes a function that throws. It stays a function
41
41
  * because the Lua engine's start-up detects a worker by
42
42
  * `typeof importScripts`.
43
+ * - Each of these names is replaced on the global and on every prototype
44
+ * the global inherits from, because a browser defines some of them
45
+ * (`fetch`) on `WorkerGlobalScope.prototype`.
43
46
  * - `fetch` keeps working for exactly one URL, the `glue.wasm` the job
44
47
  * named, because the engine loads its WebAssembly through it for every
45
48
  * script. Any other URL is refused.
46
49
  *
47
50
  * Net traffic is unaffected: it goes to the host with `postMessage`.
48
51
  * Idempotent. Covered by `worker-entry-browser.test.ts` and, in a real
49
- * browser, by the e2e suite.
52
+ * browser, by `e2e/browser-host/tests/worker-isolation.spec.ts`: the
53
+ * removed globals, a refused `fetch` to any other URL, and a prototype chain
54
+ * with no second `fetch`, `XMLHttpRequest` or `WebSocket`.
50
55
  */
51
56
  export declare function lockdownWorkerGlobals(scope: object, wasmUri: string | undefined): void;
52
57
  /**
@@ -68,13 +68,18 @@ function requestUrl(input, base) {
68
68
  * - `importScripts` becomes a function that throws. It stays a function
69
69
  * because the Lua engine's start-up detects a worker by
70
70
  * `typeof importScripts`.
71
+ * - Each of these names is replaced on the global and on every prototype
72
+ * the global inherits from, because a browser defines some of them
73
+ * (`fetch`) on `WorkerGlobalScope.prototype`.
71
74
  * - `fetch` keeps working for exactly one URL, the `glue.wasm` the job
72
75
  * named, because the engine loads its WebAssembly through it for every
73
76
  * script. Any other URL is refused.
74
77
  *
75
78
  * Net traffic is unaffected: it goes to the host with `postMessage`.
76
79
  * Idempotent. Covered by `worker-entry-browser.test.ts` and, in a real
77
- * browser, by the e2e suite.
80
+ * browser, by `e2e/browser-host/tests/worker-isolation.spec.ts`: the
81
+ * removed globals, a refused `fetch` to any other URL, and a prototype chain
82
+ * with no second `fetch`, `XMLHttpRequest` or `WebSocket`.
78
83
  */
79
84
  export function lockdownWorkerGlobals(scope, wasmUri) {
80
85
  const target = scope;
@@ -83,6 +88,7 @@ export function lockdownWorkerGlobals(scope, wasmUri) {
83
88
  const base = target.location?.href ?? 'http://localhost/';
84
89
  const allowed = wasmUri === undefined ? undefined : requestUrl(wasmUri, base);
85
90
  const realFetch = target.fetch;
91
+ const realImport = target['importScripts'];
86
92
  const shadow = (name, value) => {
87
93
  try {
88
94
  Object.defineProperty(scope, name, {
@@ -101,13 +107,39 @@ export function lockdownWorkerGlobals(scope, wasmUri) {
101
107
  }
102
108
  }
103
109
  };
110
+ /**
111
+ * Also replaces `name` on every prototype the scope inherits from. A
112
+ * browser defines `fetch` on `WorkerGlobalScope.prototype`, so shadowing
113
+ * only the global's own property would leave
114
+ * `Object.getPrototypeOf(self).fetch` as a way around the lockdown.
115
+ */
116
+ const shadowOnChain = (name, value) => {
117
+ for (let proto = Object.getPrototypeOf(scope); proto !== null && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
118
+ if (!Object.hasOwn(proto, name))
119
+ continue;
120
+ try {
121
+ Object.defineProperty(proto, name, {
122
+ value,
123
+ configurable: false,
124
+ writable: false,
125
+ });
126
+ }
127
+ catch {
128
+ // A frozen prototype keeps its member, and nothing more can be done.
129
+ }
130
+ }
131
+ };
132
+ const lock = (name, value) => {
133
+ shadow(name, value);
134
+ shadowOnChain(name, value);
135
+ };
104
136
  for (const name of REMOVED_GLOBALS)
105
- shadow(name, undefined);
106
- shadow('importScripts', () => {
137
+ lock(name, undefined);
138
+ lock('importScripts', () => {
107
139
  throw new Error('importScripts is not available in the run isolate');
108
140
  });
109
141
  if (typeof realFetch === 'function') {
110
- shadow('fetch', (input, init) => {
142
+ lock('fetch', (input, init) => {
111
143
  const url = requestUrl(input, base);
112
144
  if (allowed !== undefined && url === allowed) {
113
145
  return realFetch.call(scope, input, init);
@@ -115,6 +147,31 @@ export function lockdownWorkerGlobals(scope, wasmUri) {
115
147
  return Promise.reject(new TypeError('network access is not available in the run isolate'));
116
148
  });
117
149
  }
150
+ // Fail closed: a name that could not be replaced, on the global or on a
151
+ // prototype, means the isolate is not locked, so the run must not start.
152
+ const live = [];
153
+ const isLive = (holder, name) => {
154
+ const descriptor = Object.getOwnPropertyDescriptor(holder, name);
155
+ if (descriptor === undefined)
156
+ return false;
157
+ if (descriptor.get !== undefined || descriptor.set !== undefined) {
158
+ return true;
159
+ }
160
+ if (name === 'fetch')
161
+ return descriptor.value === realFetch;
162
+ if (name === 'importScripts')
163
+ return descriptor.value === realImport;
164
+ return descriptor.value !== undefined;
165
+ };
166
+ for (let holder = scope; holder !== null && holder !== Object.prototype; holder = Object.getPrototypeOf(holder)) {
167
+ for (const name of [...REMOVED_GLOBALS, 'importScripts', 'fetch']) {
168
+ if (isLive(holder, name))
169
+ live.push(name);
170
+ }
171
+ }
172
+ if (live.length > 0) {
173
+ throw new Error(`the run isolate could not be locked (${[...new Set(live)].join(', ')})`);
174
+ }
118
175
  Object.defineProperty(scope, '__markiiLocked', { value: true });
119
176
  }
120
177
  function start(scope) {
@@ -141,9 +198,9 @@ function start(scope) {
141
198
  return;
142
199
  }
143
200
  const job = message;
144
- lockdownWorkerGlobals(scope, job.wasmUri);
145
201
  let result;
146
202
  try {
203
+ lockdownWorkerGlobals(scope, job.wasmUri);
147
204
  result = await runJob(job, {
148
205
  createNet: () => bridge.provider,
149
206
  wasmUri: job.wasmUri,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markii/host",
3
- "version": "0.17.0-next.5",
3
+ "version": "0.17.0-next.6",
4
4
  "description": "Shared, host-neutral script-running layer for Markii hosts: tier gating, the grant model, address pinning, and the terminatable-isolate watchdog, plus the HostAdapter contract and createMarkiiHost. Entries: the neutral main entry, /internal (no semver promise), /node, /html-engine, /browser, /browser-worker-classic, /conformance.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -71,20 +71,20 @@
71
71
  "build:dist": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/build-dist-assets.mjs"
72
72
  },
73
73
  "dependencies": {
74
- "@markii/bundle": "^0.17.0-next.5",
75
- "@markii/core": "^0.17.0-next.5",
76
- "@markii/lua": "^0.17.0-next.5",
77
- "@markii/pack": "^0.17.0-next.5",
78
- "@markii/runtime": "^0.17.0-next.5",
79
- "@markii/stdlib": "^0.17.0-next.5",
74
+ "@markii/bundle": "^0.17.0-next.6",
75
+ "@markii/core": "^0.17.0-next.6",
76
+ "@markii/lua": "^0.17.0-next.6",
77
+ "@markii/pack": "^0.17.0-next.6",
78
+ "@markii/runtime": "^0.17.0-next.6",
79
+ "@markii/stdlib": "^0.17.0-next.6",
80
80
  "fflate": "^0.8.3"
81
81
  },
82
82
  "files": [
83
83
  "dist"
84
84
  ],
85
85
  "peerDependencies": {
86
- "@markii/html": "^0.17.0-next.5",
87
- "@markii/react": "^0.17.0-next.5"
86
+ "@markii/html": "^0.17.0-next.6",
87
+ "@markii/react": "^0.17.0-next.6"
88
88
  },
89
89
  "peerDependenciesMeta": {
90
90
  "@markii/html": {
@@ -95,8 +95,8 @@
95
95
  }
96
96
  },
97
97
  "devDependencies": {
98
- "@markii/html": "^0.17.0-next.5",
99
- "@markii/react": "^0.17.0-next.5",
98
+ "@markii/html": "^0.17.0-next.6",
99
+ "@markii/react": "^0.17.0-next.6",
100
100
  "esbuild": "^0.28.2"
101
101
  },
102
102
  "publishConfig": {