@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.
Files changed (178) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +21 -0
  3. package/README.md +513 -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 +120 -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 +119 -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/network.d.ts +34 -0
  37. package/lib/src/cli/network.js +56 -0
  38. package/lib/src/cli/outdated.d.ts +5 -0
  39. package/lib/src/cli/outdated.js +99 -0
  40. package/lib/src/cli/registry.d.ts +2 -0
  41. package/lib/src/cli/registry.js +127 -0
  42. package/lib/src/cli/serve-worker.d.ts +2 -0
  43. package/lib/src/cli/serve-worker.js +42 -0
  44. package/lib/src/cli/servers.d.ts +15 -0
  45. package/lib/src/cli/servers.js +467 -0
  46. package/lib/src/cli/vendor.d.ts +2 -0
  47. package/lib/src/cli/vendor.js +97 -0
  48. package/lib/src/cli/watch.d.ts +26 -0
  49. package/lib/src/cli/watch.js +81 -0
  50. package/lib/src/cli.d.ts +12 -0
  51. package/lib/src/cli.js +522 -0
  52. package/lib/src/csv.d.ts +27 -0
  53. package/lib/src/csv.js +117 -0
  54. package/lib/src/deflate.d.ts +76 -0
  55. package/lib/src/deflate.js +604 -0
  56. package/lib/src/digest.d.ts +3 -0
  57. package/lib/src/digest.js +7 -0
  58. package/lib/src/errors.d.ts +56 -0
  59. package/lib/src/errors.js +29 -0
  60. package/lib/src/executor.d.ts +393 -0
  61. package/lib/src/executor.js +2201 -0
  62. package/lib/src/explain.d.ts +64 -0
  63. package/lib/src/explain.js +445 -0
  64. package/lib/src/export-formats.d.ts +23 -0
  65. package/lib/src/export-formats.js +63 -0
  66. package/lib/src/exporter.d.ts +103 -0
  67. package/lib/src/exporter.js +676 -0
  68. package/lib/src/fingerprint.d.ts +40 -0
  69. package/lib/src/fingerprint.js +168 -0
  70. package/lib/src/frontmatter.d.ts +22 -0
  71. package/lib/src/frontmatter.js +45 -0
  72. package/lib/src/index.d.ts +13 -0
  73. package/lib/src/index.js +13 -0
  74. package/lib/src/language/ast.d.ts +360 -0
  75. package/lib/src/language/ast.js +57 -0
  76. package/lib/src/language/parser.d.ts +3 -0
  77. package/lib/src/language/parser.js +612 -0
  78. package/lib/src/language/scanner.d.ts +110 -0
  79. package/lib/src/language/scanner.js +449 -0
  80. package/lib/src/language/validate.d.ts +13 -0
  81. package/lib/src/language/validate.js +367 -0
  82. package/lib/src/links.d.ts +79 -0
  83. package/lib/src/links.js +583 -0
  84. package/lib/src/lint.d.ts +10 -0
  85. package/lib/src/lint.js +223 -0
  86. package/lib/src/markdown.d.ts +29 -0
  87. package/lib/src/markdown.js +612 -0
  88. package/lib/src/media.d.ts +82 -0
  89. package/lib/src/media.js +650 -0
  90. package/lib/src/memo.d.ts +57 -0
  91. package/lib/src/memo.js +93 -0
  92. package/lib/src/oci.d.ts +178 -0
  93. package/lib/src/oci.js +380 -0
  94. package/lib/src/outdated.d.ts +46 -0
  95. package/lib/src/outdated.js +195 -0
  96. package/lib/src/pages.d.ts +10 -0
  97. package/lib/src/pages.js +54 -0
  98. package/lib/src/paths.d.ts +39 -0
  99. package/lib/src/paths.js +101 -0
  100. package/lib/src/planner.d.ts +297 -0
  101. package/lib/src/planner.js +410 -0
  102. package/lib/src/plugins/cache.d.ts +96 -0
  103. package/lib/src/plugins/cache.js +325 -0
  104. package/lib/src/plugins/exec.d.ts +51 -0
  105. package/lib/src/plugins/exec.js +144 -0
  106. package/lib/src/plugins/host.d.ts +75 -0
  107. package/lib/src/plugins/host.js +277 -0
  108. package/lib/src/plugins/wasm.d.ts +23 -0
  109. package/lib/src/plugins/wasm.js +249 -0
  110. package/lib/src/profile.d.ts +42 -0
  111. package/lib/src/profile.js +68 -0
  112. package/lib/src/ref.d.ts +40 -0
  113. package/lib/src/ref.js +79 -0
  114. package/lib/src/registry-auth.d.ts +32 -0
  115. package/lib/src/registry-auth.js +151 -0
  116. package/lib/src/registry-cache.d.ts +71 -0
  117. package/lib/src/registry-cache.js +333 -0
  118. package/lib/src/registry-ops.d.ts +51 -0
  119. package/lib/src/registry-ops.js +248 -0
  120. package/lib/src/registry.d.ts +115 -0
  121. package/lib/src/registry.js +524 -0
  122. package/lib/src/render.d.ts +134 -0
  123. package/lib/src/render.js +576 -0
  124. package/lib/src/resources/archive.d.ts +11 -0
  125. package/lib/src/resources/archive.js +306 -0
  126. package/lib/src/resources/bundle-data.d.ts +16 -0
  127. package/lib/src/resources/bundle-data.js +50 -0
  128. package/lib/src/resources/convert.d.ts +12 -0
  129. package/lib/src/resources/convert.js +63 -0
  130. package/lib/src/resources/documents.d.ts +35 -0
  131. package/lib/src/resources/documents.js +122 -0
  132. package/lib/src/resources/git.d.ts +103 -0
  133. package/lib/src/resources/git.js +720 -0
  134. package/lib/src/resources/https.d.ts +74 -0
  135. package/lib/src/resources/https.js +413 -0
  136. package/lib/src/resources/ignore.d.ts +33 -0
  137. package/lib/src/resources/ignore.js +119 -0
  138. package/lib/src/resources/local.d.ts +64 -0
  139. package/lib/src/resources/local.js +248 -0
  140. package/lib/src/resources/lockfile.d.ts +52 -0
  141. package/lib/src/resources/lockfile.js +172 -0
  142. package/lib/src/resources/netpolicy.d.ts +6 -0
  143. package/lib/src/resources/netpolicy.js +82 -0
  144. package/lib/src/resources/network.d.ts +70 -0
  145. package/lib/src/resources/network.js +304 -0
  146. package/lib/src/resources/types.d.ts +69 -0
  147. package/lib/src/resources/types.js +13 -0
  148. package/lib/src/resources/vendor.d.ts +94 -0
  149. package/lib/src/resources/vendor.js +296 -0
  150. package/lib/src/resources/wbn-reader.d.ts +44 -0
  151. package/lib/src/resources/wbn-reader.js +302 -0
  152. package/lib/src/schema.d.ts +25 -0
  153. package/lib/src/schema.js +169 -0
  154. package/lib/src/secrets.d.ts +21 -0
  155. package/lib/src/secrets.js +103 -0
  156. package/lib/src/serve-rules.d.ts +29 -0
  157. package/lib/src/serve-rules.js +123 -0
  158. package/lib/src/server.d.ts +45 -0
  159. package/lib/src/server.js +246 -0
  160. package/lib/src/servers.d.ts +85 -0
  161. package/lib/src/servers.js +225 -0
  162. package/lib/src/site.d.ts +60 -0
  163. package/lib/src/site.js +169 -0
  164. package/lib/src/store.d.ts +151 -0
  165. package/lib/src/store.js +596 -0
  166. package/lib/src/tree.d.ts +50 -0
  167. package/lib/src/tree.js +134 -0
  168. package/lib/src/values.d.ts +7 -0
  169. package/lib/src/values.js +37 -0
  170. package/lib/src/vendor.d.ts +19 -0
  171. package/lib/src/vendor.js +145 -0
  172. package/lib/src/watch.d.ts +44 -0
  173. package/lib/src/watch.js +137 -0
  174. package/lib/src/wbn.d.ts +26 -0
  175. package/lib/src/wbn.js +241 -0
  176. package/lib/src/yaml.d.ts +34 -0
  177. package/lib/src/yaml.js +562 -0
  178. package/package.json +59 -4
@@ -0,0 +1,169 @@
1
+ // Frontmatter and data schemas: `IMPORT posts FROM ./posts AS markdown SCHEMA ./post.schema.yaml`.
2
+ //
3
+ // A schema is a small, deterministic subset of JSON Schema, written as a YAML or JSON mapping:
4
+ //
5
+ // required: [title, date] # keys every document must have (null counts as missing)
6
+ // properties: # key -> rule; keys not listed are not checked
7
+ // title: string # a type name ...
8
+ // tags: { type: list, items: string }
9
+ // status: { enum: [draft, published] }
10
+ //
11
+ // Types: string, number, boolean, date (YYYY-MM-DD or an RFC 3339 date-time, as FEED reads it) and list
12
+ // (`items` gives the type of its elements). `enum` lists the allowed strings, numbers or booleans (for a list,
13
+ // the allowed elements). In a CSV import every cell is a string, so `number` also accepts a numeric string and
14
+ // `boolean` the strings `true` and `false`, enum entries match their text, and an empty cell counts as missing. docs/LANGUAGE.md has the rules.
15
+ import { SitefileError } from './errors.js';
16
+ import { parseFeedDate } from './site.js';
17
+ const TYPES = ['string', 'number', 'boolean', 'date', 'list'];
18
+ const ITEM_TYPES = ['string', 'number', 'boolean', 'date'];
19
+ const NUMERIC = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
20
+ /** What a value of each type looks like, for messages. */
21
+ const EXPECTED = {
22
+ string: 'a string',
23
+ number: 'a number',
24
+ boolean: 'true or false',
25
+ date: 'a date (YYYY-MM-DD or an RFC 3339 date-time)',
26
+ list: 'a list',
27
+ };
28
+ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
29
+ /** Compile a parsed schema file. Errors are E_SCHEMA at `span` (the SCHEMA clause), prefixed with `what`. */
30
+ export function compileSchema(value, label, what, span) {
31
+ const fail = (m) => {
32
+ throw new SitefileError('E_SCHEMA', `${what}: schema '${label}' ${m}`, span);
33
+ };
34
+ if (!isObject(value))
35
+ fail('must be a mapping (with required and/or properties)');
36
+ const root = value;
37
+ for (const k of Object.keys(root)) {
38
+ if (k !== 'required' && k !== 'properties')
39
+ fail(`has an unknown key '${k}' (expected required or properties)`);
40
+ }
41
+ const required = [];
42
+ if (root['required'] !== undefined) {
43
+ const r = root['required'];
44
+ if (!Array.isArray(r) || !r.every((k) => typeof k === 'string' && k !== ''))
45
+ fail("needs 'required' to be a list of key names");
46
+ for (const k of r)
47
+ if (!required.includes(k))
48
+ required.push(k);
49
+ }
50
+ const properties = [];
51
+ if (root['properties'] !== undefined) {
52
+ const p = root['properties'];
53
+ if (!isObject(p))
54
+ fail("needs 'properties' to be a mapping of key names to rules");
55
+ for (const [key, rule] of Object.entries(p)) {
56
+ const at = `property '${key}':`;
57
+ const typeName = (t, allowed, kind) => {
58
+ if (typeof t !== 'string' || !allowed.includes(t)) {
59
+ const shown = typeof t === 'string' ? ` '${t}'` : '';
60
+ fail(`${at} unknown ${kind}${shown} (expected ${allowed.slice(0, -1).join(', ')} or ${allowed[allowed.length - 1]})`);
61
+ }
62
+ return t;
63
+ };
64
+ if (typeof rule === 'string') {
65
+ properties.push({ key, type: typeName(rule, TYPES, 'type') });
66
+ continue;
67
+ }
68
+ if (!isObject(rule))
69
+ fail(`${at} expected a type name or a mapping with type, items or enum`);
70
+ const r = rule;
71
+ for (const k of Object.keys(r)) {
72
+ if (k !== 'type' && k !== 'items' && k !== 'enum')
73
+ fail(`${at} unknown key '${k}' (expected type, items or enum)`);
74
+ }
75
+ const type = r['type'] !== undefined ? typeName(r['type'], TYPES, 'type') : undefined;
76
+ let items;
77
+ if (r['items'] !== undefined) {
78
+ if (type !== 'list')
79
+ fail(`${at} items needs type list`);
80
+ items = typeName(r['items'], ITEM_TYPES, 'items type');
81
+ }
82
+ let values;
83
+ if (r['enum'] !== undefined) {
84
+ const e = r['enum'];
85
+ if (!Array.isArray(e) || e.length === 0 || !e.every((x) => typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean')) {
86
+ fail(`${at} enum must be a non-empty list of strings, numbers or booleans`);
87
+ }
88
+ values = e;
89
+ }
90
+ properties.push({ key, ...(type ? { type } : {}), ...(items ? { items } : {}), ...(values ? { enum: values } : {}) });
91
+ }
92
+ }
93
+ return { label, required, properties };
94
+ }
95
+ /** How a value is shown in a mismatch message. */
96
+ function describe(v) {
97
+ if (v === null)
98
+ return 'null';
99
+ if (typeof v === 'string')
100
+ return `string '${v}'`;
101
+ if (typeof v === 'number')
102
+ return `number ${String(v)}`;
103
+ if (typeof v === 'boolean')
104
+ return `boolean ${String(v)}`;
105
+ if (Array.isArray(v))
106
+ return 'a list';
107
+ return 'an object';
108
+ }
109
+ function hasType(v, t, csv) {
110
+ switch (t) {
111
+ case 'string':
112
+ return typeof v === 'string';
113
+ case 'number':
114
+ return typeof v === 'number' || (csv && typeof v === 'string' && NUMERIC.test(v.trim()));
115
+ case 'boolean':
116
+ return typeof v === 'boolean' || (csv && (v === 'true' || v === 'false'));
117
+ case 'date':
118
+ return typeof v === 'string' && parseFeedDate(v) !== undefined;
119
+ case 'list':
120
+ return Array.isArray(v);
121
+ }
122
+ }
123
+ function inEnum(v, values, csv) {
124
+ return values.some((e) => e === v || (csv && typeof v === 'string' && v === String(e)));
125
+ }
126
+ /**
127
+ * Check an import's value against a schema: each element of a list, or a single document. Every element must be
128
+ * an object (E_TYPE otherwise). The first mismatch fails with E_SCHEMA at `span`, naming the document (its `path`,
129
+ * or `#n` for the n-th element), the key, what was expected and what was found.
130
+ */
131
+ export function checkSchema(value, schema, what, csv, span) {
132
+ const docs = Array.isArray(value) ? value : [value];
133
+ docs.forEach((doc, i) => {
134
+ if (!isObject(doc)) {
135
+ throw new SitefileError('E_TYPE', `${what}: SCHEMA checks documents (objects), but element ${i + 1} is ${describe(doc)}`, span);
136
+ }
137
+ const name = typeof doc['path'] === 'string' ? doc['path'] : `#${i + 1}`;
138
+ const fail = (m) => {
139
+ throw new SitefileError('E_SCHEMA', `${what}: document '${name}' ${m}`, span);
140
+ };
141
+ // A CSV cell is always there: an empty one counts as missing.
142
+ const missing = (v) => v === undefined || v === null || (csv && v === '');
143
+ for (const key of schema.required) {
144
+ if (missing(Object.hasOwn(doc, key) ? doc[key] : undefined))
145
+ fail(`has no '${key}' (required by schema '${schema.label}')`);
146
+ }
147
+ for (const rule of schema.properties) {
148
+ const v = Object.hasOwn(doc, rule.key) ? doc[rule.key] : undefined;
149
+ if (missing(v))
150
+ continue;
151
+ const enumText = (values) => values.map((e) => String(e)).join(', ');
152
+ if (rule.type !== undefined && !hasType(v, rule.type, csv))
153
+ fail(`has '${rule.key}' ${describe(v)}; it should be ${EXPECTED[rule.type]}`);
154
+ if (rule.type === 'list') {
155
+ v.forEach((item, j) => {
156
+ if (rule.items !== undefined && !hasType(item, rule.items, csv)) {
157
+ fail(`has '${rule.key}' item ${j + 1} ${describe(item)}; it should be ${EXPECTED[rule.items]}`);
158
+ }
159
+ if (rule.enum !== undefined && !inEnum(item, rule.enum, csv))
160
+ fail(`has '${rule.key}' item ${j + 1} ${describe(item)}; it should be one of ${enumText(rule.enum)}`);
161
+ });
162
+ }
163
+ else if (rule.enum !== undefined && !inEnum(v, rule.enum, csv)) {
164
+ fail(`has '${rule.key}' ${describe(v)}; it should be one of ${enumText(rule.enum)}`);
165
+ }
166
+ }
167
+ });
168
+ }
169
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,21 @@
1
+ export type Secrets = ReadonlyMap<string, string>;
2
+ /**
3
+ * Parse `--secret` flags: `id=<id>,env=<VAR>` (an environment variable) or `id=<id>,src=<file>` (a
4
+ * file, relative to the working directory; one trailing newline is dropped). Values are checked to
5
+ * be usable as an HTTP header value. Messages never include a value.
6
+ */
7
+ export declare function loadSecrets(flags: readonly string[], env?: NodeJS.ProcessEnv): Promise<Map<string, string>>;
8
+ /** Replace every occurrence of a secret value (longest first) with `***`. */
9
+ export declare function redactSecrets(text: string, secrets: Secrets | undefined): string;
10
+ /** A SitefileError whose message has the secrets redacted (the same error when nothing changes). */
11
+ export declare function redactError(e: unknown, secrets: Secrets | undefined): unknown;
12
+ /** The request headers of an import's SECRET clauses; a secret that was not given is E_SECRET. */
13
+ export declare function secretHeaders(spec: {
14
+ readonly name: string;
15
+ readonly secrets?: readonly {
16
+ readonly id: string;
17
+ readonly header: string;
18
+ readonly bearer: boolean;
19
+ }[];
20
+ readonly span: import('./errors.js').SourceSpan;
21
+ }, secrets: Secrets | undefined): Record<string, string> | undefined;
@@ -0,0 +1,103 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { SitefileError, isSitefileError } from './errors.js';
3
+ /**
4
+ * Build secrets (issue #85), modelled on Docker's `--secret id=…,env=…|src=…`: values that remote
5
+ * IMPORTs send as HTTP request headers (`IMPORT … SECRET <id> AS HEADER <Name>` / `AS BEARER`).
6
+ *
7
+ * A secret value lives only in memory: it is sent to the IMPORT URL's own origin (never after a
8
+ * redirect to another one) and never becomes part of the output, a cache or memo key, Sitefile.lock,
9
+ * a vendor manifest, provenance, a --profile trace, a warning or an error message. As a last line of
10
+ * defence, warnings and errors of a build that has secrets are passed through {@link redactSecrets}.
11
+ */
12
+ const ID_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
13
+ /** Visible ASCII, space and tab: what every HTTP client sends unchanged in a header value. */
14
+ const HEADER_VALUE_RE = /^[\t\x20-\x7e]+$/;
15
+ function usage(message) {
16
+ return new SitefileError('E_USAGE', message);
17
+ }
18
+ /**
19
+ * Parse `--secret` flags: `id=<id>,env=<VAR>` (an environment variable) or `id=<id>,src=<file>` (a
20
+ * file, relative to the working directory; one trailing newline is dropped). Values are checked to
21
+ * be usable as an HTTP header value. Messages never include a value.
22
+ */
23
+ export async function loadSecrets(flags, env = process.env) {
24
+ const out = new Map();
25
+ for (const flag of flags) {
26
+ const fields = new Map();
27
+ for (const part of flag.split(',')) {
28
+ const eq = part.indexOf('=');
29
+ const key = eq < 0 ? part : part.slice(0, eq);
30
+ if (eq < 0 || (key !== 'id' && key !== 'env' && key !== 'src')) {
31
+ throw usage('--secret expects id=<id>,env=<VAR> or id=<id>,src=<file>'); // never echo the flag: it might be the secret itself
32
+ }
33
+ if (fields.has(key))
34
+ throw usage(`--secret: '${key}' given twice`);
35
+ fields.set(key, part.slice(eq + 1));
36
+ }
37
+ const id = fields.get('id');
38
+ if (id === undefined || !ID_RE.test(id))
39
+ throw usage(`--secret needs id=<id> (letters, digits and _, not starting with a digit)`);
40
+ if (out.has(id))
41
+ throw usage(`--secret id=${id} given twice`);
42
+ const name = fields.get('env');
43
+ const src = fields.get('src');
44
+ if ((name === undefined) === (src === undefined))
45
+ throw usage(`--secret id=${id}: give exactly one of env=<VAR> or src=<file>`);
46
+ let value;
47
+ if (name !== undefined) {
48
+ const v = env[name];
49
+ if (v === undefined)
50
+ throw new SitefileError('E_SECRET', `--secret id=${id}: environment variable ${name} is not set`);
51
+ value = v;
52
+ }
53
+ else {
54
+ let bytes;
55
+ try {
56
+ bytes = await readFile(src);
57
+ }
58
+ catch (e) {
59
+ throw new SitefileError('E_SECRET', `--secret id=${id}: cannot read '${src}'`, undefined, { cause: isSitefileError(e) ? e : undefined });
60
+ }
61
+ value = bytes.toString('latin1').replace(/\r?\n$/, '');
62
+ }
63
+ if (value === '')
64
+ throw new SitefileError('E_SECRET', `--secret id=${id} is empty`);
65
+ if (!HEADER_VALUE_RE.test(value)) {
66
+ throw new SitefileError('E_SECRET', `--secret id=${id} cannot be sent in an HTTP header (only printable ASCII, spaces and tabs)`);
67
+ }
68
+ out.set(id, value);
69
+ }
70
+ return out;
71
+ }
72
+ /** Replace every occurrence of a secret value (longest first) with `***`. */
73
+ export function redactSecrets(text, secrets) {
74
+ if (!secrets || secrets.size === 0)
75
+ return text;
76
+ let out = text;
77
+ for (const v of [...secrets.values()].sort((a, b) => b.length - a.length))
78
+ if (v.length > 0)
79
+ out = out.split(v).join('***');
80
+ return out;
81
+ }
82
+ /** A SitefileError whose message has the secrets redacted (the same error when nothing changes). */
83
+ export function redactError(e, secrets) {
84
+ if (!isSitefileError(e) || !secrets || secrets.size === 0)
85
+ return e;
86
+ const detail = redactSecrets(e.detail, secrets);
87
+ return detail === e.detail ? e : new SitefileError(e.code, detail, e.span, e.needs ? { needs: e.needs } : undefined);
88
+ }
89
+ /** The request headers of an import's SECRET clauses; a secret that was not given is E_SECRET. */
90
+ export function secretHeaders(spec, secrets) {
91
+ if (!spec.secrets?.length)
92
+ return undefined;
93
+ const headers = {};
94
+ for (const s of spec.secrets) {
95
+ const v = secrets?.get(s.id);
96
+ if (v === undefined) {
97
+ throw new SitefileError('E_SECRET', `IMPORT ${spec.name}: secret '${s.id}' was not given; pass --secret id=${s.id},env=<VAR> or --secret id=${s.id},src=<file>`, spec.span);
98
+ }
99
+ headers[s.header.toLowerCase()] = s.bearer ? `Bearer ${v}` : v;
100
+ }
101
+ return headers;
102
+ }
103
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1,29 @@
1
+ /** A `_redirects` rule `serve` understands. */
2
+ export interface ServeRedirect {
3
+ /** Decoded path without a trailing slash (`/` for the root); for a splat rule, the prefix before `/*` (`''` for `/*`). */
4
+ readonly from: string;
5
+ readonly splat: boolean;
6
+ readonly to: string;
7
+ readonly status: number;
8
+ /** `301!`: applies even when a file exists at the path. */
9
+ readonly force: boolean;
10
+ }
11
+ export interface ServeHeaders {
12
+ readonly pattern: string;
13
+ /** Lowercase names. */
14
+ readonly headers: ReadonlyArray<readonly [string, string]>;
15
+ }
16
+ /**
17
+ * Parse `_redirects`: `from to [status[!]]` per line, `#` comments. Supported: exact paths and a
18
+ * trailing `/*` with `:splat` in the target; statuses 301, 302, 303, 307, 308 (redirect), 200
19
+ * (rewrite) and 404. Other lines (placeholders, conditions, query matching) are ignored.
20
+ */
21
+ export declare function parseRedirectRules(text: string): ServeRedirect[];
22
+ /** Parse `_headers`: a path pattern line (`*` matches anything), then indented `Name: value` lines. */
23
+ export declare function parseHeaderRules(text: string): ServeHeaders[];
24
+ /** Glob match where `*` matches any run of characters (including `/`). */
25
+ export declare function globMatch(pattern: string, s: string): boolean;
26
+ /** Every matching block's headers; a name set by several blocks gets its values joined with `, `. */
27
+ export declare function headersFor(rules: readonly ServeHeaders[], path: string): Record<string, string>;
28
+ /** The remainder a rule matches for `path` (`''` for the prefix itself), or undefined. */
29
+ export declare function matchRedirect(r: ServeRedirect, path: string): string | undefined;
@@ -0,0 +1,123 @@
1
+ // `_redirects` and `_headers` as `sitefile serve` reads them (docs/CLI.md, "serve").
2
+ // Ported as the rules section of rust/src/server.rs.
3
+ import { safeSegments } from './server.js';
4
+ const HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
5
+ /** Headers the server owns; `_headers` cannot set them. */
6
+ const PROTECTED_HEADERS = new Set(['content-length', 'transfer-encoding', 'connection', 'keep-alive', 'etag', 'location']);
7
+ const SERVE_STATUSES = new Set([200, 301, 302, 303, 307, 308, 404]);
8
+ /**
9
+ * Parse `_redirects`: `from to [status[!]]` per line, `#` comments. Supported: exact paths and a
10
+ * trailing `/*` with `:splat` in the target; statuses 301, 302, 303, 307, 308 (redirect), 200
11
+ * (rewrite) and 404. Other lines (placeholders, conditions, query matching) are ignored.
12
+ */
13
+ export function parseRedirectRules(text) {
14
+ const out = [];
15
+ for (const line of text.split('\n')) {
16
+ const t = line.trim();
17
+ if (t === '' || t.startsWith('#'))
18
+ continue;
19
+ const fields = t.split(/[ \t]+/);
20
+ if (fields.length < 2 || fields.length > 3)
21
+ continue;
22
+ const from = fields[0];
23
+ const to = fields[1];
24
+ const st = fields[2] ?? '301';
25
+ const force = st.endsWith('!');
26
+ const code = force ? st.slice(0, -1) : st;
27
+ if (!/^[0-9]{3}$/.test(code) || !SERVE_STATUSES.has(Number(code)))
28
+ continue;
29
+ const status = Number(code);
30
+ if (!/^[\x21-\x7e]+$/.test(to) || !(to.startsWith('/') || /^https?:\/\/[^/]/i.test(to)))
31
+ continue;
32
+ if ((status === 200 || status === 404) && !to.startsWith('/'))
33
+ continue;
34
+ if (!from.startsWith('/'))
35
+ continue;
36
+ const splat = from === '/*' || from.endsWith('/*');
37
+ const prefix = splat ? from.slice(0, -2) : from;
38
+ if (prefix.includes('*') || prefix.split('/').some((seg) => seg.startsWith(':')))
39
+ continue;
40
+ const segs = safeSegments(prefix === '' ? '/' : prefix);
41
+ if (!segs)
42
+ continue;
43
+ const decoded = '/' + segs.join('/');
44
+ out.push({ from: splat && decoded === '/' ? '' : decoded, splat, to, status, force });
45
+ }
46
+ return out;
47
+ }
48
+ /** Parse `_headers`: a path pattern line (`*` matches anything), then indented `Name: value` lines. */
49
+ export function parseHeaderRules(text) {
50
+ const out = [];
51
+ let cur;
52
+ for (const raw of text.split('\n')) {
53
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
54
+ const t = line.trim();
55
+ if (t === '' || t.startsWith('#'))
56
+ continue;
57
+ if (line[0] === ' ' || line[0] === '\t') {
58
+ const i = t.indexOf(':');
59
+ if (!cur || i <= 0)
60
+ continue;
61
+ const name = t.slice(0, i).trim().toLowerCase();
62
+ const value = t.slice(i + 1).trim();
63
+ if (!HEADER_TOKEN.test(name) || !/^[\t\x20-\x7e]*$/.test(value) || PROTECTED_HEADERS.has(name))
64
+ continue;
65
+ cur.headers.push([name, value]);
66
+ continue;
67
+ }
68
+ cur = t.startsWith('/') ? { pattern: t, headers: [] } : undefined;
69
+ if (cur)
70
+ out.push(cur);
71
+ }
72
+ return out;
73
+ }
74
+ /** Glob match where `*` matches any run of characters (including `/`). */
75
+ export function globMatch(pattern, s) {
76
+ let p = 0;
77
+ let i = 0;
78
+ let star = -1;
79
+ let mark = 0;
80
+ while (i < s.length) {
81
+ if (p < pattern.length && pattern[p] !== '*' && pattern[p] === s[i]) {
82
+ p++;
83
+ i++;
84
+ }
85
+ else if (p < pattern.length && pattern[p] === '*') {
86
+ star = p++;
87
+ mark = i;
88
+ }
89
+ else if (star >= 0) {
90
+ p = star + 1;
91
+ i = ++mark;
92
+ }
93
+ else {
94
+ return false;
95
+ }
96
+ }
97
+ while (pattern[p] === '*')
98
+ p++;
99
+ return p === pattern.length;
100
+ }
101
+ /** Every matching block's headers; a name set by several blocks gets its values joined with `, `. */
102
+ export function headersFor(rules, path) {
103
+ // No prototype: a header named `__proto__` is an ordinary name (as in the Rust port).
104
+ const out = Object.create(null);
105
+ for (const r of rules) {
106
+ if (!globMatch(r.pattern, path))
107
+ continue;
108
+ for (const [k, v] of r.headers)
109
+ out[k] = out[k] === undefined ? v : `${out[k]}, ${v}`;
110
+ }
111
+ return out;
112
+ }
113
+ /** The remainder a rule matches for `path` (`''` for the prefix itself), or undefined. */
114
+ export function matchRedirect(r, path) {
115
+ if (!r.splat)
116
+ return path === r.from ? '' : undefined;
117
+ if (r.from === '')
118
+ return path.slice(1);
119
+ if (path === r.from)
120
+ return '';
121
+ return path.startsWith(r.from + '/') ? path.slice(r.from.length + 1) : undefined;
122
+ }
123
+ //# sourceMappingURL=serve-rules.js.map
@@ -0,0 +1,45 @@
1
+ import http from 'node:http';
2
+ import type { Tree } from './tree.js';
3
+ export interface SiteServerOptions {
4
+ /** Fall back to `/index.html` for unknown extension-less paths (single-page apps). */
5
+ readonly spa?: boolean;
6
+ /** One access-log line per request: `METHOD /path STATUS BYTES MSms`. */
7
+ readonly log?: (line: string) => void;
8
+ /** Sent as `x-sitefile-server` so `servers` can tell its own servers from a reused port. */
9
+ readonly serverId?: string;
10
+ /**
11
+ * `serve --watch`: answer {@link RELOAD_PATH} with an event stream and add
12
+ * {@link RELOAD_SCRIPT} to HTML responses. Never set otherwise, and never written to exports.
13
+ */
14
+ readonly liveReload?: LiveReload;
15
+ }
16
+ /** Reserved for the live-reload event stream while `serve --watch` runs. */
17
+ export declare const RELOAD_PATH = "/__sitefile/reload";
18
+ /** Added to served HTML (before the last `</body>`, else at the end) under `serve --watch`. */
19
+ export declare const RELOAD_SCRIPT = "<script>new EventSource(\"/__sitefile/reload\").onmessage=function(){location.reload()}</script>";
20
+ /** Open live-reload streams; {@link LiveReload.reload} tells every page to reload. */
21
+ export declare class LiveReload {
22
+ private readonly clients;
23
+ /** Hold `res` open as a text/event-stream until the client goes away. */
24
+ attach(res: http.ServerResponse, headers: Record<string, string>): void;
25
+ reload(): void;
26
+ get size(): number;
27
+ }
28
+ /** `html` with {@link RELOAD_SCRIPT} inserted before the last `</body` (ASCII case-insensitive), else appended. */
29
+ export declare function injectReloadScript(html: Uint8Array): Uint8Array;
30
+ /** Decode a request path into safe segments, or undefined when it is malformed or unsafe. */
31
+ export declare function safeSegments(rawPath: string): string[] | undefined;
32
+ /**
33
+ * An HTTP server for an immutable Tree. GET and HEAD only; nothing outside the tree is
34
+ * ever reachable because lookups are exact map keys built from validated segments.
35
+ * Honours `/404.html`, `/_redirects` and `/_headers` (which it never serves), and answers
36
+ * `/about` with `/about.html` when there is no such file or directory.
37
+ */
38
+ export declare function createSiteServer(site: Tree | (() => Tree), options?: SiteServerOptions): http.Server;
39
+ /** Listen on host:port (port 0 = any free port); EADDRINUSE and friends become E_SERVER. */
40
+ export declare function listen(server: http.Server, host: string, port: number): Promise<{
41
+ host: string;
42
+ port: number;
43
+ }>;
44
+ export declare function closeServer(server: http.Server): Promise<void>;
45
+ export declare function formatUrl(host: string, port: number): string;