@johnhenry/sitefile 0.0.0-stage → 0.0.1
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/CHANGELOG.md +152 -0
- package/LICENSE +21 -0
- package/README.md +513 -2
- package/lib/src/archive-writers.d.ts +31 -0
- package/lib/src/archive-writers.js +197 -0
- package/lib/src/attest.d.ts +126 -0
- package/lib/src/attest.js +220 -0
- package/lib/src/bake.d.ts +74 -0
- package/lib/src/bake.js +372 -0
- package/lib/src/base-image.d.ts +76 -0
- package/lib/src/base-image.js +138 -0
- package/lib/src/build-args.d.ts +26 -0
- package/lib/src/build-args.js +164 -0
- package/lib/src/cache-limit.d.ts +54 -0
- package/lib/src/cache-limit.js +239 -0
- package/lib/src/casefold-data.d.ts +2 -0
- package/lib/src/casefold-data.js +217 -0
- package/lib/src/cbor.d.ts +39 -0
- package/lib/src/cbor.js +174 -0
- package/lib/src/checklinks.d.ts +42 -0
- package/lib/src/checklinks.js +120 -0
- package/lib/src/cli/bake.d.ts +2 -0
- package/lib/src/cli/bake.js +179 -0
- package/lib/src/cli/build-image.d.ts +21 -0
- package/lib/src/cli/build-image.js +42 -0
- package/lib/src/cli/cache.d.ts +8 -0
- package/lib/src/cli/cache.js +119 -0
- package/lib/src/cli/common.d.ts +29 -0
- package/lib/src/cli/common.js +100 -0
- package/lib/src/cli/git-context.d.ts +18 -0
- package/lib/src/cli/git-context.js +48 -0
- package/lib/src/cli/images.d.ts +6 -0
- package/lib/src/cli/images.js +285 -0
- package/lib/src/cli/index.d.ts +10 -0
- package/lib/src/cli/index.js +95 -0
- package/lib/src/cli/network.d.ts +34 -0
- package/lib/src/cli/network.js +56 -0
- package/lib/src/cli/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +99 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +127 -0
- package/lib/src/cli/serve-worker.d.ts +2 -0
- package/lib/src/cli/serve-worker.js +42 -0
- package/lib/src/cli/servers.d.ts +15 -0
- package/lib/src/cli/servers.js +467 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +97 -0
- package/lib/src/cli/watch.d.ts +26 -0
- package/lib/src/cli/watch.js +81 -0
- package/lib/src/cli.d.ts +12 -0
- package/lib/src/cli.js +522 -0
- package/lib/src/csv.d.ts +27 -0
- package/lib/src/csv.js +117 -0
- package/lib/src/deflate.d.ts +76 -0
- package/lib/src/deflate.js +604 -0
- package/lib/src/digest.d.ts +3 -0
- package/lib/src/digest.js +7 -0
- package/lib/src/errors.d.ts +56 -0
- package/lib/src/errors.js +29 -0
- package/lib/src/executor.d.ts +393 -0
- package/lib/src/executor.js +2201 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +23 -0
- package/lib/src/export-formats.js +63 -0
- package/lib/src/exporter.d.ts +103 -0
- package/lib/src/exporter.js +676 -0
- package/lib/src/fingerprint.d.ts +40 -0
- package/lib/src/fingerprint.js +168 -0
- package/lib/src/frontmatter.d.ts +22 -0
- package/lib/src/frontmatter.js +45 -0
- package/lib/src/index.d.ts +13 -0
- package/lib/src/index.js +13 -0
- package/lib/src/language/ast.d.ts +360 -0
- package/lib/src/language/ast.js +57 -0
- package/lib/src/language/parser.d.ts +3 -0
- package/lib/src/language/parser.js +612 -0
- package/lib/src/language/scanner.d.ts +110 -0
- package/lib/src/language/scanner.js +449 -0
- package/lib/src/language/validate.d.ts +13 -0
- package/lib/src/language/validate.js +367 -0
- package/lib/src/links.d.ts +79 -0
- package/lib/src/links.js +583 -0
- package/lib/src/lint.d.ts +10 -0
- package/lib/src/lint.js +223 -0
- package/lib/src/markdown.d.ts +29 -0
- package/lib/src/markdown.js +612 -0
- package/lib/src/media.d.ts +82 -0
- package/lib/src/media.js +650 -0
- package/lib/src/memo.d.ts +57 -0
- package/lib/src/memo.js +93 -0
- package/lib/src/oci.d.ts +178 -0
- package/lib/src/oci.js +380 -0
- package/lib/src/outdated.d.ts +46 -0
- package/lib/src/outdated.js +195 -0
- package/lib/src/pages.d.ts +10 -0
- package/lib/src/pages.js +54 -0
- package/lib/src/paths.d.ts +39 -0
- package/lib/src/paths.js +101 -0
- package/lib/src/planner.d.ts +297 -0
- package/lib/src/planner.js +410 -0
- package/lib/src/plugins/cache.d.ts +96 -0
- package/lib/src/plugins/cache.js +325 -0
- package/lib/src/plugins/exec.d.ts +51 -0
- package/lib/src/plugins/exec.js +144 -0
- package/lib/src/plugins/host.d.ts +75 -0
- package/lib/src/plugins/host.js +277 -0
- package/lib/src/plugins/wasm.d.ts +23 -0
- package/lib/src/plugins/wasm.js +249 -0
- package/lib/src/profile.d.ts +42 -0
- package/lib/src/profile.js +68 -0
- package/lib/src/ref.d.ts +40 -0
- package/lib/src/ref.js +79 -0
- package/lib/src/registry-auth.d.ts +32 -0
- package/lib/src/registry-auth.js +151 -0
- package/lib/src/registry-cache.d.ts +71 -0
- package/lib/src/registry-cache.js +333 -0
- package/lib/src/registry-ops.d.ts +51 -0
- package/lib/src/registry-ops.js +248 -0
- package/lib/src/registry.d.ts +115 -0
- package/lib/src/registry.js +524 -0
- package/lib/src/render.d.ts +134 -0
- package/lib/src/render.js +576 -0
- package/lib/src/resources/archive.d.ts +11 -0
- package/lib/src/resources/archive.js +306 -0
- package/lib/src/resources/bundle-data.d.ts +16 -0
- package/lib/src/resources/bundle-data.js +50 -0
- package/lib/src/resources/convert.d.ts +12 -0
- package/lib/src/resources/convert.js +63 -0
- package/lib/src/resources/documents.d.ts +35 -0
- package/lib/src/resources/documents.js +122 -0
- package/lib/src/resources/git.d.ts +103 -0
- package/lib/src/resources/git.js +720 -0
- package/lib/src/resources/https.d.ts +74 -0
- package/lib/src/resources/https.js +413 -0
- package/lib/src/resources/ignore.d.ts +33 -0
- package/lib/src/resources/ignore.js +119 -0
- package/lib/src/resources/local.d.ts +64 -0
- package/lib/src/resources/local.js +248 -0
- package/lib/src/resources/lockfile.d.ts +52 -0
- package/lib/src/resources/lockfile.js +172 -0
- package/lib/src/resources/netpolicy.d.ts +6 -0
- package/lib/src/resources/netpolicy.js +82 -0
- package/lib/src/resources/network.d.ts +70 -0
- package/lib/src/resources/network.js +304 -0
- package/lib/src/resources/types.d.ts +69 -0
- package/lib/src/resources/types.js +13 -0
- package/lib/src/resources/vendor.d.ts +94 -0
- package/lib/src/resources/vendor.js +296 -0
- package/lib/src/resources/wbn-reader.d.ts +44 -0
- package/lib/src/resources/wbn-reader.js +302 -0
- package/lib/src/schema.d.ts +25 -0
- package/lib/src/schema.js +169 -0
- package/lib/src/secrets.d.ts +21 -0
- package/lib/src/secrets.js +103 -0
- package/lib/src/serve-rules.d.ts +29 -0
- package/lib/src/serve-rules.js +123 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +85 -0
- package/lib/src/servers.js +225 -0
- package/lib/src/site.d.ts +60 -0
- package/lib/src/site.js +169 -0
- package/lib/src/store.d.ts +151 -0
- package/lib/src/store.js +596 -0
- package/lib/src/tree.d.ts +50 -0
- package/lib/src/tree.js +134 -0
- package/lib/src/values.d.ts +7 -0
- package/lib/src/values.js +37 -0
- package/lib/src/vendor.d.ts +19 -0
- package/lib/src/vendor.js +145 -0
- package/lib/src/watch.d.ts +44 -0
- package/lib/src/watch.js +137 -0
- package/lib/src/wbn.d.ts +26 -0
- package/lib/src/wbn.js +241 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +59 -4
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type SourceSpan } from '../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Explicit network settings for everything sitefile fetches (IMPORT over http(s), Git, registries).
|
|
4
|
+
* Nothing here is read from the environment implicitly: `HTTP(S)_PROXY` stays ignored, and the CLI
|
|
5
|
+
* reads `SITEFILE_PROXY` and friends through {@link networkFromEnv}. Embedders pass this in code
|
|
6
|
+
* (`FetchPolicy.network`, `RegistryOptions.network`).
|
|
7
|
+
*/
|
|
8
|
+
export interface NetworkSettings {
|
|
9
|
+
/** `http://[user:password@]host[:port]`: a CONNECT tunnel for https targets, absolute-form requests for http. */
|
|
10
|
+
readonly proxy?: string | undefined;
|
|
11
|
+
/** Hosts reached directly, never through the proxy (`NO_PROXY` syntax, see {@link hostMatches}). */
|
|
12
|
+
readonly noProxy?: readonly string[] | undefined;
|
|
13
|
+
/** When non-empty, only hosts matching one of these patterns may be contacted. */
|
|
14
|
+
readonly allowHosts?: readonly string[] | undefined;
|
|
15
|
+
/** Hosts never contacted; a deny rule beats an allow rule. */
|
|
16
|
+
readonly denyHosts?: readonly string[] | undefined;
|
|
17
|
+
/** Time limit for establishing the TCP connection (to the target, or to the proxy), ms. */
|
|
18
|
+
readonly connectTimeoutMs?: number | undefined;
|
|
19
|
+
/** Longest wait for data once connected (idle time), ms. */
|
|
20
|
+
readonly readTimeoutMs?: number | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** A configured HTTP proxy, parsed. */
|
|
23
|
+
export interface ProxyConfig {
|
|
24
|
+
readonly host: string;
|
|
25
|
+
readonly port: number;
|
|
26
|
+
/** `Basic …` when the URL carries credentials. */
|
|
27
|
+
readonly authorization?: string | undefined;
|
|
28
|
+
/** The proxy URL without credentials, for messages. */
|
|
29
|
+
readonly shown: string;
|
|
30
|
+
}
|
|
31
|
+
/** Split a list given as one string (commas and/or whitespace). */
|
|
32
|
+
export declare function splitList(raw: string | undefined): string[];
|
|
33
|
+
/** Parse a proxy URL, or throw E_USAGE. Credentials never appear in the message. */
|
|
34
|
+
export declare function parseProxy(raw: string): ProxyConfig;
|
|
35
|
+
/**
|
|
36
|
+
* True when `host` (as in a URL, IPv6 without brackets) on `port` matches `pattern`:
|
|
37
|
+
* `*` every host
|
|
38
|
+
* `example.com` example.com and every subdomain (`.example.com` and `*.example.com` too)
|
|
39
|
+
* `10.0.0.0/8`, `fd00::/8` IP-literal hosts in that network; `192.0.2.1`, `::1` that address
|
|
40
|
+
* any of these + `:port` only that port (`[::1]:8080` for IPv6)
|
|
41
|
+
* Patterns match the host as written in the URL: a name pattern never matches an IP literal and an
|
|
42
|
+
* IP pattern never matches a name, whatever it resolves to (the private-address rule covers that).
|
|
43
|
+
*/
|
|
44
|
+
export declare function hostMatches(host: string, port: number, pattern: string): boolean;
|
|
45
|
+
/** Default port of an http(s) URL. */
|
|
46
|
+
export declare function urlPort(url: URL): number;
|
|
47
|
+
/**
|
|
48
|
+
* Apply the allow/deny host rules to one hop (every redirect, token realm and blob CDN hop included).
|
|
49
|
+
* Throws E_HOST_BLOCKED naming the rule; a deny rule beats an allow rule.
|
|
50
|
+
*/
|
|
51
|
+
export declare function checkHostRules(url: URL, network: NetworkSettings | undefined, shown: string, span?: SourceSpan): void;
|
|
52
|
+
/** The proxy to use for `url`, or undefined for a direct connection (no proxy, or a no-proxy match). */
|
|
53
|
+
export declare function proxyFor(url: URL, network: NetworkSettings | undefined): ProxyConfig | undefined;
|
|
54
|
+
/** A usage problem in `network` (bad proxy URL, host pattern or timeout), or undefined. */
|
|
55
|
+
export declare function networkProblem(network: NetworkSettings): string | undefined;
|
|
56
|
+
/** Parse `SITEFILE_FETCH_TIMEOUT`: `<ms>` (connect and read) or `connect=<ms>,read=<ms>` (either part optional). */
|
|
57
|
+
export declare function parseFetchTimeout(raw: string): {
|
|
58
|
+
connectTimeoutMs?: number;
|
|
59
|
+
readTimeoutMs?: number;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Network settings from `SITEFILE_PROXY`, `SITEFILE_NO_PROXY`, `SITEFILE_ALLOW_HOSTS`,
|
|
63
|
+
* `SITEFILE_DENY_HOSTS` (comma- or space-separated) and `SITEFILE_FETCH_TIMEOUT`. Empty values are unset.
|
|
64
|
+
* `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` are never read.
|
|
65
|
+
*/
|
|
66
|
+
export declare function networkFromEnv(env?: NodeJS.ProcessEnv): NetworkSettings;
|
|
67
|
+
/** `over` on top of `base`: the proxy and timeouts are replaced, the host lists are joined (duplicates dropped). */
|
|
68
|
+
export declare function mergeNetwork(base: NetworkSettings, over: NetworkSettings): NetworkSettings;
|
|
69
|
+
/** True when any setting is present. */
|
|
70
|
+
export declare function hasNetworkSettings(n: NetworkSettings | undefined): boolean;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
import { SitefileError } from '../errors.js';
|
|
3
|
+
/** Split a list given as one string (commas and/or whitespace). */
|
|
4
|
+
export function splitList(raw) {
|
|
5
|
+
return (raw ?? '')
|
|
6
|
+
.split(/[\s,]+/)
|
|
7
|
+
.map((s) => s.trim())
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
/** A proxy URL with any credentials removed. */
|
|
11
|
+
function shownProxy(raw) {
|
|
12
|
+
try {
|
|
13
|
+
const u = new URL(raw);
|
|
14
|
+
u.username = '';
|
|
15
|
+
u.password = '';
|
|
16
|
+
return u.href.replace(/\/$/, '');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '<invalid url>';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Percent-decode, keeping invalid escapes as written (bytes decoded as UTF-8, lossily). */
|
|
23
|
+
function percentDecode(s) {
|
|
24
|
+
const bytes = [];
|
|
25
|
+
for (let i = 0; i < s.length;) {
|
|
26
|
+
if (s[i] === '%' && /^[0-9a-fA-F]{2}$/.test(s.slice(i + 1, i + 3))) {
|
|
27
|
+
bytes.push(parseInt(s.slice(i + 1, i + 3), 16));
|
|
28
|
+
i += 3;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const c = Buffer.from(s[i], 'utf8');
|
|
32
|
+
bytes.push(...c);
|
|
33
|
+
i += 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return Buffer.from(bytes).toString('utf8');
|
|
37
|
+
}
|
|
38
|
+
/** Parse a proxy URL, or throw E_USAGE. Credentials never appear in the message. */
|
|
39
|
+
export function parseProxy(raw) {
|
|
40
|
+
const bad = () => new SitefileError('E_USAGE', `invalid proxy URL '${shownProxy(raw)}': expected http://[user:password@]host[:port]`);
|
|
41
|
+
let u;
|
|
42
|
+
try {
|
|
43
|
+
u = new URL(raw);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw bad();
|
|
47
|
+
}
|
|
48
|
+
if (u.protocol !== 'http:' || u.hostname === '' || (u.pathname !== '/' && u.pathname !== '') || u.search !== '' || u.hash !== '')
|
|
49
|
+
throw bad();
|
|
50
|
+
const host = u.hostname.replace(/^\[|\]$/g, '');
|
|
51
|
+
const port = u.port === '' ? 80 : Number(u.port);
|
|
52
|
+
const user = percentDecode(u.username);
|
|
53
|
+
const pass = percentDecode(u.password);
|
|
54
|
+
const authorization = user !== '' || pass !== '' ? `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}` : undefined;
|
|
55
|
+
return { host, port, authorization, shown: shownProxy(raw) };
|
|
56
|
+
}
|
|
57
|
+
function ipToBig(ip) {
|
|
58
|
+
const family = isIP(ip);
|
|
59
|
+
if (family === 4) {
|
|
60
|
+
const addr = ip.split('.').reduce((acc, b) => (acc << 8n) | BigInt(Number(b)), 0n);
|
|
61
|
+
return { family: 4, addr };
|
|
62
|
+
}
|
|
63
|
+
if (family === 6) {
|
|
64
|
+
let s = ip.toLowerCase();
|
|
65
|
+
const v4 = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(s);
|
|
66
|
+
if (v4) {
|
|
67
|
+
const [b0, b1, b2, b3] = v4.slice(1).map(Number);
|
|
68
|
+
s = s.slice(0, v4.index) + ((b0 << 8) | b1).toString(16) + ':' + ((b2 << 8) | b3).toString(16);
|
|
69
|
+
}
|
|
70
|
+
const [head, tail] = s.split('::');
|
|
71
|
+
const h = head ? head.split(':') : [];
|
|
72
|
+
const t = tail ? tail.split(':') : [];
|
|
73
|
+
const groups = [...h, ...(tail === undefined ? [] : new Array(8 - h.length - t.length).fill('0')), ...t];
|
|
74
|
+
if (groups.length !== 8)
|
|
75
|
+
return undefined;
|
|
76
|
+
return { family: 6, addr: groups.reduce((acc, g) => (acc << 16n) | BigInt(parseInt(g, 16)), 0n) };
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const HOSTNAME_RE = /^[a-z0-9_-]+(?:\.[a-z0-9_-]+)*$/;
|
|
81
|
+
/** Parse one pattern; undefined when it is not valid. */
|
|
82
|
+
function parsePattern(raw) {
|
|
83
|
+
let p = raw.trim().toLowerCase();
|
|
84
|
+
if (p === '')
|
|
85
|
+
return undefined;
|
|
86
|
+
if (p === '*')
|
|
87
|
+
return { any: true };
|
|
88
|
+
let port;
|
|
89
|
+
// [v6]:port, [v6], v4:port, name:port. A bare IPv6 (two or more colons) has no port.
|
|
90
|
+
const bracket = /^\[([^\]]+)\](?::(\d+))?$/.exec(p);
|
|
91
|
+
if (bracket) {
|
|
92
|
+
p = bracket[1];
|
|
93
|
+
if (bracket[2] !== undefined)
|
|
94
|
+
port = Number(bracket[2]);
|
|
95
|
+
}
|
|
96
|
+
else if ((p.match(/:/g) ?? []).length === 1) {
|
|
97
|
+
const i = p.indexOf(':');
|
|
98
|
+
const ps = p.slice(i + 1);
|
|
99
|
+
if (!/^\d+$/.test(ps))
|
|
100
|
+
return undefined;
|
|
101
|
+
port = Number(ps);
|
|
102
|
+
p = p.slice(0, i);
|
|
103
|
+
}
|
|
104
|
+
if (port !== undefined && (port < 1 || port > 65535))
|
|
105
|
+
return undefined;
|
|
106
|
+
const withPort = (x) => (port === undefined ? x : { ...x, port });
|
|
107
|
+
const slash = p.indexOf('/');
|
|
108
|
+
if (slash !== -1) {
|
|
109
|
+
const ip = ipToBig(p.slice(0, slash));
|
|
110
|
+
const ps = p.slice(slash + 1);
|
|
111
|
+
if (!ip || !/^\d+$/.test(ps))
|
|
112
|
+
return undefined;
|
|
113
|
+
const prefix = Number(ps);
|
|
114
|
+
if (prefix > (ip.family === 4 ? 32 : 128))
|
|
115
|
+
return undefined;
|
|
116
|
+
return withPort({ net: { ...ip, prefix } });
|
|
117
|
+
}
|
|
118
|
+
const ip = ipToBig(p);
|
|
119
|
+
if (ip)
|
|
120
|
+
return withPort({ net: { ...ip, prefix: ip.family === 4 ? 32 : 128 } });
|
|
121
|
+
if (p.startsWith('*.'))
|
|
122
|
+
p = p.slice(2);
|
|
123
|
+
else if (p.startsWith('.'))
|
|
124
|
+
p = p.slice(1);
|
|
125
|
+
if (p.endsWith('.'))
|
|
126
|
+
p = p.slice(0, -1);
|
|
127
|
+
if (!HOSTNAME_RE.test(p))
|
|
128
|
+
return undefined;
|
|
129
|
+
return withPort({ domain: p });
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* True when `host` (as in a URL, IPv6 without brackets) on `port` matches `pattern`:
|
|
133
|
+
* `*` every host
|
|
134
|
+
* `example.com` example.com and every subdomain (`.example.com` and `*.example.com` too)
|
|
135
|
+
* `10.0.0.0/8`, `fd00::/8` IP-literal hosts in that network; `192.0.2.1`, `::1` that address
|
|
136
|
+
* any of these + `:port` only that port (`[::1]:8080` for IPv6)
|
|
137
|
+
* Patterns match the host as written in the URL: a name pattern never matches an IP literal and an
|
|
138
|
+
* IP pattern never matches a name, whatever it resolves to (the private-address rule covers that).
|
|
139
|
+
*/
|
|
140
|
+
export function hostMatches(host, port, pattern) {
|
|
141
|
+
const p = parsePattern(pattern);
|
|
142
|
+
return p !== undefined && matches(normalizeHost(host), port, p);
|
|
143
|
+
}
|
|
144
|
+
function normalizeHost(host) {
|
|
145
|
+
let h = host.toLowerCase();
|
|
146
|
+
if (h.startsWith('[') && h.endsWith(']'))
|
|
147
|
+
h = h.slice(1, -1);
|
|
148
|
+
if (h.endsWith('.'))
|
|
149
|
+
h = h.slice(0, -1);
|
|
150
|
+
return h;
|
|
151
|
+
}
|
|
152
|
+
function matches(host, port, p) {
|
|
153
|
+
if (p.any)
|
|
154
|
+
return true;
|
|
155
|
+
if (p.port !== undefined && p.port !== port)
|
|
156
|
+
return false;
|
|
157
|
+
if (p.domain !== undefined)
|
|
158
|
+
return host === p.domain || host.endsWith(`.${p.domain}`);
|
|
159
|
+
if (p.net) {
|
|
160
|
+
const ip = ipToBig(host);
|
|
161
|
+
if (!ip || ip.family !== p.net.family)
|
|
162
|
+
return false;
|
|
163
|
+
const bits = BigInt(ip.family === 4 ? 32 : 128);
|
|
164
|
+
const shift = bits - BigInt(p.net.prefix);
|
|
165
|
+
return ip.addr >> shift === p.net.addr >> shift;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
/** The first pattern of `list` that matches, if any. */
|
|
170
|
+
function firstMatch(host, port, list) {
|
|
171
|
+
const h = normalizeHost(host);
|
|
172
|
+
for (const raw of list ?? []) {
|
|
173
|
+
const p = parsePattern(raw);
|
|
174
|
+
if (p && matches(h, port, p))
|
|
175
|
+
return raw.trim();
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
/** Default port of an http(s) URL. */
|
|
180
|
+
export function urlPort(url) {
|
|
181
|
+
return url.port !== '' ? Number(url.port) : url.protocol === 'http:' ? 80 : 443;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Apply the allow/deny host rules to one hop (every redirect, token realm and blob CDN hop included).
|
|
185
|
+
* Throws E_HOST_BLOCKED naming the rule; a deny rule beats an allow rule.
|
|
186
|
+
*/
|
|
187
|
+
export function checkHostRules(url, network, shown, span) {
|
|
188
|
+
if (!network)
|
|
189
|
+
return;
|
|
190
|
+
const host = normalizeHost(url.hostname);
|
|
191
|
+
const port = urlPort(url);
|
|
192
|
+
const denied = firstMatch(host, port, network.denyHosts);
|
|
193
|
+
if (denied !== undefined) {
|
|
194
|
+
throw new SitefileError('E_HOST_BLOCKED', `refusing to connect to ${host}: it matches deny-hosts rule '${denied}' (${shown})`, span);
|
|
195
|
+
}
|
|
196
|
+
if ((network.allowHosts ?? []).length > 0 && firstMatch(host, port, network.allowHosts) === undefined) {
|
|
197
|
+
throw new SitefileError('E_HOST_BLOCKED', `refusing to connect to ${host}: it matches no allow-hosts rule (${shown})`, span);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** The proxy to use for `url`, or undefined for a direct connection (no proxy, or a no-proxy match). */
|
|
201
|
+
export function proxyFor(url, network) {
|
|
202
|
+
if (!network?.proxy)
|
|
203
|
+
return undefined;
|
|
204
|
+
if (firstMatch(url.hostname, urlPort(url), network.noProxy) !== undefined)
|
|
205
|
+
return undefined;
|
|
206
|
+
return parseProxy(network.proxy);
|
|
207
|
+
}
|
|
208
|
+
// ------------------------------------------------------------------ configuration
|
|
209
|
+
/** A usage problem in `network` (bad proxy URL, host pattern or timeout), or undefined. */
|
|
210
|
+
export function networkProblem(network) {
|
|
211
|
+
if (network.proxy) {
|
|
212
|
+
try {
|
|
213
|
+
parseProxy(network.proxy);
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
return e.message;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const [name, list] of [
|
|
220
|
+
['no-proxy', network.noProxy],
|
|
221
|
+
['allow-hosts', network.allowHosts],
|
|
222
|
+
['deny-hosts', network.denyHosts],
|
|
223
|
+
]) {
|
|
224
|
+
for (const p of list ?? []) {
|
|
225
|
+
if (!parsePattern(p))
|
|
226
|
+
return `invalid ${name} pattern '${p}': expected *, a host name (example.com, .example.com, *.example.com), an IP address or a CIDR network, optionally with :port`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const [name, v] of [
|
|
230
|
+
['connect timeout', network.connectTimeoutMs],
|
|
231
|
+
['read timeout', network.readTimeoutMs],
|
|
232
|
+
]) {
|
|
233
|
+
if (v !== undefined && (!Number.isSafeInteger(v) || v <= 0))
|
|
234
|
+
return `the ${name} must be a positive integer (ms)`;
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
function positiveMs(raw, what) {
|
|
239
|
+
if (!/^\d+$/.test(raw) || Number(raw) <= 0 || !Number.isSafeInteger(Number(raw))) {
|
|
240
|
+
throw new SitefileError('E_USAGE', `${what} expects a positive integer (ms), got '${raw}'`);
|
|
241
|
+
}
|
|
242
|
+
return Number(raw);
|
|
243
|
+
}
|
|
244
|
+
/** Parse `SITEFILE_FETCH_TIMEOUT`: `<ms>` (connect and read) or `connect=<ms>,read=<ms>` (either part optional). */
|
|
245
|
+
export function parseFetchTimeout(raw) {
|
|
246
|
+
const s = raw.trim();
|
|
247
|
+
if (s === '')
|
|
248
|
+
return {};
|
|
249
|
+
if (/^\d+$/.test(s)) {
|
|
250
|
+
const ms = positiveMs(s, 'SITEFILE_FETCH_TIMEOUT');
|
|
251
|
+
return { connectTimeoutMs: ms, readTimeoutMs: ms };
|
|
252
|
+
}
|
|
253
|
+
const out = {};
|
|
254
|
+
for (const part of s.split(',')) {
|
|
255
|
+
const m = /^\s*(connect|read)\s*=\s*(\S*)\s*$/.exec(part);
|
|
256
|
+
if (!m)
|
|
257
|
+
throw new SitefileError('E_USAGE', `SITEFILE_FETCH_TIMEOUT expects <ms> or connect=<ms>,read=<ms>, got '${raw}'`);
|
|
258
|
+
const ms = positiveMs(m[2], `SITEFILE_FETCH_TIMEOUT ${m[1]}`);
|
|
259
|
+
if (m[1] === 'connect')
|
|
260
|
+
out.connectTimeoutMs = ms;
|
|
261
|
+
else
|
|
262
|
+
out.readTimeoutMs = ms;
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Network settings from `SITEFILE_PROXY`, `SITEFILE_NO_PROXY`, `SITEFILE_ALLOW_HOSTS`,
|
|
268
|
+
* `SITEFILE_DENY_HOSTS` (comma- or space-separated) and `SITEFILE_FETCH_TIMEOUT`. Empty values are unset.
|
|
269
|
+
* `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` are never read.
|
|
270
|
+
*/
|
|
271
|
+
export function networkFromEnv(env = process.env) {
|
|
272
|
+
const proxy = (env.SITEFILE_PROXY ?? '').trim();
|
|
273
|
+
return {
|
|
274
|
+
...(proxy !== '' ? { proxy } : {}),
|
|
275
|
+
...(splitList(env.SITEFILE_NO_PROXY).length ? { noProxy: splitList(env.SITEFILE_NO_PROXY) } : {}),
|
|
276
|
+
...(splitList(env.SITEFILE_ALLOW_HOSTS).length ? { allowHosts: splitList(env.SITEFILE_ALLOW_HOSTS) } : {}),
|
|
277
|
+
...(splitList(env.SITEFILE_DENY_HOSTS).length ? { denyHosts: splitList(env.SITEFILE_DENY_HOSTS) } : {}),
|
|
278
|
+
...parseFetchTimeout(env.SITEFILE_FETCH_TIMEOUT ?? ''),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/** `over` on top of `base`: the proxy and timeouts are replaced, the host lists are joined (duplicates dropped). */
|
|
282
|
+
export function mergeNetwork(base, over) {
|
|
283
|
+
const join = (a, b) => {
|
|
284
|
+
const all = [...new Set([...(a ?? []), ...(b ?? [])])];
|
|
285
|
+
return all.length ? all : undefined;
|
|
286
|
+
};
|
|
287
|
+
const out = {
|
|
288
|
+
proxy: over.proxy ?? base.proxy,
|
|
289
|
+
noProxy: join(base.noProxy, over.noProxy),
|
|
290
|
+
allowHosts: join(base.allowHosts, over.allowHosts),
|
|
291
|
+
denyHosts: join(base.denyHosts, over.denyHosts),
|
|
292
|
+
connectTimeoutMs: over.connectTimeoutMs ?? base.connectTimeoutMs,
|
|
293
|
+
readTimeoutMs: over.readTimeoutMs ?? base.readTimeoutMs,
|
|
294
|
+
};
|
|
295
|
+
for (const k of Object.keys(out))
|
|
296
|
+
if (out[k] === undefined)
|
|
297
|
+
delete out[k];
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
/** True when any setting is present. */
|
|
301
|
+
export function hasNetworkSettings(n) {
|
|
302
|
+
return !!n && Object.values(n).some((v) => v !== undefined && !(Array.isArray(v) && v.length === 0));
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=network.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { NetworkSettings } from './network.js';
|
|
2
|
+
import type { Tree } from '../tree.js';
|
|
3
|
+
export interface Provenance {
|
|
4
|
+
/** The URI as written (after variable interpolation), or `./relative/path`. */
|
|
5
|
+
readonly uri: string;
|
|
6
|
+
/** Final URL after redirects, for remote resources. */
|
|
7
|
+
readonly resolvedUrl?: string;
|
|
8
|
+
/** `sha256:<hex>`: file bytes, tree digest, or the raw bytes a value was parsed from. */
|
|
9
|
+
readonly digest: string;
|
|
10
|
+
/** Git imports: the commit the content was read from (40 hex). */
|
|
11
|
+
readonly gitCommit?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface FileResource {
|
|
14
|
+
readonly kind: 'file';
|
|
15
|
+
readonly bytes: Uint8Array;
|
|
16
|
+
readonly mode: number;
|
|
17
|
+
/** Suggested basename when copied to a directory destination. */
|
|
18
|
+
readonly basename: string;
|
|
19
|
+
readonly provenance: Provenance;
|
|
20
|
+
}
|
|
21
|
+
export interface TreeResource {
|
|
22
|
+
readonly kind: 'tree';
|
|
23
|
+
readonly tree: Tree;
|
|
24
|
+
readonly provenance: Provenance;
|
|
25
|
+
}
|
|
26
|
+
export interface ValueResource {
|
|
27
|
+
readonly kind: 'value';
|
|
28
|
+
/** `markdown`: one document object, or a list of them for a directory. */
|
|
29
|
+
/** `html`: the text of an `IMPORT … AS html` fragment. */
|
|
30
|
+
readonly format: 'text' | 'json' | 'markdown' | 'yaml' | 'csv' | 'wbn' | 'html';
|
|
31
|
+
readonly value: unknown;
|
|
32
|
+
readonly provenance: Provenance;
|
|
33
|
+
}
|
|
34
|
+
export type Resource = FileResource | TreeResource | ValueResource;
|
|
35
|
+
/** Network policy for remote resources. Defaults are secure. */
|
|
36
|
+
export interface FetchPolicy {
|
|
37
|
+
/** Permit plain `http:` (default false). */
|
|
38
|
+
readonly allowHttp: boolean;
|
|
39
|
+
/** Permit loopback, private, link-local and other non-public addresses (default false). */
|
|
40
|
+
readonly allowPrivateNetwork: boolean;
|
|
41
|
+
/** Maximum response body size in bytes (default 50 MiB). */
|
|
42
|
+
readonly maxBytes: number;
|
|
43
|
+
/** Total time budget per resource including redirects, ms (default 30 000). */
|
|
44
|
+
readonly timeoutMs: number;
|
|
45
|
+
/** Maximum redirects followed (default 5). */
|
|
46
|
+
readonly maxRedirects: number;
|
|
47
|
+
/**
|
|
48
|
+
* Extra PEM-encoded CA certificate(s) to trust for https, in addition to
|
|
49
|
+
* Node's built-in roots (never instead of them). For tests and enterprise
|
|
50
|
+
* private CAs; certificate and hostname verification stay on.
|
|
51
|
+
*/
|
|
52
|
+
readonly ca?: string | Buffer | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Proxy, host rules and connect/read timeouts (see {@link NetworkSettings}). Unset: direct
|
|
55
|
+
* connections, every public host, only the total `timeoutMs`.
|
|
56
|
+
*/
|
|
57
|
+
readonly network?: NetworkSettings | undefined;
|
|
58
|
+
}
|
|
59
|
+
export declare const DEFAULT_FETCH_POLICY: FetchPolicy;
|
|
60
|
+
/** Limits applied when an archive is extracted with `IMPORT … AS tree`. Defaults are secure. */
|
|
61
|
+
export interface ArchiveLimits {
|
|
62
|
+
/** Maximum number of entries (default 10 000). */
|
|
63
|
+
readonly maxEntries: number;
|
|
64
|
+
/** Maximum total uncompressed bytes (default 500 MiB). */
|
|
65
|
+
readonly maxTotalBytes: number;
|
|
66
|
+
/** Maximum uncompressed:compressed ratio of the gzip/zip data (default 100). */
|
|
67
|
+
readonly maxRatio: number;
|
|
68
|
+
}
|
|
69
|
+
export declare const DEFAULT_ARCHIVE_LIMITS: ArchiveLimits;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const DEFAULT_FETCH_POLICY = {
|
|
2
|
+
allowHttp: false,
|
|
3
|
+
allowPrivateNetwork: false,
|
|
4
|
+
maxBytes: 50 * 1024 * 1024,
|
|
5
|
+
timeoutMs: 30_000,
|
|
6
|
+
maxRedirects: 5,
|
|
7
|
+
};
|
|
8
|
+
export const DEFAULT_ARCHIVE_LIMITS = {
|
|
9
|
+
maxEntries: 10_000,
|
|
10
|
+
maxTotalBytes: 500 * 1024 * 1024,
|
|
11
|
+
maxRatio: 100,
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type SourceSpan } from '../errors.js';
|
|
2
|
+
import type { Store } from '../store.js';
|
|
3
|
+
import type { LocalContext } from './local.js';
|
|
4
|
+
import type { GitSelection } from './git.js';
|
|
5
|
+
import type { ArchiveLimits } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* The vendor directory written by `sitefile vendor` (issue #96): every remote input's bytes under
|
|
8
|
+
* content-addressed names, plus a manifest that maps URLs and image tags to them.
|
|
9
|
+
*
|
|
10
|
+
* <dir>/manifest.json {"version":1,"resources":{url:{integrity,size,resolved}},"images":{ref:{digest,platform,manifest}}}
|
|
11
|
+
* <dir>/blobs/sha256/<hex> IMPORT bodies and BASE image blobs (index, manifest, config, layers)
|
|
12
|
+
*
|
|
13
|
+
* Builds prefer these copies over the network and verify each one against its digest (and, as for any
|
|
14
|
+
* fetch, against INTEGRITY and Sitefile.lock). Nothing secret is written: URLs with credentials are
|
|
15
|
+
* rejected before any fetch, and registry tokens or passwords never leave memory.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_VENDOR_DIR = "vendor/sitefile";
|
|
18
|
+
export declare const VENDOR_MANIFEST = "manifest.json";
|
|
19
|
+
export interface VendoredResource {
|
|
20
|
+
/** `sha256:<hex>` of the body: also its blob name. */
|
|
21
|
+
readonly integrity: string;
|
|
22
|
+
readonly size: number;
|
|
23
|
+
/** Final URL after redirects, as the lockfile records it. */
|
|
24
|
+
readonly resolved: string;
|
|
25
|
+
}
|
|
26
|
+
export interface VendoredImage {
|
|
27
|
+
/** What the reference resolved to (an index for multi-platform images, else the manifest). */
|
|
28
|
+
readonly digest: string;
|
|
29
|
+
/** The platform that was vendored (`os/arch`). */
|
|
30
|
+
readonly platform: string;
|
|
31
|
+
/** That platform's manifest digest (the image ID). */
|
|
32
|
+
readonly manifest: string;
|
|
33
|
+
}
|
|
34
|
+
/** A Git IMPORT: the commit it was read from and its content (a file's bytes, or a tree as a tar). */
|
|
35
|
+
export interface VendoredGit {
|
|
36
|
+
readonly commit: string;
|
|
37
|
+
readonly kind: 'tree' | 'file';
|
|
38
|
+
/** `sha256:<hex>` of the stored bytes: also their blob name. */
|
|
39
|
+
readonly integrity: string;
|
|
40
|
+
readonly size: number;
|
|
41
|
+
/** Files only: `644` or `755`. */
|
|
42
|
+
readonly mode?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface VendorManifest {
|
|
45
|
+
readonly version: 1;
|
|
46
|
+
readonly resources: Readonly<Record<string, VendoredResource>>;
|
|
47
|
+
readonly images: Readonly<Record<string, VendoredImage>>;
|
|
48
|
+
/** Written only when non-empty. */
|
|
49
|
+
readonly git?: Readonly<Record<string, VendoredGit>>;
|
|
50
|
+
}
|
|
51
|
+
/** Validate a vendor directory: a relative path inside the build context. Returns it normalized (POSIX, no trailing slash). */
|
|
52
|
+
export declare function vendorDirRel(input: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* The vendor directory a build uses: `explicit` (which must have a manifest), else the default one if
|
|
55
|
+
* its manifest exists, else none.
|
|
56
|
+
*/
|
|
57
|
+
export declare function findVendorDir(contextReal: string, explicit: string | undefined): Promise<string | undefined>;
|
|
58
|
+
/** Parse and check a vendor manifest. Errors are E_VALIDATION naming the file. */
|
|
59
|
+
export declare function parseVendorManifest(bytes: Uint8Array, shown: string): VendorManifest;
|
|
60
|
+
/** The manifest as `sitefile vendor` writes it: sorted keys, two-space indent, trailing newline. */
|
|
61
|
+
export declare function serializeVendorManifest(m: VendorManifest): string;
|
|
62
|
+
/** A vendor directory opened through the build's sandboxed context reader. */
|
|
63
|
+
export declare class Vendor {
|
|
64
|
+
/** Context-relative directory. */
|
|
65
|
+
readonly dir: string;
|
|
66
|
+
private readonly local;
|
|
67
|
+
readonly manifest: VendorManifest;
|
|
68
|
+
private constructor();
|
|
69
|
+
static open(local: LocalContext, dir: string): Promise<Vendor>;
|
|
70
|
+
resource(url: string): VendoredResource | undefined;
|
|
71
|
+
image(ref: string): VendoredImage | undefined;
|
|
72
|
+
git(url: string): VendoredGit | undefined;
|
|
73
|
+
/** The vendored content of a Git IMPORT, verified against its digest. */
|
|
74
|
+
readGit(url: string, e: VendoredGit, basename: string, limits: ArchiveLimits, span?: SourceSpan): Promise<GitSelection>;
|
|
75
|
+
/** The vendored body of `url`, verified against its digest. */
|
|
76
|
+
readResource(url: string, e: VendoredResource, span?: SourceSpan): Promise<{
|
|
77
|
+
bytes: Uint8Array;
|
|
78
|
+
finalUrl: string;
|
|
79
|
+
}>;
|
|
80
|
+
private readBlob;
|
|
81
|
+
private hasBlob;
|
|
82
|
+
/**
|
|
83
|
+
* Copy the vendored blobs of the image `rootDigest` (for `platform`) into the store, each verified,
|
|
84
|
+
* so pulling it finds everything locally. Stops quietly at the first blob that is not vendored: the
|
|
85
|
+
* pull then fetches the rest from the registry.
|
|
86
|
+
*/
|
|
87
|
+
seedStore(store: Store, rootDigest: string, platform: string): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Replace `<contextReal>/<dir>` with a vendor directory holding `manifestText` and `blobs`: written to a
|
|
91
|
+
* temporary sibling, then swapped in. Refuses a directory that resolves outside the context, a symlink,
|
|
92
|
+
* and an existing directory that is not a vendor directory (no manifest.json).
|
|
93
|
+
*/
|
|
94
|
+
export declare function writeVendorDir(contextReal: string, dir: string, manifestText: string, blobs: ReadonlyMap<string, Uint8Array>): Promise<string>;
|