@markii/host 0.17.0-next.3 → 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 +6 -0
- package/dist/browser-worker.classic.js +640 -0
- package/dist/browser-worker.js +19 -19
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +10 -3
- 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 +4 -2
- package/dist/host/conformance-runner.d.ts +0 -3
- package/dist/host/conformance-runner.js +18 -6
- package/dist/host/create-host.d.ts +25 -5
- package/dist/host/create-host.js +34 -23
- package/dist/host/export-behavior.d.ts +2 -1
- package/dist/host/export-behavior.js +1 -1
- package/dist/host/labels.d.ts +5 -14
- package/dist/host/labels.js +8 -22
- package/dist/host/pack-archive.d.ts +4 -10
- package/dist/host/pack-archive.js +11 -14
- package/dist/host/pack-install.d.ts +10 -21
- package/dist/host/pack-install.js +22 -16
- package/dist/html-engine.js +4 -4
- package/dist/index.d.ts +15 -80
- package/dist/index.js +18 -62
- 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 +36 -15
- package/dist/node.d.ts +1 -2
- package/dist/node.js +4 -3
- package/dist/packs/discover.d.ts +2 -0
- package/dist/packs/discover.js +1 -0
- 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.js +23 -12
- package/dist/run/net-refusal.d.ts +4 -0
- package/dist/run/net-refusal.js +24 -0
- package/package.json +28 -12
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
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
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 {
|
package/dist/run/net-provider.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
259
|
+
throw refuse(`"${currentUrl}" is not a valid URL`);
|
|
253
260
|
}
|
|
254
261
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
255
|
-
throw
|
|
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
|
|
266
|
+
throw refuse(`URL "${currentUrl}" embeds credentials, which is not allowed`);
|
|
260
267
|
}
|
|
261
268
|
if (signal?.aborted)
|
|
262
|
-
throw
|
|
269
|
+
throw new Error('the run has ended');
|
|
263
270
|
const host = parsed.hostname.toLowerCase();
|
|
264
271
|
if (!allowed.has(host)) {
|
|
265
|
-
throw
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.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",
|
|
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.
|
|
59
|
-
"@markii/core": "^0.17.0-next.
|
|
60
|
-
"@markii/lua": "^0.17.0-next.
|
|
61
|
-
"@markii/pack": "^0.17.0-next.
|
|
62
|
-
"@markii/runtime": "^0.17.0-next.
|
|
63
|
-
"@markii/stdlib": "^0.17.0-next.
|
|
74
|
+
"@markii/bundle": "^0.17.0-next.4",
|
|
75
|
+
"@markii/core": "^0.17.0-next.4",
|
|
76
|
+
"@markii/lua": "^0.17.0-next.4",
|
|
77
|
+
"@markii/pack": "^0.17.0-next.4",
|
|
78
|
+
"@markii/runtime": "^0.17.0-next.4",
|
|
79
|
+
"@markii/stdlib": "^0.17.0-next.4",
|
|
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.
|
|
71
|
-
"@markii/react": "^0.17.0-next.
|
|
86
|
+
"@markii/html": "^0.17.0-next.4",
|
|
87
|
+
"@markii/react": "^0.17.0-next.4"
|
|
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.
|
|
83
|
-
"@markii/react": "^0.17.0-next.
|
|
98
|
+
"@markii/html": "^0.17.0-next.4",
|
|
99
|
+
"@markii/react": "^0.17.0-next.4",
|
|
84
100
|
"esbuild": "^0.28.2"
|
|
85
101
|
},
|
|
86
102
|
"publishConfig": {
|