@johnhenry/sitefile 0.0.0-stage → 0.0.0
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 +48 -0
- package/LICENSE +21 -0
- package/README.md +475 -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 +118 -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 +109 -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/outdated.d.ts +5 -0
- package/lib/src/cli/outdated.js +94 -0
- package/lib/src/cli/registry.d.ts +2 -0
- package/lib/src/cli/registry.js +122 -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 +451 -0
- package/lib/src/cli/vendor.d.ts +2 -0
- package/lib/src/cli/vendor.js +92 -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 +486 -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 +34 -0
- package/lib/src/errors.js +20 -0
- package/lib/src/executor.d.ts +386 -0
- package/lib/src/executor.js +2193 -0
- package/lib/src/explain.d.ts +64 -0
- package/lib/src/explain.js +445 -0
- package/lib/src/export-formats.d.ts +16 -0
- package/lib/src/export-formats.js +52 -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 +12 -0
- package/lib/src/index.js +12 -0
- package/lib/src/language/ast.d.ts +353 -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 +70 -0
- package/lib/src/plugins/cache.js +226 -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 +109 -0
- package/lib/src/registry.js +505 -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 +40 -0
- package/lib/src/resources/https.js +256 -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/types.d.ts +63 -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 +122 -0
- package/lib/src/server.d.ts +45 -0
- package/lib/src/server.js +246 -0
- package/lib/src/servers.d.ts +71 -0
- package/lib/src/servers.js +139 -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 +5 -0
- package/lib/src/wbn.js +147 -0
- package/lib/src/yaml.d.ts +34 -0
- package/lib/src/yaml.js +562 -0
- package/package.json +58 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type LookupFunction } from 'node:net';
|
|
2
|
+
import { type SourceSpan } from '../errors.js';
|
|
3
|
+
import type { FetchPolicy } from './types.js';
|
|
4
|
+
export interface FetchResult {
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
/** URL of the final hop after redirects. */
|
|
7
|
+
readonly finalUrl: string;
|
|
8
|
+
/** Media type essence from `content-type` (lowercase, parameters removed). */
|
|
9
|
+
readonly mediaType?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* What to send. `headers` go to every hop; `originHeaders` (credentials: IMPORT … SECRET) only to hops
|
|
13
|
+
* on the first URL's origin (scheme, host and port), so a redirect elsewhere never receives them.
|
|
14
|
+
* A request with a body (POST) never follows redirects (E_REDIRECT).
|
|
15
|
+
*/
|
|
16
|
+
export interface FetchRequest {
|
|
17
|
+
readonly method?: 'GET' | 'POST';
|
|
18
|
+
readonly body?: Uint8Array;
|
|
19
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
20
|
+
readonly originHeaders?: Readonly<Record<string, string>>;
|
|
21
|
+
}
|
|
22
|
+
/** A URL rendered without any userinfo, safe for error messages. */
|
|
23
|
+
export declare function redactUrl(url: URL | string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Validate one hop before connecting: scheme, credentials and IP-literal
|
|
26
|
+
* hosts. Hostnames are checked at connect time by the guarded lookup.
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateHop(url: URL, policy: FetchPolicy, span?: SourceSpan): void;
|
|
29
|
+
/**
|
|
30
|
+
* A `lookup` for http(s).request that resolves every address and fails with
|
|
31
|
+
* E_SSRF_BLOCKED if any is non-public. Runs at connect time, so DNS rebinding
|
|
32
|
+
* between validation and connection cannot bypass it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createGuardedLookup(policy: FetchPolicy, span?: SourceSpan): LookupFunction;
|
|
35
|
+
/**
|
|
36
|
+
* Fetch a remote resource under the given network policy. Redirects are
|
|
37
|
+
* followed manually and every hop is re-validated; one deadline covers the
|
|
38
|
+
* whole fetch.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fetchRemote(url: string, policy: FetchPolicy, span?: SourceSpan, request?: FetchRequest): Promise<FetchResult>;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { lookup as dnsLookup } from 'node:dns';
|
|
2
|
+
import { request as httpRequest } from 'node:http';
|
|
3
|
+
import { request as httpsRequest } from 'node:https';
|
|
4
|
+
import * as tls from 'node:tls';
|
|
5
|
+
import { isIP } from 'node:net';
|
|
6
|
+
import { SitefileError, isSitefileError } from '../errors.js';
|
|
7
|
+
import { isPublicAddress } from './netpolicy.js';
|
|
8
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
9
|
+
const USER_AGENT = 'sitefile/0.1';
|
|
10
|
+
/** A URL rendered without any userinfo, safe for error messages. */
|
|
11
|
+
export function redactUrl(url) {
|
|
12
|
+
try {
|
|
13
|
+
const u = new URL(String(url));
|
|
14
|
+
u.username = '';
|
|
15
|
+
u.password = '';
|
|
16
|
+
return u.href;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '<invalid url>';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function stripBrackets(host) {
|
|
23
|
+
return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate one hop before connecting: scheme, credentials and IP-literal
|
|
27
|
+
* hosts. Hostnames are checked at connect time by the guarded lookup.
|
|
28
|
+
*/
|
|
29
|
+
export function validateHop(url, policy, span) {
|
|
30
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && policy.allowHttp)) {
|
|
31
|
+
const hint = url.protocol === 'http:' ? ' (plain http requires --allow-http)' : '';
|
|
32
|
+
throw new SitefileError('E_PROTOCOL', `unsupported protocol '${url.protocol}' for ${redactUrl(url)}${hint}`, span);
|
|
33
|
+
}
|
|
34
|
+
if (url.username !== '' || url.password !== '') {
|
|
35
|
+
throw new SitefileError('E_PROTOCOL', `URL ${redactUrl(url)} contains credentials; credentials in URLs are not allowed`, span);
|
|
36
|
+
}
|
|
37
|
+
const host = stripBrackets(url.hostname);
|
|
38
|
+
if (host === '')
|
|
39
|
+
throw new SitefileError('E_PROTOCOL', `URL ${redactUrl(url)} has no host`, span);
|
|
40
|
+
if (isIP(host) !== 0 && !policy.allowPrivateNetwork && !isPublicAddress(host)) {
|
|
41
|
+
throw new SitefileError('E_SSRF_BLOCKED', `refusing to connect to non-public address ${host} (${redactUrl(url)}); use --allow-private-network to permit it`, span);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A `lookup` for http(s).request that resolves every address and fails with
|
|
46
|
+
* E_SSRF_BLOCKED if any is non-public. Runs at connect time, so DNS rebinding
|
|
47
|
+
* between validation and connection cannot bypass it.
|
|
48
|
+
*/
|
|
49
|
+
export function createGuardedLookup(policy, span) {
|
|
50
|
+
return (hostname, options, callback) => {
|
|
51
|
+
dnsLookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
52
|
+
if (err) {
|
|
53
|
+
callback(err);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const list = addresses;
|
|
57
|
+
if (list.length === 0) {
|
|
58
|
+
callback(new SitefileError('E_FETCH', `no addresses found for ${hostname}`, span));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!policy.allowPrivateNetwork) {
|
|
62
|
+
const bad = list.find((a) => !isPublicAddress(a.address));
|
|
63
|
+
if (bad) {
|
|
64
|
+
callback(new SitefileError('E_SSRF_BLOCKED', `refusing to connect to ${hostname}: it resolves to non-public address ${bad.address}; use --allow-private-network to permit it`, span));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (options.all) {
|
|
69
|
+
callback(null, list);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const first = list[0];
|
|
73
|
+
callback(null, first.address, first.family);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Find a SitefileError carried by a socket/request error, if any. */
|
|
79
|
+
function findSitefileError(e, depth = 0) {
|
|
80
|
+
if (depth > 4 || e === null || typeof e !== 'object')
|
|
81
|
+
return undefined;
|
|
82
|
+
if (isSitefileError(e))
|
|
83
|
+
return e;
|
|
84
|
+
const anyE = e;
|
|
85
|
+
if (Array.isArray(anyE.errors)) {
|
|
86
|
+
for (const inner of anyE.errors) {
|
|
87
|
+
const found = findSitefileError(inner, depth + 1);
|
|
88
|
+
if (found)
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return findSitefileError(anyE.cause, depth + 1);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fetch a remote resource under the given network policy. Redirects are
|
|
96
|
+
* followed manually and every hop is re-validated; one deadline covers the
|
|
97
|
+
* whole fetch.
|
|
98
|
+
*/
|
|
99
|
+
export async function fetchRemote(url, policy, span, request = {}) {
|
|
100
|
+
let current;
|
|
101
|
+
try {
|
|
102
|
+
current = new URL(url);
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
throw new SitefileError('E_PROTOCOL', `invalid URL '${redactUrl(url)}'`, span, { cause: e });
|
|
106
|
+
}
|
|
107
|
+
const origin = current.origin;
|
|
108
|
+
const method = request.method ?? 'GET';
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const state = {};
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
controller.abort();
|
|
113
|
+
state.req?.destroy();
|
|
114
|
+
state.res?.destroy();
|
|
115
|
+
}, policy.timeoutMs);
|
|
116
|
+
timer.unref?.();
|
|
117
|
+
const timeoutError = () => new SitefileError('E_FETCH_TIMEOUT', `fetching ${redactUrl(url)} timed out after ${policy.timeoutMs} ms`, span);
|
|
118
|
+
try {
|
|
119
|
+
let redirects = 0;
|
|
120
|
+
for (;;) {
|
|
121
|
+
validateHop(current, policy, span);
|
|
122
|
+
if (controller.signal.aborted)
|
|
123
|
+
throw timeoutError();
|
|
124
|
+
// Credentials only ever go to the origin they were given for (not after a redirect elsewhere).
|
|
125
|
+
const headers = current.origin === origin ? { ...request.headers, ...request.originHeaders } : { ...request.headers };
|
|
126
|
+
const res = await openHop(current, policy, controller.signal, state, span, timeoutError, method, headers, request.body);
|
|
127
|
+
const status = res.statusCode ?? 0;
|
|
128
|
+
if (REDIRECT_STATUSES.has(status) && method !== 'GET') {
|
|
129
|
+
res.destroy();
|
|
130
|
+
throw new SitefileError('E_REDIRECT', `${method} ${redactUrl(current)} was redirected (HTTP ${status}); redirects are only followed for GET`, span);
|
|
131
|
+
}
|
|
132
|
+
if (REDIRECT_STATUSES.has(status)) {
|
|
133
|
+
const location = res.headers.location;
|
|
134
|
+
res.destroy();
|
|
135
|
+
state.req?.destroy();
|
|
136
|
+
if (!location) {
|
|
137
|
+
throw new SitefileError('E_REDIRECT', `redirect ${status} from ${redactUrl(current)} has no Location header`, span);
|
|
138
|
+
}
|
|
139
|
+
redirects++;
|
|
140
|
+
if (redirects > policy.maxRedirects) {
|
|
141
|
+
throw new SitefileError('E_REDIRECT', `too many redirects fetching ${redactUrl(url)} (limit ${policy.maxRedirects})`, span);
|
|
142
|
+
}
|
|
143
|
+
let next;
|
|
144
|
+
try {
|
|
145
|
+
next = new URL(location, current);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
throw new SitefileError('E_REDIRECT', `invalid redirect Location from ${redactUrl(current)}`, span, { cause: e });
|
|
149
|
+
}
|
|
150
|
+
current = next;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (status !== 200) {
|
|
154
|
+
res.destroy();
|
|
155
|
+
throw new SitefileError('E_FETCH_STATUS', `${method} ${redactUrl(current)} returned HTTP ${status}`, span);
|
|
156
|
+
}
|
|
157
|
+
const encoding = String(res.headers['content-encoding'] ?? '').trim().toLowerCase();
|
|
158
|
+
if (encoding !== '' && encoding !== 'identity') {
|
|
159
|
+
res.destroy();
|
|
160
|
+
throw new SitefileError('E_FETCH', `${redactUrl(current)} responded with content-encoding '${encoding}'; only identity is accepted`, span);
|
|
161
|
+
}
|
|
162
|
+
const lengthHeader = res.headers['content-length'];
|
|
163
|
+
if (lengthHeader !== undefined) {
|
|
164
|
+
const declared = Number(lengthHeader);
|
|
165
|
+
if (Number.isFinite(declared) && declared > policy.maxBytes) {
|
|
166
|
+
res.destroy();
|
|
167
|
+
throw new SitefileError('E_TOO_LARGE', `${redactUrl(current)} is ${declared} bytes, exceeding the limit of ${policy.maxBytes} bytes`, span);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const chunks = [];
|
|
171
|
+
let total = 0;
|
|
172
|
+
try {
|
|
173
|
+
for await (const chunk of res) {
|
|
174
|
+
total += chunk.length;
|
|
175
|
+
if (total > policy.maxBytes) {
|
|
176
|
+
res.destroy();
|
|
177
|
+
throw new SitefileError('E_TOO_LARGE', `${redactUrl(current)} exceeded the limit of ${policy.maxBytes} bytes`, span);
|
|
178
|
+
}
|
|
179
|
+
chunks.push(chunk);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
if (isSitefileError(e))
|
|
184
|
+
throw e;
|
|
185
|
+
if (controller.signal.aborted)
|
|
186
|
+
throw timeoutError();
|
|
187
|
+
throw new SitefileError('E_FETCH', `error reading ${redactUrl(current)}: ${e.message}`, span, {
|
|
188
|
+
cause: e,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (controller.signal.aborted)
|
|
192
|
+
throw timeoutError();
|
|
193
|
+
const buf = Buffer.concat(chunks, total);
|
|
194
|
+
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
195
|
+
const ct = res.headers['content-type'];
|
|
196
|
+
const mediaType = ct ? ct.split(';')[0].trim().toLowerCase() || undefined : undefined;
|
|
197
|
+
return mediaType === undefined ? { bytes, finalUrl: current.href } : { bytes, finalUrl: current.href, mediaType };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
state.res?.destroy();
|
|
203
|
+
state.req?.destroy();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function openHop(url, policy, signal, state, span, timeoutError, method, extra, body) {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const options = {
|
|
209
|
+
method,
|
|
210
|
+
headers: { 'accept-encoding': 'identity', 'user-agent': USER_AGENT, accept: '*/*', ...extra, ...(body ? { 'content-length': String(body.byteLength) } : {}) },
|
|
211
|
+
lookup: createGuardedLookup(policy, span),
|
|
212
|
+
agent: false,
|
|
213
|
+
signal,
|
|
214
|
+
};
|
|
215
|
+
if (url.protocol === 'https:' && policy.ca !== undefined) {
|
|
216
|
+
// Additive: setting `ca` replaces Node's whole default store, so start from
|
|
217
|
+
// that store (bundled roots plus NODE_EXTRA_CA_CERTS / --use-system-ca),
|
|
218
|
+
// not just the bundled roots, and add the extra CA(s).
|
|
219
|
+
options.ca = [...defaultCaCertificates(), policy.ca];
|
|
220
|
+
}
|
|
221
|
+
const requestFn = url.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
222
|
+
let req;
|
|
223
|
+
try {
|
|
224
|
+
req = requestFn(url, options);
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
reject(new SitefileError('E_FETCH', `cannot request ${redactUrl(url)}: ${e.message}`, span, { cause: e }));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
state.req = req;
|
|
231
|
+
state.res = undefined;
|
|
232
|
+
req.on('response', (res) => {
|
|
233
|
+
state.res = res;
|
|
234
|
+
resolve(res);
|
|
235
|
+
});
|
|
236
|
+
req.on('error', (e) => {
|
|
237
|
+
if (signal.aborted) {
|
|
238
|
+
reject(timeoutError());
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const inner = findSitefileError(e);
|
|
242
|
+
if (inner) {
|
|
243
|
+
reject(inner);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
reject(new SitefileError('E_FETCH', `fetching ${redactUrl(url)} failed: ${e.message}`, span, { cause: e }));
|
|
247
|
+
});
|
|
248
|
+
req.end(body);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/** Node's default CA store; `tls.getCACertificates` exists from Node 22.15/23.5. */
|
|
252
|
+
function defaultCaCertificates() {
|
|
253
|
+
const get = tls.getCACertificates;
|
|
254
|
+
return get ? get('default') : tls.rootCertificates;
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=https.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.sitefileignore`: a gitignore-style subset, implemented without dependencies.
|
|
3
|
+
*
|
|
4
|
+
* - Blank lines and lines starting with `#` are skipped (`\#` escapes a literal `#`).
|
|
5
|
+
* - `!` negates a rule (`\!` escapes a literal `!`). The last matching rule wins.
|
|
6
|
+
* - A trailing `/` makes the rule match directories only.
|
|
7
|
+
* - A leading `/` or a `/` in the middle anchors the rule to the context root;
|
|
8
|
+
* otherwise it matches at any depth.
|
|
9
|
+
* - `*` and `?` match within one path segment; `**` as a whole segment matches
|
|
10
|
+
* any number of segments.
|
|
11
|
+
* - Unescaped trailing spaces are ignored. Character classes are not supported.
|
|
12
|
+
*
|
|
13
|
+
* As in gitignore, a file cannot be re-included once a parent directory is
|
|
14
|
+
* excluded; callers get this by not descending into excluded directories.
|
|
15
|
+
*/
|
|
16
|
+
export declare class IgnoreRules {
|
|
17
|
+
private readonly rules;
|
|
18
|
+
private constructor();
|
|
19
|
+
static readonly empty: IgnoreRules;
|
|
20
|
+
static parse(text: string): IgnoreRules;
|
|
21
|
+
/** Rules from `COPY --exclude` patterns, one `.sitefileignore` line each (see `isIgnorePattern`). */
|
|
22
|
+
static fromPatterns(patterns: readonly string[]): IgnoreRules;
|
|
23
|
+
/**
|
|
24
|
+
* Whether `rel` (a POSIX path relative to the rules' root) is excluded, counting excluded parent
|
|
25
|
+
* directories: what a directory walk that does not descend into excluded directories would skip.
|
|
26
|
+
*/
|
|
27
|
+
excludes(rel: string): boolean;
|
|
28
|
+
get size(): number;
|
|
29
|
+
/** Whether the context-relative POSIX path (no leading `./`) is excluded by the last matching rule. */
|
|
30
|
+
ignores(rel: string, isDirectory: boolean): boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Whether `text` is a `.sitefileignore` rule (not blank, a `#` comment, or only `!` and `/`). */
|
|
33
|
+
export declare function isIgnorePattern(text: string): boolean;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.sitefileignore`: a gitignore-style subset, implemented without dependencies.
|
|
3
|
+
*
|
|
4
|
+
* - Blank lines and lines starting with `#` are skipped (`\#` escapes a literal `#`).
|
|
5
|
+
* - `!` negates a rule (`\!` escapes a literal `!`). The last matching rule wins.
|
|
6
|
+
* - A trailing `/` makes the rule match directories only.
|
|
7
|
+
* - A leading `/` or a `/` in the middle anchors the rule to the context root;
|
|
8
|
+
* otherwise it matches at any depth.
|
|
9
|
+
* - `*` and `?` match within one path segment; `**` as a whole segment matches
|
|
10
|
+
* any number of segments.
|
|
11
|
+
* - Unescaped trailing spaces are ignored. Character classes are not supported.
|
|
12
|
+
*
|
|
13
|
+
* As in gitignore, a file cannot be re-included once a parent directory is
|
|
14
|
+
* excluded; callers get this by not descending into excluded directories.
|
|
15
|
+
*/
|
|
16
|
+
export class IgnoreRules {
|
|
17
|
+
rules;
|
|
18
|
+
constructor(rules) {
|
|
19
|
+
this.rules = rules;
|
|
20
|
+
}
|
|
21
|
+
static empty = new IgnoreRules([]);
|
|
22
|
+
static parse(text) {
|
|
23
|
+
const rules = [];
|
|
24
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
25
|
+
const rule = parseLine(raw);
|
|
26
|
+
if (rule)
|
|
27
|
+
rules.push(rule);
|
|
28
|
+
}
|
|
29
|
+
return new IgnoreRules(rules);
|
|
30
|
+
}
|
|
31
|
+
/** Rules from `COPY --exclude` patterns, one `.sitefileignore` line each (see `isIgnorePattern`). */
|
|
32
|
+
static fromPatterns(patterns) {
|
|
33
|
+
return new IgnoreRules(patterns.map(parseLine).filter((r) => r !== undefined));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether `rel` (a POSIX path relative to the rules' root) is excluded, counting excluded parent
|
|
37
|
+
* directories: what a directory walk that does not descend into excluded directories would skip.
|
|
38
|
+
*/
|
|
39
|
+
excludes(rel) {
|
|
40
|
+
const segs = rel.split('/');
|
|
41
|
+
for (let i = 1; i < segs.length; i++)
|
|
42
|
+
if (this.ignores(segs.slice(0, i).join('/'), true))
|
|
43
|
+
return true;
|
|
44
|
+
return this.ignores(rel, false);
|
|
45
|
+
}
|
|
46
|
+
get size() {
|
|
47
|
+
return this.rules.length;
|
|
48
|
+
}
|
|
49
|
+
/** Whether the context-relative POSIX path (no leading `./`) is excluded by the last matching rule. */
|
|
50
|
+
ignores(rel, isDirectory) {
|
|
51
|
+
let ignored = false;
|
|
52
|
+
for (const r of this.rules) {
|
|
53
|
+
if (r.dirOnly && !isDirectory)
|
|
54
|
+
continue;
|
|
55
|
+
if (r.re.test(rel))
|
|
56
|
+
ignored = !r.negate;
|
|
57
|
+
}
|
|
58
|
+
return ignored;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Whether `text` is a `.sitefileignore` rule (not blank, a `#` comment, or only `!` and `/`). */
|
|
62
|
+
export function isIgnorePattern(text) {
|
|
63
|
+
return parseLine(text) !== undefined;
|
|
64
|
+
}
|
|
65
|
+
function parseLine(raw) {
|
|
66
|
+
let line = raw.replace(/(?<!\\)\s+$/, '');
|
|
67
|
+
if (line === '' || line.startsWith('#'))
|
|
68
|
+
return undefined;
|
|
69
|
+
let negate = false;
|
|
70
|
+
if (line.startsWith('!')) {
|
|
71
|
+
negate = true;
|
|
72
|
+
line = line.slice(1);
|
|
73
|
+
}
|
|
74
|
+
const dirOnly = line.endsWith('/');
|
|
75
|
+
if (dirOnly)
|
|
76
|
+
line = line.replace(/\/+$/, '');
|
|
77
|
+
const anchored = line.includes('/');
|
|
78
|
+
line = line.replace(/^\/+/, '');
|
|
79
|
+
if (line === '')
|
|
80
|
+
return undefined;
|
|
81
|
+
const segs = line.split('/');
|
|
82
|
+
let src = '';
|
|
83
|
+
segs.forEach((seg, i) => {
|
|
84
|
+
const last = i === segs.length - 1;
|
|
85
|
+
if (seg === '**') {
|
|
86
|
+
// A trailing `**` needs at least one more segment; elsewhere it may match none.
|
|
87
|
+
src += last ? '.+' : '(?:[^/]+/)*';
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
src += globSegment(seg) + (last ? '' : '/');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return { negate, dirOnly, re: new RegExp(`^${anchored ? '' : '(?:.*/)?'}${src}$`) };
|
|
94
|
+
}
|
|
95
|
+
function globSegment(seg) {
|
|
96
|
+
let out = '';
|
|
97
|
+
for (let i = 0; i < seg.length; i++) {
|
|
98
|
+
const c = seg[i];
|
|
99
|
+
if (c === '\\' && i + 1 < seg.length) {
|
|
100
|
+
out += escapeRe(seg[++i]);
|
|
101
|
+
}
|
|
102
|
+
else if (c === '*') {
|
|
103
|
+
while (seg[i + 1] === '*')
|
|
104
|
+
i++; // `**` inside a segment behaves like `*`
|
|
105
|
+
out += '[^/]*';
|
|
106
|
+
}
|
|
107
|
+
else if (c === '?') {
|
|
108
|
+
out += '[^/]';
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
out += escapeRe(c);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function escapeRe(c) {
|
|
117
|
+
return c.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=ignore.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type SourceSpan } from '../errors.js';
|
|
2
|
+
import { IgnoreRules } from './ignore.js';
|
|
3
|
+
import type { FileResource, TreeResource } from './types.js';
|
|
4
|
+
export interface LocalContextOptions {
|
|
5
|
+
/** Context-relative POSIX paths skipped while walking directories. Default `['.git']`. */
|
|
6
|
+
readonly ignore?: readonly string[];
|
|
7
|
+
/** Read `.sitefileignore` from the context root (default true). Applies to directory walks only. */
|
|
8
|
+
readonly sitefileignore?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Permit regular files with more than one hard link (default false). realpath cannot
|
|
11
|
+
* tell whether a hard link points outside the context, so by default such files are
|
|
12
|
+
* refused with `E_HARDLINK`. Allowing them means you assert they are safe.
|
|
13
|
+
*/
|
|
14
|
+
readonly allowHardlinks?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare const SITEFILEIGNORE = ".sitefileignore";
|
|
17
|
+
/**
|
|
18
|
+
* The local build context. Every read is confined to `root`: real paths are
|
|
19
|
+
* resolved and must stay inside it.
|
|
20
|
+
*/
|
|
21
|
+
export declare class LocalContext {
|
|
22
|
+
readonly root: string;
|
|
23
|
+
private readonly rules;
|
|
24
|
+
private readonly allowHardlinks;
|
|
25
|
+
private readonly ignore;
|
|
26
|
+
private constructor();
|
|
27
|
+
static open(dir: string, options?: LocalContextOptions): Promise<LocalContext>;
|
|
28
|
+
private inside;
|
|
29
|
+
private isIgnored;
|
|
30
|
+
/** Hard-linked files may alias files outside the context, which realpath cannot detect. */
|
|
31
|
+
private checkHardlink;
|
|
32
|
+
/** Context-relative POSIX path of a real path inside root. */
|
|
33
|
+
private relOfReal;
|
|
34
|
+
/** Resolve a context-relative path to its real path and stats, enforcing the sandbox. */
|
|
35
|
+
/** A context file's real path and bytes, with the usual sandbox, symlink and hard-link rules. */
|
|
36
|
+
realFile(rel: string, span?: SourceSpan): Promise<{
|
|
37
|
+
real: string;
|
|
38
|
+
bytes: Uint8Array;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* The host path of the context-relative (NFC) path `rel`. A non-ASCII
|
|
42
|
+
* segment is matched by NFC form against its directory listing, so a file
|
|
43
|
+
* is found whichever normalization its on-disk name uses (macOS stores what
|
|
44
|
+
* it was given, often NFD; Linux compares bytes). Two names in one
|
|
45
|
+
* directory with the same NFC form are E_PATH_COLLISION. A segment with no
|
|
46
|
+
* match is joined as is, so the caller sees the usual ENOENT.
|
|
47
|
+
*/
|
|
48
|
+
private hostPath;
|
|
49
|
+
private resolve;
|
|
50
|
+
private static kindOf;
|
|
51
|
+
stat(rel: string, span?: SourceSpan): Promise<'file' | 'directory'>;
|
|
52
|
+
/** The real path of a context-relative path, with the sandbox checks of every read (an imported Sitefile's context). */
|
|
53
|
+
realPath(rel: string, span?: SourceSpan): Promise<string>;
|
|
54
|
+
readFile(rel: string, span?: SourceSpan): Promise<FileResource>;
|
|
55
|
+
/** `exclude` (COPY --exclude) skips paths relative to `rel`, like `.sitefileignore` rules (excluded directories are not read). */
|
|
56
|
+
readTree(rel: string, origin: string, span?: SourceSpan, exclude?: IgnoreRules): Promise<TreeResource>;
|
|
57
|
+
/**
|
|
58
|
+
* @param realDir real path of the directory being walked
|
|
59
|
+
* @param ctxRel logical context-relative path of that directory
|
|
60
|
+
* @param treeRel path of that directory inside the resulting tree ('' = root)
|
|
61
|
+
* @param ancestors real paths of directories on the current walk path (cycle detection)
|
|
62
|
+
*/
|
|
63
|
+
private walk;
|
|
64
|
+
}
|