@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.
Files changed (174) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +475 -2
  4. package/lib/src/archive-writers.d.ts +31 -0
  5. package/lib/src/archive-writers.js +197 -0
  6. package/lib/src/attest.d.ts +126 -0
  7. package/lib/src/attest.js +220 -0
  8. package/lib/src/bake.d.ts +74 -0
  9. package/lib/src/bake.js +372 -0
  10. package/lib/src/base-image.d.ts +76 -0
  11. package/lib/src/base-image.js +138 -0
  12. package/lib/src/build-args.d.ts +26 -0
  13. package/lib/src/build-args.js +164 -0
  14. package/lib/src/cache-limit.d.ts +54 -0
  15. package/lib/src/cache-limit.js +239 -0
  16. package/lib/src/casefold-data.d.ts +2 -0
  17. package/lib/src/casefold-data.js +217 -0
  18. package/lib/src/cbor.d.ts +39 -0
  19. package/lib/src/cbor.js +174 -0
  20. package/lib/src/checklinks.d.ts +42 -0
  21. package/lib/src/checklinks.js +118 -0
  22. package/lib/src/cli/bake.d.ts +2 -0
  23. package/lib/src/cli/bake.js +179 -0
  24. package/lib/src/cli/build-image.d.ts +21 -0
  25. package/lib/src/cli/build-image.js +42 -0
  26. package/lib/src/cli/cache.d.ts +8 -0
  27. package/lib/src/cli/cache.js +109 -0
  28. package/lib/src/cli/common.d.ts +29 -0
  29. package/lib/src/cli/common.js +100 -0
  30. package/lib/src/cli/git-context.d.ts +18 -0
  31. package/lib/src/cli/git-context.js +48 -0
  32. package/lib/src/cli/images.d.ts +6 -0
  33. package/lib/src/cli/images.js +285 -0
  34. package/lib/src/cli/index.d.ts +10 -0
  35. package/lib/src/cli/index.js +95 -0
  36. package/lib/src/cli/outdated.d.ts +5 -0
  37. package/lib/src/cli/outdated.js +94 -0
  38. package/lib/src/cli/registry.d.ts +2 -0
  39. package/lib/src/cli/registry.js +122 -0
  40. package/lib/src/cli/serve-worker.d.ts +2 -0
  41. package/lib/src/cli/serve-worker.js +42 -0
  42. package/lib/src/cli/servers.d.ts +15 -0
  43. package/lib/src/cli/servers.js +451 -0
  44. package/lib/src/cli/vendor.d.ts +2 -0
  45. package/lib/src/cli/vendor.js +92 -0
  46. package/lib/src/cli/watch.d.ts +26 -0
  47. package/lib/src/cli/watch.js +81 -0
  48. package/lib/src/cli.d.ts +12 -0
  49. package/lib/src/cli.js +486 -0
  50. package/lib/src/csv.d.ts +27 -0
  51. package/lib/src/csv.js +117 -0
  52. package/lib/src/deflate.d.ts +76 -0
  53. package/lib/src/deflate.js +604 -0
  54. package/lib/src/digest.d.ts +3 -0
  55. package/lib/src/digest.js +7 -0
  56. package/lib/src/errors.d.ts +34 -0
  57. package/lib/src/errors.js +20 -0
  58. package/lib/src/executor.d.ts +386 -0
  59. package/lib/src/executor.js +2193 -0
  60. package/lib/src/explain.d.ts +64 -0
  61. package/lib/src/explain.js +445 -0
  62. package/lib/src/export-formats.d.ts +16 -0
  63. package/lib/src/export-formats.js +52 -0
  64. package/lib/src/exporter.d.ts +103 -0
  65. package/lib/src/exporter.js +676 -0
  66. package/lib/src/fingerprint.d.ts +40 -0
  67. package/lib/src/fingerprint.js +168 -0
  68. package/lib/src/frontmatter.d.ts +22 -0
  69. package/lib/src/frontmatter.js +45 -0
  70. package/lib/src/index.d.ts +12 -0
  71. package/lib/src/index.js +12 -0
  72. package/lib/src/language/ast.d.ts +353 -0
  73. package/lib/src/language/ast.js +57 -0
  74. package/lib/src/language/parser.d.ts +3 -0
  75. package/lib/src/language/parser.js +612 -0
  76. package/lib/src/language/scanner.d.ts +110 -0
  77. package/lib/src/language/scanner.js +449 -0
  78. package/lib/src/language/validate.d.ts +13 -0
  79. package/lib/src/language/validate.js +367 -0
  80. package/lib/src/links.d.ts +79 -0
  81. package/lib/src/links.js +583 -0
  82. package/lib/src/lint.d.ts +10 -0
  83. package/lib/src/lint.js +223 -0
  84. package/lib/src/markdown.d.ts +29 -0
  85. package/lib/src/markdown.js +612 -0
  86. package/lib/src/media.d.ts +82 -0
  87. package/lib/src/media.js +650 -0
  88. package/lib/src/memo.d.ts +57 -0
  89. package/lib/src/memo.js +93 -0
  90. package/lib/src/oci.d.ts +178 -0
  91. package/lib/src/oci.js +380 -0
  92. package/lib/src/outdated.d.ts +46 -0
  93. package/lib/src/outdated.js +195 -0
  94. package/lib/src/pages.d.ts +10 -0
  95. package/lib/src/pages.js +54 -0
  96. package/lib/src/paths.d.ts +39 -0
  97. package/lib/src/paths.js +101 -0
  98. package/lib/src/planner.d.ts +297 -0
  99. package/lib/src/planner.js +410 -0
  100. package/lib/src/plugins/cache.d.ts +70 -0
  101. package/lib/src/plugins/cache.js +226 -0
  102. package/lib/src/plugins/exec.d.ts +51 -0
  103. package/lib/src/plugins/exec.js +144 -0
  104. package/lib/src/plugins/host.d.ts +75 -0
  105. package/lib/src/plugins/host.js +277 -0
  106. package/lib/src/plugins/wasm.d.ts +23 -0
  107. package/lib/src/plugins/wasm.js +249 -0
  108. package/lib/src/profile.d.ts +42 -0
  109. package/lib/src/profile.js +68 -0
  110. package/lib/src/ref.d.ts +40 -0
  111. package/lib/src/ref.js +79 -0
  112. package/lib/src/registry-auth.d.ts +32 -0
  113. package/lib/src/registry-auth.js +151 -0
  114. package/lib/src/registry-cache.d.ts +71 -0
  115. package/lib/src/registry-cache.js +333 -0
  116. package/lib/src/registry-ops.d.ts +51 -0
  117. package/lib/src/registry-ops.js +248 -0
  118. package/lib/src/registry.d.ts +109 -0
  119. package/lib/src/registry.js +505 -0
  120. package/lib/src/render.d.ts +134 -0
  121. package/lib/src/render.js +576 -0
  122. package/lib/src/resources/archive.d.ts +11 -0
  123. package/lib/src/resources/archive.js +306 -0
  124. package/lib/src/resources/bundle-data.d.ts +16 -0
  125. package/lib/src/resources/bundle-data.js +50 -0
  126. package/lib/src/resources/convert.d.ts +12 -0
  127. package/lib/src/resources/convert.js +63 -0
  128. package/lib/src/resources/documents.d.ts +35 -0
  129. package/lib/src/resources/documents.js +122 -0
  130. package/lib/src/resources/git.d.ts +103 -0
  131. package/lib/src/resources/git.js +720 -0
  132. package/lib/src/resources/https.d.ts +40 -0
  133. package/lib/src/resources/https.js +256 -0
  134. package/lib/src/resources/ignore.d.ts +33 -0
  135. package/lib/src/resources/ignore.js +119 -0
  136. package/lib/src/resources/local.d.ts +64 -0
  137. package/lib/src/resources/local.js +248 -0
  138. package/lib/src/resources/lockfile.d.ts +52 -0
  139. package/lib/src/resources/lockfile.js +172 -0
  140. package/lib/src/resources/netpolicy.d.ts +6 -0
  141. package/lib/src/resources/netpolicy.js +82 -0
  142. package/lib/src/resources/types.d.ts +63 -0
  143. package/lib/src/resources/types.js +13 -0
  144. package/lib/src/resources/vendor.d.ts +94 -0
  145. package/lib/src/resources/vendor.js +296 -0
  146. package/lib/src/resources/wbn-reader.d.ts +44 -0
  147. package/lib/src/resources/wbn-reader.js +302 -0
  148. package/lib/src/schema.d.ts +25 -0
  149. package/lib/src/schema.js +169 -0
  150. package/lib/src/secrets.d.ts +21 -0
  151. package/lib/src/secrets.js +103 -0
  152. package/lib/src/serve-rules.d.ts +29 -0
  153. package/lib/src/serve-rules.js +122 -0
  154. package/lib/src/server.d.ts +45 -0
  155. package/lib/src/server.js +246 -0
  156. package/lib/src/servers.d.ts +71 -0
  157. package/lib/src/servers.js +139 -0
  158. package/lib/src/site.d.ts +60 -0
  159. package/lib/src/site.js +169 -0
  160. package/lib/src/store.d.ts +151 -0
  161. package/lib/src/store.js +596 -0
  162. package/lib/src/tree.d.ts +50 -0
  163. package/lib/src/tree.js +134 -0
  164. package/lib/src/values.d.ts +7 -0
  165. package/lib/src/values.js +37 -0
  166. package/lib/src/vendor.d.ts +19 -0
  167. package/lib/src/vendor.js +145 -0
  168. package/lib/src/watch.d.ts +44 -0
  169. package/lib/src/watch.js +137 -0
  170. package/lib/src/wbn.d.ts +5 -0
  171. package/lib/src/wbn.js +147 -0
  172. package/lib/src/yaml.d.ts +34 -0
  173. package/lib/src/yaml.js +562 -0
  174. package/package.json +58 -4
@@ -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>;
@@ -0,0 +1,296 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { INTEGRITY_RE, sha256 } from '../digest.js';
5
+ import { SitefileError, isSitefileError } from '../errors.js';
6
+ import { MEDIA, parseManifest } from '../oci.js';
7
+ import { selectPlatform } from '../registry-ops.js';
8
+ import { extractArchive } from './archive.js';
9
+ /**
10
+ * The vendor directory written by `sitefile vendor` (issue #96): every remote input's bytes under
11
+ * content-addressed names, plus a manifest that maps URLs and image tags to them.
12
+ *
13
+ * <dir>/manifest.json {"version":1,"resources":{url:{integrity,size,resolved}},"images":{ref:{digest,platform,manifest}}}
14
+ * <dir>/blobs/sha256/<hex> IMPORT bodies and BASE image blobs (index, manifest, config, layers)
15
+ *
16
+ * Builds prefer these copies over the network and verify each one against its digest (and, as for any
17
+ * fetch, against INTEGRITY and Sitefile.lock). Nothing secret is written: URLs with credentials are
18
+ * rejected before any fetch, and registry tokens or passwords never leave memory.
19
+ */
20
+ export const DEFAULT_VENDOR_DIR = 'vendor/sitefile';
21
+ export const VENDOR_MANIFEST = 'manifest.json';
22
+ /** Validate a vendor directory: a relative path inside the build context. Returns it normalized (POSIX, no trailing slash). */
23
+ export function vendorDirRel(input) {
24
+ const n = path.posix.normalize(input).replace(/\/+$/, '');
25
+ if (input === '' || path.posix.isAbsolute(input) || input.includes('\\') || n === '.' || n === '..' || n.startsWith('../')) {
26
+ throw new SitefileError('E_USAGE', `--vendor-dir expects a relative path inside the build context, got '${input}'`);
27
+ }
28
+ return n;
29
+ }
30
+ const blobRel = (dir, digest) => `${dir}/blobs/sha256/${digest.slice('sha256:'.length)}`;
31
+ /**
32
+ * The vendor directory a build uses: `explicit` (which must have a manifest), else the default one if
33
+ * its manifest exists, else none.
34
+ */
35
+ export async function findVendorDir(contextReal, explicit) {
36
+ const dir = vendorDirRel(explicit ?? DEFAULT_VENDOR_DIR);
37
+ const exists = await fs.stat(path.join(contextReal, ...dir.split('/'), VENDOR_MANIFEST)).then((st) => st.isFile(), () => false);
38
+ if (exists)
39
+ return dir;
40
+ if (explicit !== undefined) {
41
+ throw new SitefileError('E_NOT_FOUND', `vendor directory '${dir}' has no ${VENDOR_MANIFEST}; run 'sitefile vendor --vendor-dir ${dir}' first`);
42
+ }
43
+ return undefined;
44
+ }
45
+ function isObject(v) {
46
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
47
+ }
48
+ /** Parse and check a vendor manifest. Errors are E_VALIDATION naming the file. */
49
+ export function parseVendorManifest(bytes, shown) {
50
+ const bad = (why) => new SitefileError('E_VALIDATION', `malformed vendor manifest ${shown}: ${why}`);
51
+ let data;
52
+ try {
53
+ data = JSON.parse(new TextDecoder().decode(bytes));
54
+ }
55
+ catch {
56
+ throw bad('invalid JSON');
57
+ }
58
+ if (!isObject(data))
59
+ throw bad('expected a JSON object');
60
+ if (data.version !== 1)
61
+ throw bad(`unsupported version ${JSON.stringify(data.version) ?? 'undefined'} (expected 1)`);
62
+ const resources = {};
63
+ const images = {};
64
+ const rs = data.resources ?? {};
65
+ if (!isObject(rs))
66
+ throw bad('"resources" must be an object');
67
+ for (const [url, e] of Object.entries(rs)) {
68
+ if (!isObject(e) || typeof e.integrity !== 'string' || !INTEGRITY_RE.test(e.integrity) || typeof e.size !== 'number' || !Number.isSafeInteger(e.size) || e.size < 0 || typeof e.resolved !== 'string') {
69
+ throw bad(`entry for ${url} needs "integrity" (sha256:<64 hex>), "size" and "resolved"`);
70
+ }
71
+ resources[url] = { integrity: e.integrity, size: e.size, resolved: e.resolved };
72
+ }
73
+ const is = data.images ?? {};
74
+ if (!isObject(is))
75
+ throw bad('"images" must be an object');
76
+ for (const [ref, e] of Object.entries(is)) {
77
+ if (!isObject(e) || typeof e.digest !== 'string' || !INTEGRITY_RE.test(e.digest) || typeof e.platform !== 'string' || typeof e.manifest !== 'string' || !INTEGRITY_RE.test(e.manifest)) {
78
+ throw bad(`image entry for ${ref} needs "digest", "platform" and "manifest"`);
79
+ }
80
+ images[ref] = { digest: e.digest, platform: e.platform, manifest: e.manifest };
81
+ }
82
+ const git = {};
83
+ const gs = data.git ?? {};
84
+ if (!isObject(gs))
85
+ throw bad('"git" must be an object');
86
+ for (const [url, e] of Object.entries(gs)) {
87
+ if (!isObject(e) || typeof e.commit !== 'string' || !/^[0-9a-f]{40}$/.test(e.commit) || (e.kind !== 'tree' && e.kind !== 'file') ||
88
+ typeof e.integrity !== 'string' || !INTEGRITY_RE.test(e.integrity) || typeof e.size !== 'number' || !Number.isSafeInteger(e.size) || e.size < 0 ||
89
+ (e.kind === 'file' ? e.mode !== '644' && e.mode !== '755' : e.mode !== undefined)) {
90
+ throw bad(`git entry for ${url} needs "commit", "kind" (tree or file), "integrity", "size" (and "mode" for a file)`);
91
+ }
92
+ git[url] = { commit: e.commit, kind: e.kind, integrity: e.integrity, size: e.size, ...(e.kind === 'file' ? { mode: e.mode } : {}) };
93
+ }
94
+ return { version: 1, resources, images, ...(Object.keys(git).length ? { git } : {}) };
95
+ }
96
+ /** The manifest as `sitefile vendor` writes it: sorted keys, two-space indent, trailing newline. */
97
+ export function serializeVendorManifest(m) {
98
+ const resources = {};
99
+ for (const url of Object.keys(m.resources).sort()) {
100
+ const e = m.resources[url];
101
+ resources[url] = { integrity: e.integrity, size: e.size, resolved: e.resolved };
102
+ }
103
+ const images = {};
104
+ for (const ref of Object.keys(m.images).sort()) {
105
+ const e = m.images[ref];
106
+ images[ref] = { digest: e.digest, platform: e.platform, manifest: e.manifest };
107
+ }
108
+ const git = {};
109
+ for (const url of Object.keys(m.git ?? {}).sort()) {
110
+ const e = m.git[url];
111
+ git[url] = { commit: e.commit, kind: e.kind, integrity: e.integrity, size: e.size, ...(e.mode !== undefined ? { mode: e.mode } : {}) };
112
+ }
113
+ return JSON.stringify({ version: 1, resources, images, ...(Object.keys(git).length ? { git } : {}) }, null, 2) + '\n';
114
+ }
115
+ function isIndex(bytes) {
116
+ try {
117
+ const j = JSON.parse(new TextDecoder().decode(bytes));
118
+ if (typeof j.mediaType === 'string')
119
+ return j.mediaType === MEDIA.index || j.mediaType === MEDIA.dockerList;
120
+ return Array.isArray(j.manifests);
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ }
126
+ /** A vendor directory opened through the build's sandboxed context reader. */
127
+ export class Vendor {
128
+ dir;
129
+ local;
130
+ manifest;
131
+ constructor(
132
+ /** Context-relative directory. */
133
+ dir, local, manifest) {
134
+ this.dir = dir;
135
+ this.local = local;
136
+ this.manifest = manifest;
137
+ }
138
+ static async open(local, dir) {
139
+ const rel = `${dir}/${VENDOR_MANIFEST}`;
140
+ const f = await local.readFile(rel);
141
+ return new Vendor(dir, local, parseVendorManifest(f.bytes, rel));
142
+ }
143
+ resource(url) {
144
+ return Object.hasOwn(this.manifest.resources, url) ? this.manifest.resources[url] : undefined;
145
+ }
146
+ image(ref) {
147
+ return Object.hasOwn(this.manifest.images, ref) ? this.manifest.images[ref] : undefined;
148
+ }
149
+ git(url) {
150
+ const g = this.manifest.git;
151
+ return g && Object.hasOwn(g, url) ? g[url] : undefined;
152
+ }
153
+ /** The vendored content of a Git IMPORT, verified against its digest. */
154
+ async readGit(url, e, basename, limits, span) {
155
+ const bytes = await this.readBlob(e.integrity, `vendored copy of '${url}'`, span);
156
+ if (e.kind === 'file')
157
+ return { kind: 'file', bytes, mode: e.mode === '755' ? 0o755 : 0o644, basename };
158
+ const tree = await extractArchive(bytes, `vendored copy of '${url}'`, limits, span);
159
+ return { kind: 'tree', files: tree.entries().map(([p, n]) => [p, { bytes: n.bytes, mode: n.mode }]) };
160
+ }
161
+ /** The vendored body of `url`, verified against its digest. */
162
+ async readResource(url, e, span) {
163
+ return { bytes: await this.readBlob(e.integrity, `vendored copy of '${url}'`, span), finalUrl: e.resolved };
164
+ }
165
+ async readBlob(digest, what, span) {
166
+ const rel = blobRel(this.dir, digest);
167
+ const { bytes } = await this.local.readFile(rel, span);
168
+ const got = sha256(bytes);
169
+ if (got !== digest) {
170
+ throw new SitefileError('E_INTEGRITY', `${what} (${rel}) is corrupt: expected ${digest}, got ${got}; run 'sitefile vendor' again`, span);
171
+ }
172
+ return bytes;
173
+ }
174
+ async hasBlob(digest) {
175
+ try {
176
+ return (await this.local.stat(blobRel(this.dir, digest))) === 'file';
177
+ }
178
+ catch (e) {
179
+ if (isSitefileError(e, 'E_NOT_FOUND'))
180
+ return false;
181
+ throw e;
182
+ }
183
+ }
184
+ /**
185
+ * Copy the vendored blobs of the image `rootDigest` (for `platform`) into the store, each verified,
186
+ * so pulling it finds everything locally. Stops quietly at the first blob that is not vendored: the
187
+ * pull then fetches the rest from the registry.
188
+ */
189
+ async seedStore(store, rootDigest, platform) {
190
+ if (!(await this.hasBlob(rootDigest)))
191
+ return;
192
+ const root = await this.readBlob(rootDigest, 'vendored image blob');
193
+ await store.putSmallBlob(root);
194
+ let manifest = root;
195
+ if (isIndex(root)) {
196
+ let d;
197
+ try {
198
+ d = selectPlatform(root, platform);
199
+ }
200
+ catch {
201
+ return;
202
+ }
203
+ if (!(await this.hasBlob(d.digest)))
204
+ return;
205
+ manifest = await this.readBlob(d.digest, 'vendored image blob');
206
+ await store.putSmallBlob(manifest);
207
+ }
208
+ let parsed;
209
+ try {
210
+ parsed = parseManifest(manifest);
211
+ }
212
+ catch {
213
+ return;
214
+ }
215
+ for (const d of [parsed.config, ...parsed.layers]) {
216
+ if (await store.hasBlob(d.digest, d.size))
217
+ continue;
218
+ if (!(await this.hasBlob(d.digest)))
219
+ return;
220
+ const bytes = await this.readBlob(d.digest, 'vendored image blob');
221
+ await store.importBlob(d.digest, d.size, (async function* () {
222
+ yield bytes;
223
+ })());
224
+ }
225
+ }
226
+ }
227
+ /** The nearest existing ancestor's real path, with the missing tail appended. */
228
+ async function realpathLoose(p) {
229
+ const tail = [];
230
+ let cur = p;
231
+ for (;;) {
232
+ try {
233
+ return path.join(await fs.realpath(cur), ...tail.reverse());
234
+ }
235
+ catch {
236
+ const parent = path.dirname(cur);
237
+ if (parent === cur)
238
+ return p;
239
+ tail.push(path.basename(cur));
240
+ cur = parent;
241
+ }
242
+ }
243
+ }
244
+ const inside = (root, real) => real.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
245
+ /**
246
+ * Replace `<contextReal>/<dir>` with a vendor directory holding `manifestText` and `blobs`: written to a
247
+ * temporary sibling, then swapped in. Refuses a directory that resolves outside the context, a symlink,
248
+ * and an existing directory that is not a vendor directory (no manifest.json).
249
+ */
250
+ export async function writeVendorDir(contextReal, dir, manifestText, blobs) {
251
+ const target = path.join(contextReal, ...dir.split('/'));
252
+ const parent = path.dirname(target);
253
+ if (!inside(contextReal, await realpathLoose(target))) {
254
+ throw new SitefileError('E_PATH_ESCAPE', `vendor directory '${dir}' resolves outside the build context`);
255
+ }
256
+ const st = await fs.lstat(target).catch(() => undefined);
257
+ if (st?.isSymbolicLink())
258
+ throw new SitefileError('E_EXPORT_UNSAFE', `vendor directory '${dir}' is a symlink; refusing to replace it`);
259
+ if (st && !st.isDirectory())
260
+ throw new SitefileError('E_EXPORT_UNSAFE', `vendor directory '${dir}' exists and is not a directory`);
261
+ if (st) {
262
+ const names = await fs.readdir(target);
263
+ if (names.length > 0 && !names.includes(VENDOR_MANIFEST)) {
264
+ throw new SitefileError('E_EXPORT_UNSAFE', `'${dir}' exists and is not a sitefile vendor directory (no ${VENDOR_MANIFEST}); remove it or choose another --vendor-dir`);
265
+ }
266
+ }
267
+ await fs.mkdir(parent, { recursive: true }).catch((e) => {
268
+ throw new SitefileError('E_EXPORT', `cannot write vendor directory '${dir}': ${e.message}`, undefined, { cause: e });
269
+ });
270
+ const parentReal = await fs.realpath(parent);
271
+ if (!inside(contextReal, parentReal) && parentReal !== contextReal) {
272
+ throw new SitefileError('E_PATH_ESCAPE', `vendor directory '${dir}' resolves outside the build context`);
273
+ }
274
+ const tag = `${process.pid}-${randomBytes(4).toString('hex')}`;
275
+ const tmp = path.join(parent, `.${path.basename(target)}.sitefile-vendor-tmp-${tag}`);
276
+ const old = path.join(parent, `.${path.basename(target)}.sitefile-vendor-old-${tag}`);
277
+ try {
278
+ await fs.mkdir(path.join(tmp, 'blobs', 'sha256'), { recursive: true });
279
+ for (const [digest, bytes] of [...blobs].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
280
+ await fs.writeFile(path.join(tmp, 'blobs', 'sha256', digest.slice('sha256:'.length)), bytes);
281
+ }
282
+ await fs.writeFile(path.join(tmp, VENDOR_MANIFEST), manifestText);
283
+ if (st)
284
+ await fs.rename(target, old);
285
+ await fs.rename(tmp, target);
286
+ }
287
+ catch (e) {
288
+ await fs.rm(tmp, { recursive: true, force: true });
289
+ if (st && !(await fs.lstat(target).catch(() => undefined)))
290
+ await fs.rename(old, target).catch(() => undefined);
291
+ throw new SitefileError('E_EXPORT', `cannot write vendor directory '${dir}': ${e.message}`, undefined, { cause: e });
292
+ }
293
+ await fs.rm(old, { recursive: true, force: true });
294
+ return target;
295
+ }
296
+ //# sourceMappingURL=vendor.js.map
@@ -0,0 +1,44 @@
1
+ import { type SourceSpan } from '../errors.js';
2
+ import { Tree } from '../tree.js';
3
+ import type { ArchiveLimits } from './types.js';
4
+ /**
5
+ * Reader for unsigned Web Bundles, formats b1 and b2 (the WICG
6
+ * "draft-yasskin-wpack-bundled-exchanges" lineage and
7
+ * draft-ietf-wpack-bundled-responses). Layouts, as implemented by the WICG Go
8
+ * reference reader (go/bundle/decoder.go):
9
+ *
10
+ * b2: array(5) [ magic, version "b2\0\0", section-lengths, sections, length ]
11
+ * b1: array(6) [ magic, version "b1\0\0", primary URL (text), section-lengths, sections, length ]
12
+ *
13
+ * section-lengths is a byte string holding a CBOR array of (name, length) pairs;
14
+ * `sections` is an array with one item per pair; `responses` must be last. The
15
+ * trailing length is an 8-byte big-endian byte string equal to the bundle size.
16
+ * index = { url => value }: b2 value = [offset, length]; b1 value =
17
+ * [variants-value bstr, offset, length, ...] (empty variants-value: exactly one
18
+ * location). Offsets are counted from the first byte of the responses section's
19
+ * CBOR encoding. A response is array(2) [ bstr(CBOR map of bstr -> bstr incl.
20
+ * ":status"), body bstr ].
21
+ *
22
+ * Signatures, manifest and any unknown section are length-checked and ignored;
23
+ * nothing is verified or executed.
24
+ */
25
+ export interface BundleEntry {
26
+ url: string;
27
+ status: number;
28
+ headers: Record<string, string>;
29
+ body: Uint8Array;
30
+ }
31
+ export interface WebBundle {
32
+ version: 'b1' | 'b2';
33
+ primaryUrl: string | undefined;
34
+ entries: BundleEntry[];
35
+ }
36
+ export declare function isWebBundle(bytes: Uint8Array): boolean;
37
+ export declare function readWebBundle(bytes: Uint8Array, origin: string, limits: ArchiveLimits, span?: SourceSpan): WebBundle;
38
+ /**
39
+ * Turn a bundle into a Tree. Only status-200 entries become files. When all
40
+ * such URLs share one origin the artifact path is the URL path; otherwise each
41
+ * path is prefixed with `/<host[:port]>`. A path ending in `/` maps to
42
+ * `<path>index.html`; segments are percent-decoded.
43
+ */
44
+ export declare function webBundleToTree(bundle: WebBundle, origin: string, span?: SourceSpan): Tree;