@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,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
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join, sep, relative, posix } from 'node:path';
|
|
3
|
+
import { SitefileError } from '../errors.js';
|
|
4
|
+
import { sha256 } from '../digest.js';
|
|
5
|
+
import { Tree, makeFile } from '../tree.js';
|
|
6
|
+
import { nfc } from '../paths.js';
|
|
7
|
+
import { IgnoreRules } from './ignore.js';
|
|
8
|
+
export const SITEFILEIGNORE = '.sitefileignore';
|
|
9
|
+
const NON_ASCII = /[^\x00-\x7f]/;
|
|
10
|
+
function normalizationCollision(rel, span) {
|
|
11
|
+
return new SitefileError('E_PATH_COLLISION', `'${display(rel)}' names more than one file in the build context: their names differ only in Unicode normalization (NFC/NFD); rename one`, span);
|
|
12
|
+
}
|
|
13
|
+
function errCode(e) {
|
|
14
|
+
return e?.code;
|
|
15
|
+
}
|
|
16
|
+
async function exists(p) {
|
|
17
|
+
return fs.lstat(p).then(() => true, () => false);
|
|
18
|
+
}
|
|
19
|
+
function display(rel) {
|
|
20
|
+
return rel === '' ? '.' : './' + rel;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The local build context. Every read is confined to `root`: real paths are
|
|
24
|
+
* resolved and must stay inside it.
|
|
25
|
+
*/
|
|
26
|
+
export class LocalContext {
|
|
27
|
+
root;
|
|
28
|
+
rules;
|
|
29
|
+
allowHardlinks;
|
|
30
|
+
ignore;
|
|
31
|
+
constructor(root, ignore, rules, allowHardlinks) {
|
|
32
|
+
this.root = root;
|
|
33
|
+
this.rules = rules;
|
|
34
|
+
this.allowHardlinks = allowHardlinks;
|
|
35
|
+
this.ignore = ignore.map((p) => posix.normalize(nfc(p)).replace(/^\.\/+/, '').replace(/\/+$/, '')).filter((p) => p !== '' && p !== '.');
|
|
36
|
+
}
|
|
37
|
+
static async open(dir, options) {
|
|
38
|
+
let root;
|
|
39
|
+
try {
|
|
40
|
+
root = await fs.realpath(dir);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
if (errCode(e) === 'ENOENT')
|
|
44
|
+
throw new SitefileError('E_NOT_FOUND', `build context '${dir}' does not exist`, undefined, { cause: e });
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
const st = await fs.stat(root);
|
|
48
|
+
if (!st.isDirectory())
|
|
49
|
+
throw new SitefileError('E_UNSUPPORTED_FILE', `build context '${dir}' is not a directory`);
|
|
50
|
+
let rules = IgnoreRules.empty;
|
|
51
|
+
if (options?.sitefileignore !== false && (await exists(join(root, SITEFILEIGNORE)))) {
|
|
52
|
+
// Read through the sandbox: a symlinked .sitefileignore must stay inside the context.
|
|
53
|
+
const probe = new LocalContext(root, [], IgnoreRules.empty, true);
|
|
54
|
+
// A broken ignore file fails the build: skipping it could publish files it was meant to exclude.
|
|
55
|
+
const { real, st } = await probe.resolve(SITEFILEIGNORE).catch((e) => {
|
|
56
|
+
if (!(e instanceof SitefileError) || e.code !== 'E_NOT_FOUND')
|
|
57
|
+
throw e;
|
|
58
|
+
throw new SitefileError('E_NOT_FOUND', `'${SITEFILEIGNORE}' is a symlink to a missing file; fix or remove it (its rules would otherwise be skipped)`, undefined, { cause: e });
|
|
59
|
+
});
|
|
60
|
+
if (!st.isFile())
|
|
61
|
+
throw new SitefileError('E_UNSUPPORTED_FILE', `'${SITEFILEIGNORE}' is not a regular file`);
|
|
62
|
+
rules = IgnoreRules.parse(nfc(await fs.readFile(real, 'utf8')));
|
|
63
|
+
}
|
|
64
|
+
return new LocalContext(root, options?.ignore ?? ['.git'], rules, options?.allowHardlinks ?? false);
|
|
65
|
+
}
|
|
66
|
+
inside(real) {
|
|
67
|
+
return real === this.root || real.startsWith(this.root.endsWith(sep) ? this.root : this.root + sep);
|
|
68
|
+
}
|
|
69
|
+
isIgnored(rel) {
|
|
70
|
+
return this.ignore.some((ig) => rel === ig || rel.startsWith(ig + '/'));
|
|
71
|
+
}
|
|
72
|
+
/** Hard-linked files may alias files outside the context, which realpath cannot detect. */
|
|
73
|
+
checkHardlink(st, rel, span) {
|
|
74
|
+
if (this.allowHardlinks || !st.isFile() || st.nlink <= 1)
|
|
75
|
+
return;
|
|
76
|
+
throw new SitefileError('E_HARDLINK', `'${display(rel)}' has ${st.nlink} hard links and may alias a file outside the build context (--allow-hardlinks permits this)`, span);
|
|
77
|
+
}
|
|
78
|
+
/** Context-relative POSIX path of a real path inside root. */
|
|
79
|
+
relOfReal(real) {
|
|
80
|
+
return relative(this.root, real).split(sep).join('/');
|
|
81
|
+
}
|
|
82
|
+
/** Resolve a context-relative path to its real path and stats, enforcing the sandbox. */
|
|
83
|
+
/** A context file's real path and bytes, with the usual sandbox, symlink and hard-link rules. */
|
|
84
|
+
async realFile(rel, span) {
|
|
85
|
+
const { real } = await this.resolve(rel, span);
|
|
86
|
+
const f = await this.readFile(rel, span);
|
|
87
|
+
return { real, bytes: f.bytes };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The host path of the context-relative (NFC) path `rel`. A non-ASCII
|
|
91
|
+
* segment is matched by NFC form against its directory listing, so a file
|
|
92
|
+
* is found whichever normalization its on-disk name uses (macOS stores what
|
|
93
|
+
* it was given, often NFD; Linux compares bytes). Two names in one
|
|
94
|
+
* directory with the same NFC form are E_PATH_COLLISION. A segment with no
|
|
95
|
+
* match is joined as is, so the caller sees the usual ENOENT.
|
|
96
|
+
*/
|
|
97
|
+
async hostPath(rel, span) {
|
|
98
|
+
if (rel === '')
|
|
99
|
+
return this.root;
|
|
100
|
+
if (!NON_ASCII.test(rel))
|
|
101
|
+
return join(this.root, ...rel.split('/'));
|
|
102
|
+
let cur = this.root;
|
|
103
|
+
let done = '';
|
|
104
|
+
for (const seg of rel.split('/')) {
|
|
105
|
+
done = done === '' ? seg : `${done}/${seg}`;
|
|
106
|
+
if (!NON_ASCII.test(seg)) {
|
|
107
|
+
cur = join(cur, seg);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const names = await fs.readdir(cur).catch(() => []);
|
|
111
|
+
const matches = names.filter((n) => nfc(n) === seg);
|
|
112
|
+
if (matches.length > 1)
|
|
113
|
+
throw normalizationCollision(done, span);
|
|
114
|
+
cur = join(cur, matches[0] ?? seg);
|
|
115
|
+
}
|
|
116
|
+
return cur;
|
|
117
|
+
}
|
|
118
|
+
async resolve(rel, span, linkName) {
|
|
119
|
+
let real;
|
|
120
|
+
try {
|
|
121
|
+
real = await fs.realpath(await this.hostPath(rel, span));
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e instanceof SitefileError)
|
|
125
|
+
throw e;
|
|
126
|
+
const code = errCode(e);
|
|
127
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
128
|
+
throw new SitefileError('E_NOT_FOUND', linkName ? `symlink '${linkName}' is dangling` : `'${display(rel)}' does not exist in the build context`, span, { cause: e });
|
|
129
|
+
}
|
|
130
|
+
if (code === 'ELOOP') {
|
|
131
|
+
throw new SitefileError('E_UNSUPPORTED_FILE', `'${display(rel)}' is a symlink loop`, span, { cause: e });
|
|
132
|
+
}
|
|
133
|
+
throw new SitefileError('E_NOT_FOUND', `cannot resolve '${display(rel)}': ${e.message}`, span, { cause: e });
|
|
134
|
+
}
|
|
135
|
+
if (!this.inside(real)) {
|
|
136
|
+
throw new SitefileError('E_PATH_ESCAPE', linkName
|
|
137
|
+
? `symlink '${linkName}' points outside the build context`
|
|
138
|
+
: `'${display(rel)}' resolves outside the build context`, span);
|
|
139
|
+
}
|
|
140
|
+
const st = await fs.stat(real);
|
|
141
|
+
return { real, st };
|
|
142
|
+
}
|
|
143
|
+
static kindOf(st, rel, span) {
|
|
144
|
+
if (st.isFile())
|
|
145
|
+
return 'file';
|
|
146
|
+
if (st.isDirectory())
|
|
147
|
+
return 'directory';
|
|
148
|
+
throw new SitefileError('E_UNSUPPORTED_FILE', `'${display(rel)}' is not a regular file or directory (sockets, fifos and devices are not supported)`, span);
|
|
149
|
+
}
|
|
150
|
+
async stat(rel, span) {
|
|
151
|
+
const { st } = await this.resolve(rel, span);
|
|
152
|
+
return LocalContext.kindOf(st, rel, span);
|
|
153
|
+
}
|
|
154
|
+
/** The real path of a context-relative path, with the sandbox checks of every read (an imported Sitefile's context). */
|
|
155
|
+
async realPath(rel, span) {
|
|
156
|
+
return (await this.resolve(rel, span)).real;
|
|
157
|
+
}
|
|
158
|
+
async readFile(rel, span) {
|
|
159
|
+
const { real, st } = await this.resolve(rel, span);
|
|
160
|
+
if (LocalContext.kindOf(st, rel, span) !== 'file') {
|
|
161
|
+
throw new SitefileError('E_UNSUPPORTED_FILE', `'${display(rel)}' is a directory, not a file`, span);
|
|
162
|
+
}
|
|
163
|
+
this.checkHardlink(st, rel, span);
|
|
164
|
+
const bytes = new Uint8Array(await fs.readFile(real));
|
|
165
|
+
return {
|
|
166
|
+
kind: 'file',
|
|
167
|
+
bytes,
|
|
168
|
+
mode: st.mode & 0o111 ? 0o755 : 0o644,
|
|
169
|
+
basename: posix.basename(rel),
|
|
170
|
+
provenance: { uri: './' + rel, digest: sha256(bytes) },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** `exclude` (COPY --exclude) skips paths relative to `rel`, like `.sitefileignore` rules (excluded directories are not read). */
|
|
174
|
+
async readTree(rel, origin, span, exclude) {
|
|
175
|
+
const { real, st } = await this.resolve(rel, span);
|
|
176
|
+
if (LocalContext.kindOf(st, rel, span) !== 'directory') {
|
|
177
|
+
throw new SitefileError('E_UNSUPPORTED_FILE', `'${display(rel)}' is a file, not a directory`, span);
|
|
178
|
+
}
|
|
179
|
+
const files = [];
|
|
180
|
+
await this.walk(real, rel, '', origin, new Set([real]), files, span, exclude);
|
|
181
|
+
// With the instruction's span: two names that differ only in case (possible on a
|
|
182
|
+
// case-sensitive file system) fail at the line that read the directory.
|
|
183
|
+
const tree = Tree.empty.withFiles(files, 'error', span);
|
|
184
|
+
return { kind: 'tree', tree, provenance: { uri: rel === '' ? './' : './' + rel, digest: tree.digest() } };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* @param realDir real path of the directory being walked
|
|
188
|
+
* @param ctxRel logical context-relative path of that directory
|
|
189
|
+
* @param treeRel path of that directory inside the resulting tree ('' = root)
|
|
190
|
+
* @param ancestors real paths of directories on the current walk path (cycle detection)
|
|
191
|
+
*/
|
|
192
|
+
async walk(realDir, ctxRel, treeRel, origin, ancestors, out, span, exclude) {
|
|
193
|
+
const entries = await fs.readdir(realDir, { withFileTypes: true });
|
|
194
|
+
const names = entries.map((d) => d.name).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
195
|
+
const byName = new Map(entries.map((d) => [d.name, d]));
|
|
196
|
+
// Tree paths are NFC, so two names that differ only in normalization (possible
|
|
197
|
+
// on Linux, not on macOS) would be one path: refuse rather than pick one.
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
for (const raw of names) {
|
|
200
|
+
const dirent = byName.get(raw);
|
|
201
|
+
const name = nfc(raw);
|
|
202
|
+
if (seen.has(name))
|
|
203
|
+
throw normalizationCollision(ctxRel === '' ? name : `${ctxRel}/${name}`, span);
|
|
204
|
+
seen.add(name);
|
|
205
|
+
const childCtx = ctxRel === '' ? name : `${ctxRel}/${name}`;
|
|
206
|
+
const childTree = treeRel === '' ? name : `${treeRel}/${name}`;
|
|
207
|
+
if (this.isIgnored(childCtx))
|
|
208
|
+
continue;
|
|
209
|
+
let childReal = join(realDir, raw);
|
|
210
|
+
let st;
|
|
211
|
+
if (dirent.isSymbolicLink()) {
|
|
212
|
+
const r = await this.resolve(childCtx, span, display(childCtx));
|
|
213
|
+
childReal = r.real;
|
|
214
|
+
st = r.st;
|
|
215
|
+
// A link to an ignored location (e.g. .git) is skipped too.
|
|
216
|
+
if (this.isIgnored(this.relOfReal(childReal)))
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
st = await fs.lstat(childReal);
|
|
221
|
+
}
|
|
222
|
+
if (this.rules.ignores(childCtx, st.isDirectory()))
|
|
223
|
+
continue;
|
|
224
|
+
if (exclude?.ignores(childTree, st.isDirectory()))
|
|
225
|
+
continue;
|
|
226
|
+
if (st.isFile()) {
|
|
227
|
+
this.checkHardlink(st, childCtx, span);
|
|
228
|
+
const bytes = new Uint8Array(await fs.readFile(childReal));
|
|
229
|
+
out.push(['/' + childTree, makeFile(bytes, origin, st.mode)]);
|
|
230
|
+
}
|
|
231
|
+
else if (st.isDirectory()) {
|
|
232
|
+
if (ancestors.has(childReal))
|
|
233
|
+
continue; // directory symlink cycle
|
|
234
|
+
ancestors.add(childReal);
|
|
235
|
+
try {
|
|
236
|
+
await this.walk(childReal, childCtx, childTree, origin, ancestors, out, span, exclude);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
ancestors.delete(childReal);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
throw new SitefileError('E_UNSUPPORTED_FILE', `'${display(childCtx)}' is not a regular file or directory (sockets, fifos and devices are not supported)`, span);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface LockEntry {
|
|
2
|
+
/** `sha256:<hex>` of the response body. */
|
|
3
|
+
readonly integrity: string;
|
|
4
|
+
/** Body size in bytes. */
|
|
5
|
+
readonly size: number;
|
|
6
|
+
/** Final URL after redirects. */
|
|
7
|
+
readonly resolved: string;
|
|
8
|
+
}
|
|
9
|
+
/** A container image reference (`BASE`) pinned to the digest its tag resolved to. */
|
|
10
|
+
export interface ImageLockEntry {
|
|
11
|
+
/** `sha256:<hex>` of what the tag pointed at: an image index or a manifest. */
|
|
12
|
+
readonly digest: string;
|
|
13
|
+
}
|
|
14
|
+
/** A Git IMPORT (`git+https://…#ref:path`, keyed by the URI as written) pinned to the commit its ref resolved to. */
|
|
15
|
+
export interface GitLockEntry {
|
|
16
|
+
/** 40 lowercase hex digits. */
|
|
17
|
+
readonly commit: string;
|
|
18
|
+
}
|
|
19
|
+
/** `Sitefile.lock`: pinned digests of remote resources. */
|
|
20
|
+
export declare class Lockfile {
|
|
21
|
+
readonly path: string;
|
|
22
|
+
private existed;
|
|
23
|
+
private originalText;
|
|
24
|
+
private readonly entries;
|
|
25
|
+
private readonly used;
|
|
26
|
+
private readonly images;
|
|
27
|
+
private readonly usedImages;
|
|
28
|
+
private readonly gits;
|
|
29
|
+
private readonly usedGits;
|
|
30
|
+
private constructor();
|
|
31
|
+
static load(path: string): Promise<Lockfile>;
|
|
32
|
+
/** The commit a Git IMPORT URI was locked to. */
|
|
33
|
+
getGit(url: string): GitLockEntry | undefined;
|
|
34
|
+
recordGit(url: string, entry: GitLockEntry): void;
|
|
35
|
+
/** The pinned digest for a canonical image reference (`docker.io/library/nginx:alpine`). */
|
|
36
|
+
getImage(ref: string): ImageLockEntry | undefined;
|
|
37
|
+
recordImage(ref: string, entry: ImageLockEntry): void;
|
|
38
|
+
markImageUsed(ref: string): void;
|
|
39
|
+
get(url: string): LockEntry | undefined;
|
|
40
|
+
/** Add or replace an entry and mark it used in this build. */
|
|
41
|
+
record(url: string, entry: LockEntry): void;
|
|
42
|
+
/** Keep an existing entry in the saved lockfile. */
|
|
43
|
+
markUsed(url: string): void;
|
|
44
|
+
/** The JSON text `save()` would write. */
|
|
45
|
+
serialize(): string;
|
|
46
|
+
/**
|
|
47
|
+
* Write the entries used in this build. Returns true if the file was
|
|
48
|
+
* written. Does nothing when the content is unchanged, and never creates
|
|
49
|
+
* a lockfile that would have no entries.
|
|
50
|
+
*/
|
|
51
|
+
save(): Promise<boolean>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { SitefileError } from '../errors.js';
|
|
3
|
+
import { INTEGRITY_RE } from '../digest.js';
|
|
4
|
+
function isObject(v) {
|
|
5
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
6
|
+
}
|
|
7
|
+
/** `Sitefile.lock`: pinned digests of remote resources. */
|
|
8
|
+
export class Lockfile {
|
|
9
|
+
path;
|
|
10
|
+
existed;
|
|
11
|
+
originalText;
|
|
12
|
+
entries;
|
|
13
|
+
used = new Set();
|
|
14
|
+
images = new Map();
|
|
15
|
+
usedImages = new Set();
|
|
16
|
+
gits = new Map();
|
|
17
|
+
usedGits = new Set();
|
|
18
|
+
constructor(path, entries, existed, originalText) {
|
|
19
|
+
this.path = path;
|
|
20
|
+
this.existed = existed;
|
|
21
|
+
this.originalText = originalText;
|
|
22
|
+
this.entries = entries;
|
|
23
|
+
}
|
|
24
|
+
static async load(path) {
|
|
25
|
+
let text;
|
|
26
|
+
try {
|
|
27
|
+
text = await fs.readFile(path, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
if (e.code === 'ENOENT')
|
|
31
|
+
return new Lockfile(path, new Map(), false, undefined);
|
|
32
|
+
throw e;
|
|
33
|
+
}
|
|
34
|
+
const bad = (why, cause) => new SitefileError('E_VALIDATION', `malformed lockfile ${path}: ${why}`, undefined, cause ? { cause } : undefined);
|
|
35
|
+
let data;
|
|
36
|
+
try {
|
|
37
|
+
data = JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
throw bad(e.message, e);
|
|
41
|
+
}
|
|
42
|
+
if (!isObject(data))
|
|
43
|
+
throw bad('expected a JSON object');
|
|
44
|
+
if (data.version !== 1)
|
|
45
|
+
throw bad(`unsupported version ${JSON.stringify(data.version)} (expected 1)`);
|
|
46
|
+
const resources = data.resources ?? {};
|
|
47
|
+
if (!isObject(resources))
|
|
48
|
+
throw bad('"resources" must be an object');
|
|
49
|
+
const entries = new Map();
|
|
50
|
+
for (const [url, raw] of Object.entries(resources)) {
|
|
51
|
+
if (!isObject(raw))
|
|
52
|
+
throw bad(`entry for ${url} must be an object`);
|
|
53
|
+
const { integrity, size, resolved } = raw;
|
|
54
|
+
if (typeof integrity !== 'string' || !INTEGRITY_RE.test(integrity)) {
|
|
55
|
+
throw bad(`entry for ${url} has an invalid "integrity" (expected sha256:<64 hex>)`);
|
|
56
|
+
}
|
|
57
|
+
if (typeof size !== 'number' || !Number.isSafeInteger(size) || size < 0) {
|
|
58
|
+
throw bad(`entry for ${url} has an invalid "size"`);
|
|
59
|
+
}
|
|
60
|
+
if (typeof resolved !== 'string')
|
|
61
|
+
throw bad(`entry for ${url} has an invalid "resolved"`);
|
|
62
|
+
entries.set(url, { integrity, size, resolved });
|
|
63
|
+
}
|
|
64
|
+
const lock = new Lockfile(path, entries, true, text);
|
|
65
|
+
const images = data.images ?? {};
|
|
66
|
+
if (!isObject(images))
|
|
67
|
+
throw bad('"images" must be an object');
|
|
68
|
+
for (const [ref, raw] of Object.entries(images)) {
|
|
69
|
+
if (!isObject(raw) || typeof raw.digest !== 'string' || !INTEGRITY_RE.test(raw.digest)) {
|
|
70
|
+
throw bad(`image entry for ${ref} needs a "digest" (sha256:<64 hex>)`);
|
|
71
|
+
}
|
|
72
|
+
lock.images.set(ref, { digest: raw.digest });
|
|
73
|
+
}
|
|
74
|
+
const gits = data.git ?? {};
|
|
75
|
+
if (!isObject(gits))
|
|
76
|
+
throw bad('"git" must be an object');
|
|
77
|
+
for (const [url, raw] of Object.entries(gits)) {
|
|
78
|
+
if (!isObject(raw) || typeof raw.commit !== 'string' || !/^[0-9a-f]{40}$/.test(raw.commit)) {
|
|
79
|
+
throw bad(`git entry for ${url} needs a "commit" (40 lowercase hex digits)`);
|
|
80
|
+
}
|
|
81
|
+
lock.gits.set(url, { commit: raw.commit });
|
|
82
|
+
}
|
|
83
|
+
return lock;
|
|
84
|
+
}
|
|
85
|
+
/** The commit a Git IMPORT URI was locked to. */
|
|
86
|
+
getGit(url) {
|
|
87
|
+
return this.gits.get(url);
|
|
88
|
+
}
|
|
89
|
+
recordGit(url, entry) {
|
|
90
|
+
this.gits.set(url, { commit: entry.commit });
|
|
91
|
+
this.usedGits.add(url);
|
|
92
|
+
}
|
|
93
|
+
/** The pinned digest for a canonical image reference (`docker.io/library/nginx:alpine`). */
|
|
94
|
+
getImage(ref) {
|
|
95
|
+
return this.images.get(ref);
|
|
96
|
+
}
|
|
97
|
+
recordImage(ref, entry) {
|
|
98
|
+
this.images.set(ref, { digest: entry.digest });
|
|
99
|
+
this.usedImages.add(ref);
|
|
100
|
+
}
|
|
101
|
+
markImageUsed(ref) {
|
|
102
|
+
if (this.images.has(ref))
|
|
103
|
+
this.usedImages.add(ref);
|
|
104
|
+
}
|
|
105
|
+
get(url) {
|
|
106
|
+
return this.entries.get(url);
|
|
107
|
+
}
|
|
108
|
+
/** Add or replace an entry and mark it used in this build. */
|
|
109
|
+
record(url, entry) {
|
|
110
|
+
this.entries.set(url, { integrity: entry.integrity, size: entry.size, resolved: entry.resolved });
|
|
111
|
+
this.used.add(url);
|
|
112
|
+
}
|
|
113
|
+
/** Keep an existing entry in the saved lockfile. */
|
|
114
|
+
markUsed(url) {
|
|
115
|
+
if (this.entries.has(url))
|
|
116
|
+
this.used.add(url);
|
|
117
|
+
}
|
|
118
|
+
/** The JSON text `save()` would write. */
|
|
119
|
+
serialize() {
|
|
120
|
+
const resources = {};
|
|
121
|
+
for (const url of [...this.used].sort()) {
|
|
122
|
+
const e = this.entries.get(url);
|
|
123
|
+
if (e)
|
|
124
|
+
resources[url] = { integrity: e.integrity, size: e.size, resolved: e.resolved };
|
|
125
|
+
}
|
|
126
|
+
const data = { version: 1, resources };
|
|
127
|
+
if (this.usedImages.size > 0) {
|
|
128
|
+
const images = {};
|
|
129
|
+
for (const ref of [...this.usedImages].sort()) {
|
|
130
|
+
const e = this.images.get(ref);
|
|
131
|
+
if (e)
|
|
132
|
+
images[ref] = { digest: e.digest };
|
|
133
|
+
}
|
|
134
|
+
data.images = images;
|
|
135
|
+
}
|
|
136
|
+
if (this.usedGits.size > 0) {
|
|
137
|
+
const gits = {};
|
|
138
|
+
for (const url of [...this.usedGits].sort()) {
|
|
139
|
+
const e = this.gits.get(url);
|
|
140
|
+
if (e)
|
|
141
|
+
gits[url] = { commit: e.commit };
|
|
142
|
+
}
|
|
143
|
+
data.git = gits;
|
|
144
|
+
}
|
|
145
|
+
return JSON.stringify(data, null, 2) + '\n';
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Write the entries used in this build. Returns true if the file was
|
|
149
|
+
* written. Does nothing when the content is unchanged, and never creates
|
|
150
|
+
* a lockfile that would have no entries.
|
|
151
|
+
*/
|
|
152
|
+
async save() {
|
|
153
|
+
const text = this.serialize();
|
|
154
|
+
if (!this.existed && this.used.size === 0 && this.usedImages.size === 0 && this.usedGits.size === 0)
|
|
155
|
+
return false;
|
|
156
|
+
if (this.existed && text === this.originalText)
|
|
157
|
+
return false;
|
|
158
|
+
const tmp = `${this.path}.${process.pid}.${Date.now()}.tmp`;
|
|
159
|
+
try {
|
|
160
|
+
await fs.writeFile(tmp, text);
|
|
161
|
+
await fs.rename(tmp, this.path);
|
|
162
|
+
this.existed = true;
|
|
163
|
+
this.originalText = text;
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
await fs.rm(tmp, { force: true });
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=lockfile.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when `address` is an IP literal that is publicly routable. Loopback,
|
|
3
|
+
* private, link-local, CGNAT, documentation, multicast, reserved and every
|
|
4
|
+
* IPv4-mapped IPv6 address are not public. Non-IP strings return false.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isPublicAddress(address: string): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { BlockList, isIP } from 'node:net';
|
|
2
|
+
// Separate lists: a BlockList checks IPv4 addresses against IPv4-mapped IPv6
|
|
3
|
+
// rules too, so `::ffff:0:0/96` in a shared list would block every IPv4.
|
|
4
|
+
const blocked4 = new BlockList();
|
|
5
|
+
const blocked6 = new BlockList();
|
|
6
|
+
const V4 = [
|
|
7
|
+
['0.0.0.0', 8],
|
|
8
|
+
['10.0.0.0', 8],
|
|
9
|
+
['100.64.0.0', 10],
|
|
10
|
+
['127.0.0.0', 8],
|
|
11
|
+
['169.254.0.0', 16],
|
|
12
|
+
['172.16.0.0', 12],
|
|
13
|
+
['192.0.0.0', 24],
|
|
14
|
+
['192.0.2.0', 24],
|
|
15
|
+
['192.168.0.0', 16],
|
|
16
|
+
['198.18.0.0', 15],
|
|
17
|
+
['198.51.100.0', 24],
|
|
18
|
+
['203.0.113.0', 24],
|
|
19
|
+
['224.0.0.0', 4],
|
|
20
|
+
['240.0.0.0', 4], // includes 255.255.255.255
|
|
21
|
+
];
|
|
22
|
+
const V6 = [
|
|
23
|
+
['::', 128],
|
|
24
|
+
['::1', 128],
|
|
25
|
+
['::ffff:0:0', 96], // all IPv4-mapped addresses
|
|
26
|
+
['64:ff9b:1::', 48],
|
|
27
|
+
['100::', 64],
|
|
28
|
+
['2001:db8::', 32],
|
|
29
|
+
['fc00::', 7],
|
|
30
|
+
['fe80::', 10],
|
|
31
|
+
['ff00::', 8],
|
|
32
|
+
];
|
|
33
|
+
for (const [net, prefix] of V4)
|
|
34
|
+
blocked4.addSubnet(net, prefix, 'ipv4');
|
|
35
|
+
for (const [net, prefix] of V6)
|
|
36
|
+
blocked6.addSubnet(net, prefix, 'ipv6');
|
|
37
|
+
/**
|
|
38
|
+
* True when `address` is an IP literal that is publicly routable. Loopback,
|
|
39
|
+
* private, link-local, CGNAT, documentation, multicast, reserved and every
|
|
40
|
+
* IPv4-mapped IPv6 address are not public. Non-IP strings return false.
|
|
41
|
+
*/
|
|
42
|
+
export function isPublicAddress(address) {
|
|
43
|
+
let a = address;
|
|
44
|
+
if (a.startsWith('[') && a.endsWith(']'))
|
|
45
|
+
a = a.slice(1, -1);
|
|
46
|
+
// Zone IDs (fe80::1%en0) are link-local by definition.
|
|
47
|
+
if (a.includes('%'))
|
|
48
|
+
return false;
|
|
49
|
+
const family = isIP(a);
|
|
50
|
+
if (family === 4)
|
|
51
|
+
return !blocked4.check(a, 'ipv4');
|
|
52
|
+
if (family === 6) {
|
|
53
|
+
if (blocked6.check(a, 'ipv6'))
|
|
54
|
+
return false;
|
|
55
|
+
// NAT64 well-known prefix 64:ff9b::/96 embeds an IPv4 address in the low
|
|
56
|
+
// 32 bits; judge it by that address (64:ff9b::7f00:1 is 127.0.0.1).
|
|
57
|
+
const groups = expandIPv6(a);
|
|
58
|
+
if (groups && groups[0] === 0x64 && groups[1] === 0xff9b && groups.slice(2, 6).every((g) => g === 0)) {
|
|
59
|
+
const hi = groups[6];
|
|
60
|
+
const lo = groups[7];
|
|
61
|
+
return !blocked4.check(`${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}`, 'ipv4');
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/** Eight 16-bit groups of a valid IPv6 literal (which may end in dotted IPv4). */
|
|
68
|
+
function expandIPv6(a) {
|
|
69
|
+
let s = a.toLowerCase();
|
|
70
|
+
const v4 = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(s);
|
|
71
|
+
if (v4) {
|
|
72
|
+
const [b0, b1, b2, b3] = v4.slice(1).map(Number);
|
|
73
|
+
s = s.slice(0, v4.index) + ((b0 << 8) | b1).toString(16) + ':' + ((b2 << 8) | b3).toString(16);
|
|
74
|
+
}
|
|
75
|
+
const [head, tail] = s.split('::');
|
|
76
|
+
const h = head ? head.split(':') : [];
|
|
77
|
+
const t = tail ? tail.split(':') : [];
|
|
78
|
+
const zeros = tail === undefined ? [] : new Array(8 - h.length - t.length).fill('0');
|
|
79
|
+
const all = [...h, ...zeros, ...t].map((g) => parseInt(g, 16));
|
|
80
|
+
return all.length === 8 ? all : undefined;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=netpolicy.js.map
|