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

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 (55) hide show
  1. package/README.md +34 -1
  2. package/dist/browser-worker.classic.js +640 -0
  3. package/dist/browser-worker.js +19 -19
  4. package/dist/browser.d.ts +1 -1
  5. package/dist/browser.js +10 -3
  6. package/dist/conformance/host/05-export-html-pack-directive/expected.json +2 -0
  7. package/dist/conformance/host/06-install-pack-then-render/expected.json +6 -1
  8. package/dist/conformance/host/17-export-linked-set/expected.json +17 -0
  9. package/dist/conformance/host/17-export-linked-set/files/second.mk.md +3 -0
  10. package/dist/conformance/host/17-export-linked-set/note.mk.md +4 -0
  11. package/dist/conformance/host/17-export-linked-set/scenario.json +6 -0
  12. package/dist/conformance/host/README.md +17 -12
  13. package/dist/diagnostics/render-diagnostics.d.ts +1 -1
  14. package/dist/export/note-export.d.ts +1 -1
  15. package/dist/host/adapter.d.ts +13 -3
  16. package/dist/host/cascade-export-behavior.d.ts +24 -8
  17. package/dist/host/cascade-export-behavior.js +61 -1
  18. package/dist/host/conformance-runner.d.ts +3 -4
  19. package/dist/host/conformance-runner.js +58 -8
  20. package/dist/host/create-host.d.ts +65 -8
  21. package/dist/host/create-host.js +54 -23
  22. package/dist/host/export-behavior.d.ts +2 -1
  23. package/dist/host/export-behavior.js +1 -1
  24. package/dist/host/labels.d.ts +5 -14
  25. package/dist/host/labels.js +8 -22
  26. package/dist/host/pack-archive.d.ts +4 -10
  27. package/dist/host/pack-archive.js +11 -14
  28. package/dist/host/pack-install.d.ts +10 -21
  29. package/dist/host/pack-install.js +22 -16
  30. package/dist/host/pack-trust-behavior.js +1 -1
  31. package/dist/host/run-behavior.d.ts +4 -0
  32. package/dist/host/run-behavior.js +9 -1
  33. package/dist/host/terminal-packs.d.ts +1 -1
  34. package/dist/html-engine.js +4 -4
  35. package/dist/index.d.ts +19 -81
  36. package/dist/index.js +19 -62
  37. package/dist/internal.d.ts +28 -0
  38. package/dist/internal.js +31 -0
  39. package/dist/node-fs.d.ts +0 -3
  40. package/dist/node-fs.js +1 -21
  41. package/dist/node-worker.d.ts +8 -0
  42. package/dist/node-worker.js +36 -15
  43. package/dist/node.d.ts +1 -2
  44. package/dist/node.js +4 -3
  45. package/dist/packs/discover.d.ts +2 -0
  46. package/dist/packs/discover.js +1 -0
  47. package/dist/run/bundle-run.d.ts +1 -1
  48. package/dist/run/bundle-run.js +1 -1
  49. package/dist/run/net-bridge.js +7 -5
  50. package/dist/run/net-pinning.d.ts +2 -0
  51. package/dist/run/net-pinning.js +17 -3
  52. package/dist/run/net-provider.js +23 -12
  53. package/dist/run/net-refusal.d.ts +4 -0
  54. package/dist/run/net-refusal.js +24 -0
  55. package/package.json +28 -12
@@ -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 { ok: false, reason: `host "${hostname}" could not be resolved` };
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 { ok: false, reason: `host "${hostname}" resolved to no address` };
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 { ok: false, reason: `host "${hostname}" resolved to no address` };
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 {
@@ -21,6 +21,7 @@
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);
@@ -161,12 +162,14 @@ denial, signal) {
161
162
  res.on('error', (err) => settleReject(err));
162
163
  });
163
164
  req.on('error', (err) => {
164
- settleReject(denial(`request failed: ${describeThrown(err)}`));
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
  });
166
169
  if (signal !== undefined) {
167
170
  const onAbort = () => {
168
171
  req.destroy();
169
- settleReject(denial('the run has ended'));
172
+ settleReject(new Error('the run has ended'));
170
173
  };
171
174
  if (signal.aborted)
172
175
  onAbort();
@@ -241,6 +244,10 @@ denial, lookup = defaultHostLookup,
241
244
  /** Aborted when the run ends: an in-flight request is destroyed and no further hop is made. */
242
245
  signal) {
243
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));
244
251
  async function fetchAllowed(startUrl, method, body) {
245
252
  let currentUrl = startUrl;
246
253
  for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
@@ -249,43 +256,47 @@ signal) {
249
256
  parsed = new URL(currentUrl);
250
257
  }
251
258
  catch {
252
- throw denial(`"${currentUrl}" is not a valid URL`);
259
+ throw refuse(`"${currentUrl}" is not a valid URL`);
253
260
  }
254
261
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
255
- throw denial(`scheme "${parsed.protocol}" is not allowed`);
262
+ throw refuse(`scheme "${parsed.protocol}" is not allowed`);
256
263
  }
257
264
  // N-4: a URL carrying credentials is refused outright, on every hop.
258
265
  if (parsed.username !== '' || parsed.password !== '') {
259
- throw denial(`URL "${currentUrl}" embeds credentials, which is not allowed`);
266
+ throw refuse(`URL "${currentUrl}" embeds credentials, which is not allowed`);
260
267
  }
261
268
  if (signal?.aborted)
262
- throw denial('the run has ended');
269
+ throw new Error('the run has ended');
263
270
  const host = parsed.hostname.toLowerCase();
264
271
  if (!allowed.has(host)) {
265
- throw denial(`host "${host}" is not on the run's allowlist`);
272
+ throw refuse(`host "${host}" is not on the run's allowlist`);
266
273
  }
267
274
  // Issue #10: resolve-then-pin, closing the DNS-rebinding / private-
268
275
  // range SSRF gap a hostname allowlist alone cannot see.
269
276
  const pinResult = await pinHostAddress(host, policy, lookup);
270
277
  if (!pinResult.ok) {
271
- throw denial(pinResult.reason);
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);
272
283
  }
273
- const response = await issueRequest(parsed, method, body, pinResult.pinned, maxFetchBytes, denial, signal);
284
+ const response = await issueRequest(parsed, method, body, pinResult.pinned, maxFetchBytes, refuse, signal);
274
285
  if (response.status >= 300 && response.status < 400) {
275
286
  if (!response.location) {
276
- throw denial('redirect response carried no Location header');
287
+ throw refuse('redirect response carried no Location header');
277
288
  }
278
289
  try {
279
290
  currentUrl = new URL(response.location, currentUrl).toString();
280
291
  }
281
292
  catch {
282
- throw denial('redirect response carried an unparseable Location header');
293
+ throw refuse('redirect response carried an unparseable Location header');
283
294
  }
284
295
  continue;
285
296
  }
286
297
  return { status: response.status, body: response.body };
287
298
  }
288
- throw denial(`exceeded ${MAX_REDIRECTS} redirects`);
299
+ throw refuse(`exceeded ${MAX_REDIRECTS} redirects`);
289
300
  }
290
301
  return {
291
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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@markii/host",
3
- "version": "0.17.0-next.3",
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, /node, /browser, /conformance.",
3
+ "version": "0.17.0-next.5",
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",
7
7
  "mk.md",
@@ -27,11 +27,21 @@
27
27
  "import": "./dist/index.js",
28
28
  "default": "./dist/index.js"
29
29
  },
30
+ "./internal": {
31
+ "types": "./dist/internal.d.ts",
32
+ "import": "./dist/internal.js",
33
+ "default": "./dist/internal.js"
34
+ },
30
35
  "./node": {
31
36
  "types": "./dist/node.d.ts",
32
37
  "import": "./dist/node.js",
33
38
  "default": "./dist/node.js"
34
39
  },
40
+ "./html-engine": {
41
+ "types": "./dist/html-engine.d.ts",
42
+ "import": "./dist/html-engine.js",
43
+ "default": "./dist/html-engine.js"
44
+ },
35
45
  "./node-worker": {
36
46
  "default": "./dist/node-worker.js"
37
47
  },
@@ -40,12 +50,18 @@
40
50
  "import": "./dist/browser.js",
41
51
  "default": "./dist/browser.js"
42
52
  },
53
+ "./browser-worker-classic": {
54
+ "default": "./dist/browser-worker.classic.js"
55
+ },
43
56
  "./conformance": {
44
57
  "types": "./dist/conformance.d.ts",
45
58
  "import": "./dist/conformance.js",
46
59
  "default": "./dist/conformance.js"
47
60
  }
48
61
  },
62
+ "engines": {
63
+ "node": ">=22"
64
+ },
49
65
  "scripts": {
50
66
  "pretest": "npm run generate:doc-css --workspace @markii/html",
51
67
  "test": "vitest run",
@@ -55,20 +71,20 @@
55
71
  "build:dist": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/build-dist-assets.mjs"
56
72
  },
57
73
  "dependencies": {
58
- "@markii/bundle": "^0.17.0-next.3",
59
- "@markii/core": "^0.17.0-next.3",
60
- "@markii/lua": "^0.17.0-next.3",
61
- "@markii/pack": "^0.17.0-next.3",
62
- "@markii/runtime": "^0.17.0-next.3",
63
- "@markii/stdlib": "^0.17.0-next.3",
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",
64
80
  "fflate": "^0.8.3"
65
81
  },
66
82
  "files": [
67
83
  "dist"
68
84
  ],
69
85
  "peerDependencies": {
70
- "@markii/html": "^0.17.0-next.3",
71
- "@markii/react": "^0.17.0-next.3"
86
+ "@markii/html": "^0.17.0-next.5",
87
+ "@markii/react": "^0.17.0-next.5"
72
88
  },
73
89
  "peerDependenciesMeta": {
74
90
  "@markii/html": {
@@ -79,8 +95,8 @@
79
95
  }
80
96
  },
81
97
  "devDependencies": {
82
- "@markii/html": "^0.17.0-next.3",
83
- "@markii/react": "^0.17.0-next.3",
98
+ "@markii/html": "^0.17.0-next.5",
99
+ "@markii/react": "^0.17.0-next.5",
84
100
  "esbuild": "^0.28.2"
85
101
  },
86
102
  "publishConfig": {