@hollware/core 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -55,6 +55,170 @@ var Problems = class {
55
55
  }
56
56
  };
57
57
  //#endregion
58
+ //#region src/config/values.ts
59
+ /** A value with keys: not null, not an array. */
60
+ function isRecord$3(value) {
61
+ return typeof value === "object" && value !== null && !Array.isArray(value);
62
+ }
63
+ /** A string as the messages show it: quoted, with what could break a line escaped. */
64
+ function quote$1(text) {
65
+ return JSON.stringify(text);
66
+ }
67
+ /** Text of an error that someone else wrote, on one line, so that a problem stays one line. */
68
+ function oneLine(text) {
69
+ return text.replace(/\s+/g, " ").trim();
70
+ }
71
+ /** What a value is, for a message: `"es"`, `42`, `null`, `an array`. */
72
+ function found(value) {
73
+ if (value === null) return "null";
74
+ switch (typeof value) {
75
+ case "string": return quote$1(value);
76
+ case "number":
77
+ case "boolean":
78
+ case "bigint": return String(value);
79
+ case "function": return "a function";
80
+ case "symbol": return "a symbol";
81
+ default: return Array.isArray(value) ? "an array" : "an object";
82
+ }
83
+ }
84
+ /** `is 42; it must be a string`, or `is missing; …` when the value is not there. */
85
+ function wrongType(value, expected) {
86
+ return `${value === void 0 ? "is missing" : `is ${found(value)}`}; it must be ${expected}`;
87
+ }
88
+ /** An optional string: `null` when it is absent, and a problem when it is anything else. */
89
+ function readOptionalString(path, value, fix, problems) {
90
+ if (value === void 0) return null;
91
+ if (typeof value === "string") return value;
92
+ problems.add(path, wrongType(value, "a string"), fix);
93
+ return null;
94
+ }
95
+ const PLAIN_KEY = /^[A-Za-z0-9_-]+$/;
96
+ /** The path of a key inside `parent`: `sites.demo`, or `sites["my site"]` for an odd key. */
97
+ function keyPath(parent, key) {
98
+ return PLAIN_KEY.test(key) ? `${parent}.${key}` : `${parent}[${quote$1(key)}]`;
99
+ }
100
+ /** The path of an item of an array: `sites.demo.locales[1]`. */
101
+ function itemPath(parent, index) {
102
+ return `${parent}[${index}]`;
103
+ }
104
+ //#endregion
105
+ //#region src/config/events.ts
106
+ /** The name of a Worker secret or variable: what `wrangler secret put` takes. */
107
+ const BINDING_NAME = /^[A-Z][A-Z0-9_]*$/;
108
+ /**
109
+ * The Worker secrets a definition may not name: Hollware's own (its master secret, its analytics
110
+ * token) and Cloudflare's. A site's definition is written by its owner or an agent with
111
+ * schema:admin, who need not hold the account, so it names only secrets made for its webhooks.
112
+ */
113
+ const RESERVED_SECRET = /^(HOLLWARE|CLOUDFLARE|CF)_/;
114
+ /**
115
+ * An IndexNow key, which is public by design (search engines fetch it at `/<key>.txt`): the
116
+ * definition holds the key itself, never the name of a Worker secret, so no value of the Worker
117
+ * can be sent to IndexNow (T-69 of docs/security/threat-model.md).
118
+ */
119
+ const INDEXNOW_KEY = /^[A-Za-z0-9-]{8,128}$/;
120
+ /** A subscriber's name: lowercase words joined by hyphens. */
121
+ const NAME$1 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
122
+ /** The public URL of a site: an absolute http or https URL with no path, query or fragment. */
123
+ function readSiteUrl(path, raw, problems) {
124
+ if (raw === void 0) return null;
125
+ const fix = "Write the public origin of the site, such as \"https://example.com\".";
126
+ if (typeof raw !== "string" || !URL.canParse(raw)) {
127
+ problems.add(path, wrongType(raw, "an absolute URL"), fix);
128
+ return null;
129
+ }
130
+ const url = new URL(raw);
131
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
132
+ problems.add(path, `${quote$1(raw)} is not an origin`, fix);
133
+ return null;
134
+ }
135
+ return url.origin;
136
+ }
137
+ /** The origins of a site's pages that may read its published content from the browser. */
138
+ function readCors(path, raw, problems) {
139
+ if (raw === void 0) return { origins: [] };
140
+ if (!isRecord$3(raw) || !Array.isArray(raw["origins"])) {
141
+ problems.add(path, wrongType(raw, "an object with origins"), "Declare it as { origins: [\"https://example.com\"] }, or remove it.");
142
+ return { origins: [] };
143
+ }
144
+ const origins = [];
145
+ for (const [index, origin] of raw["origins"].entries()) {
146
+ const read = readSiteUrl(itemPath(keyPath(path, "origins"), index), origin, problems);
147
+ if (read !== null && !origins.includes(read)) origins.push(read);
148
+ }
149
+ return { origins };
150
+ }
151
+ function readBindingName(path, raw, problems) {
152
+ if (typeof raw === "string" && BINDING_NAME.test(raw) && RESERVED_SECRET.test(raw)) {
153
+ problems.add(path, `names ${quote$1(raw)}, a secret of Hollware or of Cloudflare, which no webhook may use`, "Leave the secret out, and Hollware makes one for the webhook (make_webhook_secret).");
154
+ return null;
155
+ }
156
+ if (typeof raw === "string" && BINDING_NAME.test(raw)) return raw;
157
+ problems.add(path, wrongType(raw, "the name of a Worker secret"), "Name the secret that holds the value, such as \"WEBHOOK_SECRET\", and set it with wrangler secret put.");
158
+ return null;
159
+ }
160
+ function readWebhook(path, raw, problems) {
161
+ if (!isRecord$3(raw)) {
162
+ problems.add(path, wrongType(raw, "a webhook"), "Declare it as { name: \"newsletter\", url: \"https://…\", secret: \"WEBHOOK_SECRET\" }.");
163
+ return null;
164
+ }
165
+ const name = raw["name"];
166
+ if (typeof name !== "string" || !NAME$1.test(name)) problems.add(keyPath(path, "name"), wrongType(name, "a name"), "Name it with lowercase words joined by hyphens, such as \"newsletter\".");
167
+ const url = raw["url"];
168
+ const urlOk = typeof url === "string" && URL.canParse(url) && new URL(url).protocol === "https:";
169
+ if (!urlOk) problems.add(keyPath(path, "url"), wrongType(url, "an https URL"), "Give the https URL that receives the events.");
170
+ const secret = raw["secret"] === void 0 ? null : readBindingName(keyPath(path, "secret"), raw["secret"], problems);
171
+ if (typeof name !== "string" || !NAME$1.test(name) || !urlOk) return null;
172
+ if (raw["secret"] !== void 0 && secret === null) return null;
173
+ return {
174
+ name,
175
+ url: String(url),
176
+ secret
177
+ };
178
+ }
179
+ /** Who hears about publications (ADR-025): webhooks, and IndexNow when the site has a URL. */
180
+ function readEvents(path, raw, siteUrl, problems) {
181
+ const events = {
182
+ webhooks: [],
183
+ indexNow: null
184
+ };
185
+ if (raw === void 0) return events;
186
+ if (!isRecord$3(raw)) {
187
+ problems.add(path, wrongType(raw, "an object"), "Declare { webhooks: [ … ], indexNow: { … } }.");
188
+ return events;
189
+ }
190
+ const webhooks = raw["webhooks"];
191
+ if (webhooks !== void 0) {
192
+ if (!Array.isArray(webhooks)) problems.add(keyPath(path, "webhooks"), wrongType(webhooks, "a list"), "List the webhooks.");
193
+ else {
194
+ const seen = /* @__PURE__ */ new Set();
195
+ webhooks.forEach((item, index) => {
196
+ const webhook = readWebhook(itemPath(keyPath(path, "webhooks"), index), item, problems);
197
+ if (webhook === null) return;
198
+ if (seen.has(webhook.name)) {
199
+ problems.add(itemPath(keyPath(path, "webhooks"), index), `repeats the name ${quote$1(webhook.name)}`, "Give each webhook a name of its own.");
200
+ return;
201
+ }
202
+ seen.add(webhook.name);
203
+ events.webhooks.push(webhook);
204
+ });
205
+ }
206
+ }
207
+ const indexNow = raw["indexNow"];
208
+ if (indexNow !== void 0) {
209
+ const indexPath = keyPath(path, "indexNow");
210
+ if (!isRecord$3(indexNow)) problems.add(indexPath, wrongType(indexNow, "an object"), "Declare { key: \"<your IndexNow key>\" }.");
211
+ else {
212
+ const raw = indexNow["key"];
213
+ const key = typeof raw === "string" && INDEXNOW_KEY.test(raw) ? raw : null;
214
+ if (key === null) problems.add(keyPath(indexPath, "key"), wrongType(raw, "an IndexNow key: 8 to 128 letters, digits or hyphens"), "Put the key itself, the one your site serves at /<key>.txt, not the name of a secret.");
215
+ if (siteUrl === null) problems.add(indexPath, "needs the URL of the site", "Set url on the site: IndexNow is told the public URLs of the entries.");
216
+ else if (key !== null) events.indexNow = { key };
217
+ }
218
+ }
219
+ return events;
220
+ }
221
+ //#endregion
58
222
  //#region src/config/constants.ts
59
223
  /** The largest Markdown file a collection accepts when its config does not say, 512 KiB. */
60
224
  const DEFAULT_MAX_MARKDOWN_BYTES = 524288;
@@ -70,7 +234,7 @@ const MAX_MARKDOWN_BYTES_CEILING = 786432;
70
234
  * letter, at most 63 characters. A component name is what Markdown writes after the colons
71
235
  * (`::callout`). A site key has its own rule (`site-key.ts`).
72
236
  */
73
- const NAME$1 = /^[a-z][a-z0-9-]{0,62}$/;
237
+ const NAME = /^[a-z][a-z0-9-]{0,62}$/;
74
238
  /** A language tag: a language, then an optional script and region: `es`, `pt-BR`, `zh-Hant-TW`. */
75
239
  const LOCALE_TAG = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?$/;
76
240
  /** The id of a contract: lowercase words joined by single hyphens. */
@@ -125,7 +289,7 @@ function runtimeOf(contractId, context) {
125
289
  */
126
290
  /** The most characters of the entry's own text that a message repeats. */
127
291
  const MAX_ECHO_LENGTH = 80;
128
- function isRecord$3(value) {
292
+ function isRecord$2(value) {
129
293
  return typeof value === "object" && value !== null && !Array.isArray(value);
130
294
  }
131
295
  /** Orders two texts by UTF-16 code units: the same in every runtime, unlike `localeCompare`. */
@@ -140,7 +304,7 @@ function clip(text, max = MAX_ECHO_LENGTH) {
140
304
  return `${last >= 55296 && last <= 56319 ? head.slice(0, -1) : head}…`;
141
305
  }
142
306
  /** Text from the entry as a message shows it: clipped, in JSON quotes, control characters escaped. */
143
- function quote$1(text) {
307
+ function quote(text) {
144
308
  return JSON.stringify(clip(text));
145
309
  }
146
310
  /** A JSON Pointer (RFC 6901) from its segments: `['tags', '0']` is `/tags/0`, none is the root. */
@@ -178,11 +342,11 @@ function fragmentAt(schema, segments) {
178
342
  let node = schema;
179
343
  for (const segment of segments) {
180
344
  const properties = node.properties;
181
- if (isRecord$3(properties) && Object.hasOwn(properties, segment)) {
345
+ if (isRecord$2(properties) && Object.hasOwn(properties, segment)) {
182
346
  const next = properties[segment];
183
- if (!isRecord$3(next)) return void 0;
347
+ if (!isRecord$2(next)) return void 0;
184
348
  node = next;
185
- } else if (/^\d+$/.test(segment) && isRecord$3(node.items)) node = node.items;
349
+ } else if (/^\d+$/.test(segment) && isRecord$2(node.items)) node = node.items;
186
350
  else return;
187
351
  }
188
352
  return node;
@@ -190,7 +354,7 @@ function fragmentAt(schema, segments) {
190
354
  function valueAt(input, segments) {
191
355
  let node = input;
192
356
  for (const segment of segments) if (Array.isArray(node) && /^\d+$/.test(segment)) node = node[Number(segment)];
193
- else if (isRecord$3(node) && Object.hasOwn(node, segment)) node = node[segment];
357
+ else if (isRecord$2(node) && Object.hasOwn(node, segment)) node = node[segment];
194
358
  else return void 0;
195
359
  return node;
196
360
  }
@@ -226,11 +390,11 @@ function numberExpectation(type, schema) {
226
390
  }
227
391
  function arrayExpectation(schema) {
228
392
  const size = range(schema.minItems, schema.maxItems, "item");
229
- const items = isRecord$3(schema.items) ? expectation(schema.items) : void 0;
393
+ const items = isRecord$2(schema.items) ? expectation(schema.items) : void 0;
230
394
  return `an array${size === void 0 ? "" : ` of ${size}`}${items === void 0 ? "" : `, each ${items}`}`;
231
395
  }
232
396
  function objectExpectation(schema) {
233
- const keys = isRecord$3(schema.properties) ? Object.keys(schema.properties) : [];
397
+ const keys = isRecord$2(schema.properties) ? Object.keys(schema.properties) : [];
234
398
  return keys.length === 0 ? "an object" : `an object with the keys ${keys.join(", ")}`;
235
399
  }
236
400
  function typeExpectation(type, schema) {
@@ -253,26 +417,26 @@ function expectation(schema) {
253
417
  if ("const" in schema) return `exactly ${JSON.stringify(schema.const)}`;
254
418
  if (Array.isArray(schema.enum)) return `one of ${listValues(schema.enum)}`;
255
419
  const alternatives = schema.anyOf ?? schema.oneOf;
256
- const parts = Array.isArray(alternatives) ? alternatives.map((option) => isRecord$3(option) ? expectation(option) : void 0) : (Array.isArray(schema.type) ? schema.type : [schema.type]).map((type) => typeExpectation(type, schema));
420
+ const parts = Array.isArray(alternatives) ? alternatives.map((option) => isRecord$2(option) ? expectation(option) : void 0) : (Array.isArray(schema.type) ? schema.type : [schema.type]).map((type) => typeExpectation(type, schema));
257
421
  return parts.every((part) => part !== void 0) ? parts.join(" or ") : void 0;
258
422
  }
259
423
  /** The keys of `value` that an object schema closed to other keys does not list. */
260
424
  function unknownKeys(fragment, value) {
261
425
  const { additionalProperties, properties } = fragment;
262
- if (additionalProperties !== false || !isRecord$3(properties) || !isRecord$3(value)) return [];
426
+ if (additionalProperties !== false || !isRecord$2(properties) || !isRecord$2(value)) return [];
263
427
  return Object.keys(value).filter((key) => !Object.hasOwn(properties, key));
264
428
  }
265
429
  /** The fix for one issue: imperative, naming the field and what the schema accepts there. */
266
430
  function fixForIssue({ subject, schema, input, segments, message }) {
267
- const target = segments.length === 0 ? subject.container : `the ${subject.noun} ${quote$1(label(segments))}`;
431
+ const target = segments.length === 0 ? subject.container : `the ${subject.noun} ${quote(label(segments))}`;
268
432
  const fragment = schema === null ? void 0 : fragmentAt(schema, segments);
269
433
  if (fragment !== void 0) {
270
434
  const unknown = unknownKeys(fragment, valueAt(input, segments));
271
435
  if (unknown.length > 0) {
272
- const allowed = isRecord$3(fragment.properties) ? Object.keys(fragment.properties) : [];
436
+ const allowed = isRecord$2(fragment.properties) ? Object.keys(fragment.properties) : [];
273
437
  const nouns = unknown.length === 1 ? subject.noun : `${subject.noun}s`;
274
438
  const where = allowed.length === 0 ? `no ${subject.noun}s are allowed there` : `the ${subject.noun}s allowed there are: ${allowed.join(", ")}`;
275
- return `Remove the ${nouns} ${unknown.map((key) => quote$1(key)).join(", ")} from ${target}; ${where}.`;
439
+ return `Remove the ${nouns} ${unknown.map((key) => quote(key)).join(", ")} from ${target}; ${where}.`;
276
440
  }
277
441
  }
278
442
  const expected = fragment === void 0 ? void 0 : expectation(fragment);
@@ -354,15 +518,15 @@ const COMPONENTS = registerBuiltIn({
354
518
  const result = await definition.props["~standard"].validate(use.props);
355
519
  if (!result.issues) continue;
356
520
  const subject = {
357
- container: `the component ${quote$1(use.name)}`,
521
+ container: `the component ${quote(use.name)}`,
358
522
  noun: "prop"
359
523
  };
360
524
  for (const issue of result.issues) {
361
525
  const segments = issueSegments(issue);
362
- const which = segments.length === 0 ? "The props" : `The prop ${quote$1(label(segments))}`;
526
+ const which = segments.length === 0 ? "The props" : `The prop ${quote(label(segments))}`;
363
527
  findings.push({
364
528
  path: "/body",
365
- message: `${which} of the component ${quote$1(use.name)} ${segments.length === 0 ? "are" : "is"} invalid: ${issue.message}`,
529
+ message: `${which} of the component ${quote(use.name)} ${segments.length === 0 ? "are" : "is"} invalid: ${issue.message}`,
366
530
  fix: fixForIssue({
367
531
  subject,
368
532
  schema: schemas.get(use.name) ?? null,
@@ -429,16 +593,16 @@ const RAW_HTML = markerContract("raw-html", "The body holds Markdown and the com
429
593
  const UNKNOWN_COMPONENT = markerContract("unknown-component", "The body uses only the components that the collection declares.", (marker, collection) => {
430
594
  const components = allowedComponents(collection);
431
595
  return {
432
- message: `The component ${quote$1(marker.name)} does not exist in this collection.`,
596
+ message: `The component ${quote(marker.name)} does not exist in this collection.`,
433
597
  fix: components.length === 0 ? "This collection allows no components: remove it and use Markdown." : `Use one of the allowed components: ${components.join(", ")}.`
434
598
  };
435
599
  });
436
600
  const DISALLOWED_ATTRIBUTE = markerContract("disallowed-attribute", "Links, images and code carry only the attributes Markdown produces, and a component only the props its schema declares.", (marker) => ({
437
- message: `The attribute ${quote$1(marker.name)} is not allowed.`,
438
- fix: `Remove the attribute ${quote$1(marker.name)}. Links keep href and title, images keep src, alt, title, width and height, and a component takes only the props its schema declares.`
601
+ message: `The attribute ${quote(marker.name)} is not allowed.`,
602
+ fix: `Remove the attribute ${quote(marker.name)}. Links keep href and title, images keep src, alt, title, width and height, and a component takes only the props its schema declares.`
439
603
  }));
440
604
  const UNSAFE_URL = markerContract("unsafe-url", "Links and images use an https:, http: or mailto: URL, a path of the site or an anchor.", (marker) => ({
441
- message: `The URL ${quote$1(marker.name)} is not allowed.`,
605
+ message: `The URL ${quote(marker.name)} is not allowed.`,
442
606
  fix: "Use an https: or http: URL, a mailto: address, a path of this site that starts with /, or an anchor that starts with #."
443
607
  }));
444
608
  //#endregion
@@ -622,7 +786,7 @@ function chunksOf(entry) {
622
786
  //#endregion
623
787
  //#region src/contracts/rules.ts
624
788
  function present(frontmatter, field) {
625
- return isRecord$3(frontmatter) ? frontmatter[field] : void 0;
789
+ return isRecord$2(frontmatter) ? frontmatter[field] : void 0;
626
790
  }
627
791
  const GRAPHEMES = new Intl.Segmenter(void 0, { granularity: "grapheme" });
628
792
  /** The length of a value as a reader counts it: characters of a text (an emoji is one), items of a list. */
@@ -633,11 +797,11 @@ function lengthOf(value) {
633
797
  /** A text, or a list, of a field has between `min` and `max` characters or items. */
634
798
  function fieldLength(params, options = {}) {
635
799
  const bounds = [params.min === void 0 ? null : `at least ${params.min}`, params.max === void 0 ? null : `at most ${params.max}`].filter((bound) => bound !== null);
636
- const unless = params.unless === void 0 ? "" : `, unless ${quote$1(params.unless)} is given`;
800
+ const unless = params.unless === void 0 ? "" : `, unless ${quote(params.unless)} is given`;
637
801
  return registerBuiltIn({
638
802
  id: options.id ?? "field-length",
639
803
  severity: options.severity ?? "error",
640
- description: `${quote$1(params.field)} has ${bounds.join(" and ")} characters or items${unless}.`,
804
+ description: `${quote(params.field)} has ${bounds.join(" and ")} characters or items${unless}.`,
641
805
  rule: {
642
806
  name: "field-length",
643
807
  params: { ...params }
@@ -651,13 +815,13 @@ function fieldLength(params, options = {}) {
651
815
  const path = `/frontmatter/${params.field}`;
652
816
  if (params.max !== void 0 && length > params.max) return [{
653
817
  path,
654
- message: `${quote$1(params.field)} has ${count(length, unit)}; the most is ${params.max}.`,
655
- fix: `Shorten ${quote$1(params.field)} to ${count(params.max, unit)} or fewer${params.unless === void 0 ? "" : `, or add ${quote$1(params.unless)}`}.`
818
+ message: `${quote(params.field)} has ${count(length, unit)}; the most is ${params.max}.`,
819
+ fix: `Shorten ${quote(params.field)} to ${count(params.max, unit)} or fewer${params.unless === void 0 ? "" : `, or add ${quote(params.unless)}`}.`
656
820
  }];
657
821
  if (params.min !== void 0 && length < params.min) return [{
658
822
  path,
659
- message: `${quote$1(params.field)} has ${count(length, unit)}; the least is ${params.min}.`,
660
- fix: `Lengthen ${quote$1(params.field)} to ${count(params.min, unit)} or more.`
823
+ message: `${quote(params.field)} has ${count(length, unit)}; the least is ${params.min}.`,
824
+ fix: `Lengthen ${quote(params.field)} to ${count(params.min, unit)} or more.`
661
825
  }];
662
826
  return [];
663
827
  }
@@ -669,7 +833,7 @@ function fieldPattern(params, options = {}) {
669
833
  return registerBuiltIn({
670
834
  id: options.id ?? "field-pattern",
671
835
  severity: options.severity ?? "error",
672
- description: `${quote$1(params.field)} is ${params.description}.`,
836
+ description: `${quote(params.field)} is ${params.description}.`,
673
837
  rule: {
674
838
  name: "field-pattern",
675
839
  params: { ...params }
@@ -679,8 +843,8 @@ function fieldPattern(params, options = {}) {
679
843
  if (typeof value !== "string" || pattern.test(value)) return [];
680
844
  return [{
681
845
  path: `/frontmatter/${params.field}`,
682
- message: `${quote$1(params.field)} is not ${params.description}.`,
683
- fix: `Write ${quote$1(params.field)} as ${params.description}.`
846
+ message: `${quote(params.field)} is not ${params.description}.`,
847
+ fix: `Write ${quote(params.field)} as ${params.description}.`
684
848
  }];
685
849
  }
686
850
  }, { readsFrontmatter: true });
@@ -692,15 +856,15 @@ function requiredToPublish(params, options = {}) {
692
856
  id: options.id ?? "required-to-publish",
693
857
  severity: options.severity ?? "error",
694
858
  phases: ["publish"],
695
- description: `To publish, ${params.fields.map(quote$1).join(", ")} must have a value.`,
859
+ description: `To publish, ${params.fields.map(quote).join(", ")} must have a value.`,
696
860
  rule: {
697
861
  name: "required-to-publish",
698
862
  params: { fields: [...params.fields] }
699
863
  },
700
864
  check: ({ frontmatter }) => params.fields.filter((field) => empty(present(frontmatter, field))).map((field) => ({
701
865
  path: `/frontmatter/${field}`,
702
- message: `${quote$1(field)} is empty, and the entry cannot be published without it.`,
703
- fix: `Give ${quote$1(field)} a value, then publish.`
866
+ message: `${quote(field)} is empty, and the entry cannot be published without it.`,
867
+ fix: `Give ${quote(field)} a value, then publish.`
704
868
  }))
705
869
  }, { readsFrontmatter: true });
706
870
  }
@@ -716,7 +880,7 @@ function bannedWords(params, options = {}) {
716
880
  return registerBuiltIn({
717
881
  id: options.id ?? "banned-words",
718
882
  severity: options.severity ?? "error",
719
- description: `None of these words: ${words.map(quote$1).join(", ")}${because}.`,
883
+ description: `None of these words: ${words.map(quote).join(", ")}${because}.`,
720
884
  rule: {
721
885
  name: "banned-words",
722
886
  params: {
@@ -727,19 +891,19 @@ function bannedWords(params, options = {}) {
727
891
  check: (context) => {
728
892
  if (pattern === null) return [];
729
893
  const findings = [];
730
- const frontmatter = isRecord$3(context.frontmatter) ? context.frontmatter : {};
894
+ const frontmatter = isRecord$2(context.frontmatter) ? context.frontmatter : {};
731
895
  for (const [field, value] of Object.entries(frontmatter)) {
732
896
  if (typeof value !== "string") continue;
733
897
  for (const word of found(value)) findings.push({
734
898
  path: `/frontmatter/${field}`,
735
- message: `${quote$1(field)} uses ${quote$1(word)}.`,
736
- fix: `Write ${quote$1(field)} without ${quote$1(word)}${because}.`
899
+ message: `${quote(field)} uses ${quote(word)}.`,
900
+ fix: `Write ${quote(field)} without ${quote(word)}${because}.`
737
901
  });
738
902
  }
739
903
  for (const word of found(documentText(context.markdown.document))) findings.push({
740
904
  path: "/body",
741
- message: `The body uses ${quote$1(word)}.`,
742
- fix: `Rewrite the body without ${quote$1(word)}${because}.`
905
+ message: `The body uses ${quote(word)}.`,
906
+ fix: `Rewrite the body without ${quote(word)}${because}.`
743
907
  });
744
908
  return findings;
745
909
  }
@@ -768,7 +932,7 @@ function translationPair(options = {}) {
768
932
  const field = collection.translationGroupField;
769
933
  if (field === null) return [];
770
934
  const path = `/frontmatter${pointer([field])}`;
771
- const group = isRecord$3(context.frontmatter) ? context.frontmatter[field] : void 0;
935
+ const group = isRecord$2(context.frontmatter) ? context.frontmatter[field] : void 0;
772
936
  if (typeof group !== "string" || group === "") return [{
773
937
  path,
774
938
  message: `The entry has no translation group: its frontmatter field ${field} is empty.`,
@@ -778,8 +942,8 @@ function translationPair(options = {}) {
778
942
  const present = /* @__PURE__ */ new Set([context.locale, ...found.map((entry) => entry.locale)]);
779
943
  return site.locales.filter((locale) => !present.has(locale)).map((locale) => ({
780
944
  path,
781
- message: `The ${locale} version of the translation group ${quote$1(group)} does not exist.`,
782
- fix: `Create the entry in ${locale} with ${field}: ${quote$1(group)} in its frontmatter before publishing.`
945
+ message: `The ${locale} version of the translation group ${quote(group)} does not exist.`,
946
+ fix: `Create the entry in ${locale} with ${field}: ${quote(group)} in its frontmatter before publishing.`
783
947
  }));
784
948
  }
785
949
  }, { readsFrontmatter: true });
@@ -826,7 +990,7 @@ function internalLinksResolve(options = {}) {
826
990
  const entry = `the entry ${clip(`${target.collection}/${target.locale}/${target.slug}`)}`;
827
991
  findings.push({
828
992
  path: "/body",
829
- message: found === null ? `The link ${quote$1(link.href)} points at ${entry}, which does not exist.` : `The link ${quote$1(link.href)} points at ${entry}, which is not published yet (${found.status}).`,
993
+ message: found === null ? `The link ${quote(link.href)} points at ${entry}, which does not exist.` : `The link ${quote(link.href)} points at ${entry}, which is not published yet (${found.status}).`,
830
994
  fix: found === null ? `Create and publish ${entry}, point the link at an entry that is published, or remove the link.` : `Publish ${entry} first, point the link at an entry that is published, or remove the link.`,
831
995
  line: link.line
832
996
  });
@@ -859,7 +1023,7 @@ function imagesHaveAlt(options = {}) {
859
1023
  description: "Every image has alt text that describes it.",
860
1024
  check: (context) => context.markdown.extracted.images.filter((image) => image.alt.trim() === "").map((image) => ({
861
1025
  path: "/body",
862
- message: `The image ${quote$1(image.src)} has no alt text.`,
1026
+ message: `The image ${quote(image.src)} has no alt text.`,
863
1027
  fix: `Add alt text that describes the image, as in ![a short description](${clip(image.src)}).`,
864
1028
  line: image.line
865
1029
  }))
@@ -1026,7 +1190,7 @@ function toViolations(contract, findings) {
1026
1190
  severity: contract.severity,
1027
1191
  path: finding.path,
1028
1192
  message: finding.message,
1029
- fix: finding.fix.trim() === "" ? `Change the entry so that the rule ${quote$1(contract.id)} passes: ${contract.description}` : finding.fix,
1193
+ fix: finding.fix.trim() === "" ? `Change the entry so that the rule ${quote(contract.id)} passes: ${contract.description}` : finding.fix,
1030
1194
  ...finding.line === void 0 ? {} : { line: finding.line }
1031
1195
  }));
1032
1196
  }
@@ -1043,8 +1207,8 @@ function thrown(contract, error) {
1043
1207
  contract: contract.id,
1044
1208
  severity: "error",
1045
1209
  path: "",
1046
- message: `The contract ${quote$1(contract.id)} failed with an error while it checked this entry: ${clip(describeError(error), 200)}`,
1047
- fix: `This is a problem in the config of the site, not in the entry: report it to whoever maintains the config, naming the contract ${quote$1(contract.id)}.`
1210
+ message: `The contract ${quote(contract.id)} failed with an error while it checked this entry: ${clip(describeError(error), 200)}`,
1211
+ fix: `This is a problem in the config of the site, not in the entry: report it to whoever maintains the config, naming the contract ${quote(contract.id)}.`
1048
1212
  };
1049
1213
  }
1050
1214
  async function settle(contract, context) {
@@ -1169,57 +1333,10 @@ function uniqueTranslationViolation(holder, field) {
1169
1333
  severity: "error",
1170
1334
  path: `/frontmatter${pointer([field])}`,
1171
1335
  message: `Another entry of this collection and locale, ${holder}, already belongs to this translation group.`,
1172
- fix: `Give ${quote$1(field)} another value, or change the entry ${holder} with update_draft: a translation group holds one entry per locale.`
1336
+ fix: `Give ${quote(field)} another value, or change the entry ${holder} with update_draft: a translation group holds one entry per locale.`
1173
1337
  };
1174
1338
  }
1175
1339
  //#endregion
1176
- //#region src/config/values.ts
1177
- /** A value with keys: not null, not an array. */
1178
- function isRecord$2(value) {
1179
- return typeof value === "object" && value !== null && !Array.isArray(value);
1180
- }
1181
- /** A string as the messages show it: quoted, with what could break a line escaped. */
1182
- function quote(text) {
1183
- return JSON.stringify(text);
1184
- }
1185
- /** Text of an error that someone else wrote, on one line, so that a problem stays one line. */
1186
- function oneLine(text) {
1187
- return text.replace(/\s+/g, " ").trim();
1188
- }
1189
- /** What a value is, for a message: `"es"`, `42`, `null`, `an array`. */
1190
- function found(value) {
1191
- if (value === null) return "null";
1192
- switch (typeof value) {
1193
- case "string": return quote(value);
1194
- case "number":
1195
- case "boolean":
1196
- case "bigint": return String(value);
1197
- case "function": return "a function";
1198
- case "symbol": return "a symbol";
1199
- default: return Array.isArray(value) ? "an array" : "an object";
1200
- }
1201
- }
1202
- /** `is 42; it must be a string`, or `is missing; …` when the value is not there. */
1203
- function wrongType(value, expected) {
1204
- return `${value === void 0 ? "is missing" : `is ${found(value)}`}; it must be ${expected}`;
1205
- }
1206
- /** An optional string: `null` when it is absent, and a problem when it is anything else. */
1207
- function readOptionalString(path, value, fix, problems) {
1208
- if (value === void 0) return null;
1209
- if (typeof value === "string") return value;
1210
- problems.add(path, wrongType(value, "a string"), fix);
1211
- return null;
1212
- }
1213
- const PLAIN_KEY = /^[A-Za-z0-9_-]+$/;
1214
- /** The path of a key inside `parent`: `sites.demo`, or `sites["my site"]` for an odd key. */
1215
- function keyPath(parent, key) {
1216
- return PLAIN_KEY.test(key) ? `${parent}.${key}` : `${parent}[${quote(key)}]`;
1217
- }
1218
- /** The path of an item of an array: `sites.demo.locales[1]`. */
1219
- function itemPath(parent, index) {
1220
- return `${parent}[${index}]`;
1221
- }
1222
- //#endregion
1223
1340
  //#region src/config/json-schema.ts
1224
1341
  /**
1225
1342
  * JSON Schema as the config uses it: what agents see of a schema, and what the checks read of it
@@ -1233,7 +1350,7 @@ const TARGET = "draft-2020-12";
1233
1350
  */
1234
1351
  function toJsonSchema$1(schema) {
1235
1352
  const converted = schema["~standard"].jsonSchema.input({ target: TARGET });
1236
- if (!isRecord$2(converted)) throw new TypeError(`the library ${wrongType(converted, "a JSON Schema object")}`);
1353
+ if (!isRecord$3(converted)) throw new TypeError(`the library ${wrongType(converted, "a JSON Schema object")}`);
1237
1354
  return JSON.parse(JSON.stringify(converted));
1238
1355
  }
1239
1356
  /** Reference chains longer than this are cycles. */
@@ -1245,7 +1362,7 @@ function pointTo(root, ref) {
1245
1362
  let node = root;
1246
1363
  for (const part of ref.slice(2).split("/")) {
1247
1364
  const key = part.replaceAll("~1", "/").replaceAll("~0", "~");
1248
- if (!isRecord$2(node) || !Object.hasOwn(node, key)) return void 0;
1365
+ if (!isRecord$3(node) || !Object.hasOwn(node, key)) return void 0;
1249
1366
  node = node[key];
1250
1367
  }
1251
1368
  return node;
@@ -1257,7 +1374,7 @@ function pointTo(root, ref) {
1257
1374
  function resolveSchema(root, schema) {
1258
1375
  let node = schema;
1259
1376
  for (let hop = 0; hop <= MAX_REF_HOPS; hop += 1) {
1260
- if (!isRecord$2(node)) return null;
1377
+ if (!isRecord$3(node)) return null;
1261
1378
  const ref = node["$ref"];
1262
1379
  if (typeof ref !== "string") return node;
1263
1380
  node = pointTo(root, ref);
@@ -1374,14 +1491,14 @@ function standardProps(value) {
1374
1491
  /** A Standard Schema that also converts to JSON Schema, which is what a collection needs. */
1375
1492
  function isDescribedSchema(value) {
1376
1493
  const standard = standardProps(value);
1377
- if (!isRecord$2(standard) || typeof standard["validate"] !== "function") return false;
1494
+ if (!isRecord$3(standard) || typeof standard["validate"] !== "function") return false;
1378
1495
  const converter = standard["jsonSchema"];
1379
- return isRecord$2(converter) && typeof converter["input"] === "function";
1496
+ return isRecord$3(converter) && typeof converter["input"] === "function";
1380
1497
  }
1381
1498
  /** What `isDescribedSchema` found missing, for the message. */
1382
1499
  function whyNotDescribed(value) {
1383
1500
  const standard = standardProps(value);
1384
- if (!isRecord$2(standard)) return wrongType(value, "a Standard Schema with JSON Schema");
1501
+ if (!isRecord$3(standard)) return wrongType(value, "a Standard Schema with JSON Schema");
1385
1502
  if (typeof standard["validate"] !== "function") return "has no ~standard.validate function, so it is no Standard Schema";
1386
1503
  return "has no ~standard.jsonSchema.input function, so describe_site could not show it to agents";
1387
1504
  }
@@ -1429,12 +1546,12 @@ const SLUG_PLACEHOLDER = /:slug(?![A-Za-z0-9_])/g;
1429
1546
  function isRoutePattern(path, route, problems) {
1430
1547
  let valid = true;
1431
1548
  if (!route.startsWith("/")) {
1432
- problems.add(path, `${quote(route)} does not start with "/"`, "Write the path from the root of the site, such as \"/blog/:slug/\".");
1549
+ problems.add(path, `${quote$1(route)} does not start with "/"`, "Write the path from the root of the site, such as \"/blog/:slug/\".");
1433
1550
  valid = false;
1434
1551
  }
1435
1552
  const slugs = [...route.matchAll(SLUG_PLACEHOLDER)].length;
1436
1553
  if (slugs !== 1) {
1437
- problems.add(path, slugs === 0 ? `${quote(route)} has no :slug` : `${quote(route)} has :slug ${slugs} times`, "Use :slug exactly once, where the slug of the entry goes, such as \"/blog/:slug/\".");
1554
+ problems.add(path, slugs === 0 ? `${quote$1(route)} has no :slug` : `${quote$1(route)} has :slug ${slugs} times`, "Use :slug exactly once, where the slug of the entry goes, such as \"/blog/:slug/\".");
1438
1555
  valid = false;
1439
1556
  }
1440
1557
  return valid;
@@ -1447,11 +1564,11 @@ function claimRoute(route, path, site, problems) {
1447
1564
  site.routes.set(pattern, path);
1448
1565
  return;
1449
1566
  }
1450
- problems.add(path, `${quote(route)} is the same pattern as the route at ${owner}`, "Give every route of the site its own path: internal links are matched against them.");
1567
+ problems.add(path, `${quote$1(route)} is the same pattern as the route at ${owner}`, "Give every route of the site its own path: internal links are matched against them.");
1451
1568
  }
1452
1569
  /** The route of each locale, in the order of the site's locales. */
1453
1570
  function readRoutes(path, raw, collection, site, problems) {
1454
- if (!isRecord$2(raw)) {
1571
+ if (!isRecord$3(raw)) {
1455
1572
  problems.add(path, wrongType(raw, "an object with one path per locale"), ROUTES_FIX);
1456
1573
  return {};
1457
1574
  }
@@ -1459,12 +1576,12 @@ function readRoutes(path, raw, collection, site, problems) {
1459
1576
  const routePath = keyPath(path, locale);
1460
1577
  const route = raw[locale];
1461
1578
  const known = !site.localesOk || site.locales.includes(locale);
1462
- if (!known) problems.add(routePath, `is a route for ${quote(locale)}, which is not a locale of the site (${site.locales.join(", ")})`, `Remove it, or add ${quote(locale)} to the locales of the site.`);
1579
+ if (!known) problems.add(routePath, `is a route for ${quote$1(locale)}, which is not a locale of the site (${site.locales.join(", ")})`, `Remove it, or add ${quote$1(locale)} to the locales of the site.`);
1463
1580
  if (typeof route !== "string") problems.add(routePath, wrongType(route, "a path such as \"/blog/:slug/\""), ROUTES_FIX);
1464
1581
  else if (isRoutePattern(routePath, route, problems) && known) claimRoute(route, routePath, site, problems);
1465
1582
  }
1466
1583
  if (site.localesOk) {
1467
- for (const locale of site.locales) if (!Object.hasOwn(raw, locale)) problems.add(path, `has no route for the locale ${quote(locale)}`, `Add ${keyPath("routes", locale)}, a path such as ${quote(`/${locale}/${collection}/:slug/`)}.`);
1584
+ for (const locale of site.locales) if (!Object.hasOwn(raw, locale)) problems.add(path, `has no route for the locale ${quote$1(locale)}`, `Add ${keyPath("routes", locale)}, a path such as ${quote$1(`/${locale}/${collection}/:slug/`)}.`);
1468
1585
  }
1469
1586
  const routes = {};
1470
1587
  for (const locale of site.locales) {
@@ -1476,12 +1593,12 @@ function readRoutes(path, raw, collection, site, problems) {
1476
1593
  const GROUP_BY = {
1477
1594
  mustBe: "a string",
1478
1595
  accepts: isStringSchema,
1479
- fix: (field) => `Declare ${quote(field)} as z.string() in the schema, or name another field.`
1596
+ fix: (field) => `Declare ${quote$1(field)} as z.string() in the schema, or name another field.`
1480
1597
  };
1481
1598
  const TAGS = {
1482
1599
  mustBe: "an array of strings",
1483
1600
  accepts: isStringArraySchema,
1484
- fix: (field) => `Declare ${quote(field)} as z.array(z.string()) in the schema, or name another field.`
1601
+ fix: (field) => `Declare ${quote$1(field)} as z.array(z.string()) in the schema, or name another field.`
1485
1602
  };
1486
1603
  /**
1487
1604
  * Listings sort by this field as text, which is chronological only for ISO 8601 dates. A string
@@ -1490,7 +1607,7 @@ const TAGS = {
1490
1607
  const SORT_BY = {
1491
1608
  mustBe: "a string with the \"date\" or \"date-time\" format",
1492
1609
  accepts: isDateSchema,
1493
- fix: (field) => `Declare ${quote(field)} as z.iso.date() or z.iso.datetime() in the schema, or name another field.`
1610
+ fix: (field) => `Declare ${quote$1(field)} as z.iso.date() or z.iso.datetime() in the schema, or name another field.`
1494
1611
  };
1495
1612
  /** The name of a field of the frontmatter, checked against the schema when there is one. */
1496
1613
  function readField(path, raw, schema, kind, problems) {
@@ -1499,16 +1616,16 @@ function readField(path, raw, schema, kind, problems) {
1499
1616
  return null;
1500
1617
  }
1501
1618
  if (schema === null) return raw;
1502
- const properties = isRecord$2(schema.object["properties"]) ? schema.object["properties"] : {};
1619
+ const properties = isRecord$3(schema.object["properties"]) ? schema.object["properties"] : {};
1503
1620
  if (!Object.hasOwn(properties, raw)) {
1504
1621
  const names = Object.keys(properties);
1505
- problems.add(path, `${quote(raw)} is not a property of the schema${names.length === 0 ? ", which declares none" : ` (its properties are ${names.map(quote).join(", ")})`}`, `Name a field that the schema declares, or add ${quote(raw)} to the schema.`);
1506
- } else if (!kind.accepts(schema.document, properties[raw])) problems.add(path, `${quote(raw)} is declared as ${describeShape(schema.document, properties[raw])}, but it must be ${kind.mustBe}`, kind.fix(raw));
1622
+ problems.add(path, `${quote$1(raw)} is not a property of the schema${names.length === 0 ? ", which declares none" : ` (its properties are ${names.map(quote$1).join(", ")})`}`, `Name a field that the schema declares, or add ${quote$1(raw)} to the schema.`);
1623
+ } else if (!kind.accepts(schema.document, properties[raw])) problems.add(path, `${quote$1(raw)} is declared as ${describeShape(schema.document, properties[raw])}, but it must be ${kind.mustBe}`, kind.fix(raw));
1507
1624
  return raw;
1508
1625
  }
1509
1626
  function readTranslations(path, raw, schema, problems) {
1510
1627
  if (raw === void 0) return null;
1511
- if (!isRecord$2(raw)) {
1628
+ if (!isRecord$3(raw)) {
1512
1629
  problems.add(path, wrongType(raw, "an object like { groupBy: \"translationGroup\" }"), "Set it to { groupBy: \"<field>\" }, the field of the frontmatter that holds the key the translations of an entry share, or remove it.");
1513
1630
  return null;
1514
1631
  }
@@ -1516,8 +1633,8 @@ function readTranslations(path, raw, schema, problems) {
1516
1633
  }
1517
1634
  const COMPONENT_FIX = "Declare it with defineComponent({ props: z.strictObject({ … }) }).";
1518
1635
  function readComponent(path, name, raw, problems) {
1519
- if (!NAME$1.test(name)) problems.add(path, `${quote(name)} is not a valid component name`, nameFix("callout"));
1520
- if (!isRecord$2(raw)) {
1636
+ if (!NAME.test(name)) problems.add(path, `${quote$1(name)} is not a valid component name`, nameFix("callout"));
1637
+ if (!isRecord$3(raw)) {
1521
1638
  problems.add(path, wrongType(raw, "a component"), COMPONENT_FIX);
1522
1639
  return null;
1523
1640
  }
@@ -1535,7 +1652,7 @@ function readComponent(path, name, raw, problems) {
1535
1652
  function readComponents(path, raw, problems) {
1536
1653
  const components = /* @__PURE__ */ new Map();
1537
1654
  if (raw === void 0) return components;
1538
- if (!isRecord$2(raw)) {
1655
+ if (!isRecord$3(raw)) {
1539
1656
  problems.add(path, wrongType(raw, "an object that maps each component name to its definition"), COMPONENT_FIX);
1540
1657
  return components;
1541
1658
  }
@@ -1567,7 +1684,7 @@ function checkPhases(path, raw, problems) {
1567
1684
  for (let index = 0; index < phases.length; index += 1) {
1568
1685
  const phase = phases[index];
1569
1686
  if (!isPhase(phase)) problems.add(itemPath(path, index), wrongType(phase, "\"write\" or \"publish\""), PHASES_FIX);
1570
- else if (seen.has(phase)) problems.add(itemPath(path, index), `repeats ${quote(phase)}`, PHASES_FIX);
1687
+ else if (seen.has(phase)) problems.add(itemPath(path, index), `repeats ${quote$1(phase)}`, PHASES_FIX);
1571
1688
  seen.add(phase);
1572
1689
  }
1573
1690
  }
@@ -1576,7 +1693,7 @@ function checkPhases(path, raw, problems) {
1576
1693
  * what makes it a `Contract`. Whether it may have its id is a question for `checkIdentity`.
1577
1694
  */
1578
1695
  function isContract(item, path, problems) {
1579
- if (!isRecord$2(item)) {
1696
+ if (!isRecord$3(item)) {
1580
1697
  problems.add(path, wrongType(item, "a contract"), "Write it as { id, severity, description, check }.");
1581
1698
  return false;
1582
1699
  }
@@ -1599,7 +1716,7 @@ function isContract(item, path, problems) {
1599
1716
  */
1600
1717
  function checkIdentity(id, own, path, ids, problems) {
1601
1718
  if (RESERVED.has(id) && !own) {
1602
- problems.add(keyPath(path, "id"), `${quote(id)} is the id of a built-in contract`, `Choose another id: ${BUILT_IN_CONTRACT_IDS.join(", ")} belong to Hollware's built-in contracts, and the opt-in ones are listed as contracts.translationPair(), contracts.internalLinksResolve() and contracts.imagesHaveAlt().`);
1719
+ problems.add(keyPath(path, "id"), `${quote$1(id)} is the id of a built-in contract`, `Choose another id: ${BUILT_IN_CONTRACT_IDS.join(", ")} belong to Hollware's built-in contracts, and the opt-in ones are listed as contracts.translationPair(), contracts.internalLinksResolve() and contracts.imagesHaveAlt().`);
1603
1720
  return;
1604
1721
  }
1605
1722
  const first = ids.get(id);
@@ -1607,7 +1724,7 @@ function checkIdentity(id, own, path, ids, problems) {
1607
1724
  ids.set(id, path);
1608
1725
  return;
1609
1726
  }
1610
- problems.add(keyPath(path, "id"), `${quote(id)} is already the id of the contract at ${first}`, "Give every contract of the collection its own id.");
1727
+ problems.add(keyPath(path, "id"), `${quote$1(id)} is already the id of the contract at ${first}`, "Give every contract of the collection its own id.");
1611
1728
  }
1612
1729
  function readContracts(path, raw, problems) {
1613
1730
  const contracts = [];
@@ -1624,7 +1741,7 @@ function readContracts(path, raw, problems) {
1624
1741
  if (isContract(item, itemAt, problems)) {
1625
1742
  checkIdentity(item.id, isBuiltInContract(item), itemAt, ids, problems);
1626
1743
  contracts.push(item);
1627
- } else if (isRecord$2(item) && typeof item["id"] === "string" && CONTRACT_ID.test(item["id"])) checkIdentity(item["id"], false, itemAt, ids, problems);
1744
+ } else if (isRecord$3(item) && typeof item["id"] === "string" && CONTRACT_ID.test(item["id"])) checkIdentity(item["id"], false, itemAt, ids, problems);
1628
1745
  }
1629
1746
  return contracts;
1630
1747
  }
@@ -1639,8 +1756,8 @@ function readMaxMarkdownBytes(path, raw, problems) {
1639
1756
  * problem already: `resolveConfig` throws when there is any, so nothing else keeps track of them.
1640
1757
  */
1641
1758
  function readCollection(path, name, raw, site, problems) {
1642
- if (!NAME$1.test(name)) problems.add(path, `${quote(name)} is not a valid collection name`, nameFix("blog"));
1643
- if (!isRecord$2(raw)) {
1759
+ if (!NAME.test(name)) problems.add(path, `${quote$1(name)} is not a valid collection name`, nameFix("blog"));
1760
+ if (!isRecord$3(raw)) {
1644
1761
  problems.add(path, wrongType(raw, "a collection"), "Declare it with defineCollection({ schema, routes, … }).");
1645
1762
  return null;
1646
1763
  }
@@ -1668,105 +1785,6 @@ function readCollection(path, name, raw, site, problems) {
1668
1785
  };
1669
1786
  }
1670
1787
  //#endregion
1671
- //#region src/config/events.ts
1672
- /** The name of a Worker secret or variable: what `wrangler secret put` takes. */
1673
- const BINDING_NAME = /^[A-Z][A-Z0-9_]*$/;
1674
- /** A subscriber's name: lowercase words joined by hyphens. */
1675
- const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1676
- /** The public URL of a site: an absolute http or https URL with no path, query or fragment. */
1677
- function readSiteUrl(path, raw, problems) {
1678
- if (raw === void 0) return null;
1679
- const fix = "Write the public origin of the site, such as \"https://example.com\".";
1680
- if (typeof raw !== "string" || !URL.canParse(raw)) {
1681
- problems.add(path, wrongType(raw, "an absolute URL"), fix);
1682
- return null;
1683
- }
1684
- const url = new URL(raw);
1685
- if (url.protocol !== "https:" && url.protocol !== "http:" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
1686
- problems.add(path, `${quote(raw)} is not an origin`, fix);
1687
- return null;
1688
- }
1689
- return url.origin;
1690
- }
1691
- /** The origins of a site's pages that may read its published content from the browser. */
1692
- function readCors(path, raw, problems) {
1693
- if (raw === void 0) return { origins: [] };
1694
- if (!isRecord$2(raw) || !Array.isArray(raw["origins"])) {
1695
- problems.add(path, wrongType(raw, "an object with origins"), "Declare it as { origins: [\"https://example.com\"] }, or remove it.");
1696
- return { origins: [] };
1697
- }
1698
- const origins = [];
1699
- for (const [index, origin] of raw["origins"].entries()) {
1700
- const read = readSiteUrl(itemPath(keyPath(path, "origins"), index), origin, problems);
1701
- if (read !== null && !origins.includes(read)) origins.push(read);
1702
- }
1703
- return { origins };
1704
- }
1705
- function readBindingName(path, raw, problems) {
1706
- if (typeof raw === "string" && BINDING_NAME.test(raw)) return raw;
1707
- problems.add(path, wrongType(raw, "the name of a Worker secret"), "Name the secret that holds the value, such as \"WEBHOOK_SECRET\", and set it with wrangler secret put.");
1708
- return null;
1709
- }
1710
- function readWebhook(path, raw, problems) {
1711
- if (!isRecord$2(raw)) {
1712
- problems.add(path, wrongType(raw, "a webhook"), "Declare it as { name: \"newsletter\", url: \"https://…\", secret: \"WEBHOOK_SECRET\" }.");
1713
- return null;
1714
- }
1715
- const name = raw["name"];
1716
- if (typeof name !== "string" || !NAME.test(name)) problems.add(keyPath(path, "name"), wrongType(name, "a name"), "Name it with lowercase words joined by hyphens, such as \"newsletter\".");
1717
- const url = raw["url"];
1718
- const urlOk = typeof url === "string" && URL.canParse(url) && new URL(url).protocol === "https:";
1719
- if (!urlOk) problems.add(keyPath(path, "url"), wrongType(url, "an https URL"), "Give the https URL that receives the events.");
1720
- const secret = raw["secret"] === void 0 ? null : readBindingName(keyPath(path, "secret"), raw["secret"], problems);
1721
- if (typeof name !== "string" || !NAME.test(name) || !urlOk) return null;
1722
- if (raw["secret"] !== void 0 && secret === null) return null;
1723
- return {
1724
- name,
1725
- url: String(url),
1726
- secret
1727
- };
1728
- }
1729
- /** Who hears about publications (ADR-025): webhooks, and IndexNow when the site has a URL. */
1730
- function readEvents(path, raw, siteUrl, problems) {
1731
- const events = {
1732
- webhooks: [],
1733
- indexNow: null
1734
- };
1735
- if (raw === void 0) return events;
1736
- if (!isRecord$2(raw)) {
1737
- problems.add(path, wrongType(raw, "an object"), "Declare { webhooks: [ … ], indexNow: { … } }.");
1738
- return events;
1739
- }
1740
- const webhooks = raw["webhooks"];
1741
- if (webhooks !== void 0) {
1742
- if (!Array.isArray(webhooks)) problems.add(keyPath(path, "webhooks"), wrongType(webhooks, "a list"), "List the webhooks.");
1743
- else {
1744
- const seen = /* @__PURE__ */ new Set();
1745
- webhooks.forEach((item, index) => {
1746
- const webhook = readWebhook(itemPath(keyPath(path, "webhooks"), index), item, problems);
1747
- if (webhook === null) return;
1748
- if (seen.has(webhook.name)) {
1749
- problems.add(itemPath(keyPath(path, "webhooks"), index), `repeats the name ${quote(webhook.name)}`, "Give each webhook a name of its own.");
1750
- return;
1751
- }
1752
- seen.add(webhook.name);
1753
- events.webhooks.push(webhook);
1754
- });
1755
- }
1756
- }
1757
- const indexNow = raw["indexNow"];
1758
- if (indexNow !== void 0) {
1759
- const indexPath = keyPath(path, "indexNow");
1760
- if (!isRecord$2(indexNow)) problems.add(indexPath, wrongType(indexNow, "an object"), "Declare { key: \"INDEXNOW_KEY\" }.");
1761
- else {
1762
- const key = readBindingName(keyPath(indexPath, "key"), indexNow["key"], problems);
1763
- if (siteUrl === null) problems.add(indexPath, "needs the URL of the site", "Set url on the site: IndexNow is told the public URLs of the entries.");
1764
- else if (key !== null) events.indexNow = { key };
1765
- }
1766
- }
1767
- return events;
1768
- }
1769
- //#endregion
1770
1788
  //#region src/config/site-key.ts
1771
1789
  /**
1772
1790
  * The key of a site, which is the id of its row in the database: lowercase letters and digits
@@ -1816,14 +1834,14 @@ function readLocales(path, raw, problems) {
1816
1834
  }
1817
1835
  locales.push(tag);
1818
1836
  if (!LOCALE_TAG.test(tag)) {
1819
- problems.add(tagPath, `${quote(tag)} is not a language tag`, LOCALE_FIX);
1837
+ problems.add(tagPath, `${quote$1(tag)} is not a language tag`, LOCALE_FIX);
1820
1838
  ok = false;
1821
1839
  continue;
1822
1840
  }
1823
1841
  const first = firstAt.get(tag);
1824
1842
  if (first === void 0) firstAt.set(tag, index);
1825
1843
  else {
1826
- problems.add(tagPath, `${quote(tag)} is already listed at ${itemPath(path, first)}`, "List each locale once.");
1844
+ problems.add(tagPath, `${quote$1(tag)} is already listed at ${itemPath(path, first)}`, "List each locale once.");
1827
1845
  ok = false;
1828
1846
  }
1829
1847
  }
@@ -1837,13 +1855,13 @@ function readDefaultLocale(path, raw, locales, problems) {
1837
1855
  problems.add(path, wrongType(raw, "one of the locales"), "Set it to the main language of the site, one of its locales.");
1838
1856
  return "";
1839
1857
  }
1840
- if (locales.length > 0 && !locales.includes(raw)) problems.add(path, `is ${quote(raw)}, which is not one of the locales (${locales.map(quote).join(", ")})`, `Set it to one of ${locales.map(quote).join(", ")}, or add ${quote(raw)} to locales.`);
1858
+ if (locales.length > 0 && !locales.includes(raw)) problems.add(path, `is ${quote$1(raw)}, which is not one of the locales (${locales.map(quote$1).join(", ")})`, `Set it to one of ${locales.map(quote$1).join(", ")}, or add ${quote$1(raw)} to locales.`);
1841
1859
  return raw;
1842
1860
  }
1843
1861
  const COLLECTIONS_FIX = "Declare at least one: collections: { blog: defineCollection({ schema, routes }) }.";
1844
1862
  function readCollections(path, raw, site, problems) {
1845
1863
  const collections = /* @__PURE__ */ new Map();
1846
- if (!isRecord$2(raw)) {
1864
+ if (!isRecord$3(raw)) {
1847
1865
  problems.add(path, wrongType(raw, "an object that maps each collection name to its collection"), COLLECTIONS_FIX);
1848
1866
  return collections;
1849
1867
  }
@@ -1858,8 +1876,8 @@ function readCollections(path, raw, site, problems) {
1858
1876
  /** Checks one site; `null` when it is no object, which is a problem already. */
1859
1877
  function readSite(key, raw, problems) {
1860
1878
  const path = keyPath("sites", key);
1861
- if (!isSiteKey(key)) problems.add(path, `${quote(key)} is not a valid site key`, SITE_KEY_FIX);
1862
- if (!isRecord$2(raw)) {
1879
+ if (!isSiteKey(key)) problems.add(path, `${quote$1(key)} is not a valid site key`, SITE_KEY_FIX);
1880
+ if (!isRecord$3(raw)) {
1863
1881
  problems.add(path, wrongType(raw, "a site"), "Declare it as { locales: [\"en\"], defaultLocale: \"en\", collections: { … } }.");
1864
1882
  return null;
1865
1883
  }
@@ -1886,7 +1904,7 @@ function readSite(key, raw, problems) {
1886
1904
  }
1887
1905
  function readSites(raw, problems) {
1888
1906
  const sites = /* @__PURE__ */ new Map();
1889
- if (!isRecord$2(raw)) {
1907
+ if (!isRecord$3(raw)) {
1890
1908
  problems.add("sites", wrongType(raw, "an object that maps the site key to the site"), SITE_FIX);
1891
1909
  return sites;
1892
1910
  }
@@ -1901,7 +1919,7 @@ function readSites(raw, problems) {
1901
1919
  /** `auth` of the config: absent, or an object whose `dynamicClientRegistration` is a boolean. */
1902
1920
  function readAuth(raw, problems) {
1903
1921
  if (raw === void 0) return { dynamicClientRegistration: false };
1904
- if (!isRecord$2(raw)) {
1922
+ if (!isRecord$3(raw)) {
1905
1923
  problems.add("auth", wrongType(raw, "an object"), "Write auth: { dynamicClientRegistration: true }, or leave auth out.");
1906
1924
  return { dynamicClientRegistration: false };
1907
1925
  }
@@ -1943,7 +1961,7 @@ function resolveConfig(config) {
1943
1961
  const problems = new Problems();
1944
1962
  let sites = /* @__PURE__ */ new Map();
1945
1963
  let auth = { dynamicClientRegistration: false };
1946
- if (isRecord$2(config)) {
1964
+ if (isRecord$3(config)) {
1947
1965
  sites = readSites(config["sites"], problems);
1948
1966
  auth = readAuth(config["auth"], problems);
1949
1967
  } else problems.add("config", wrongType(config, "an object"), "Export the result of defineConfig({ sites: { … } }) from hollware.config.ts.");
@@ -3828,6 +3846,7 @@ const entrySummary = z.object({
3828
3846
  hasUnpublishedChanges: z.boolean().describe("Whether the latest revision differs from the published one."),
3829
3847
  publishedAt: z.string().nullable().describe("When the entry was first published, or null."),
3830
3848
  scheduledAt: z.string().nullable().describe("When a scheduled entry will be published, or null for one that is not scheduled."),
3849
+ deletedAt: z.string().nullable().describe("When the entry went to the trash, or null for one that is not in the trash."),
3831
3850
  updatedAt: z.string()
3832
3851
  });
3833
3852
  const publication = z.object({
@@ -3958,12 +3977,13 @@ const describeSite = defineOperation({
3958
3977
  const listEntries = defineOperation({
3959
3978
  name: "list_entries",
3960
3979
  title: "List entries",
3961
- description: "Lists the entries of a collection, newest first, filtered by locale, tag or status. Pass the nextCursor of an answer as cursor to get the next page.",
3980
+ description: "Lists the entries of a collection, newest first, filtered by locale, tag or status. Pass the nextCursor of an answer as cursor to get the next page. Entries in the trash are left out, unless trashed asks for them.",
3962
3981
  input: z.strictObject({
3963
3982
  collection: collectionName,
3964
3983
  locale: locale.optional(),
3965
3984
  tag: z.string().min(1).optional(),
3966
3985
  status: entryStatus.optional(),
3986
+ trashed: z.boolean().optional().describe("true to list the entries in the trash, last trashed first, instead of the others."),
3967
3987
  limit: z.number().int().min(1).max(100).default(20),
3968
3988
  cursor: z.string().min(1).optional()
3969
3989
  }),
@@ -4046,7 +4066,7 @@ const getTranslations = defineOperation({
4046
4066
  const createDraft = defineOperation({
4047
4067
  name: "create_draft",
4048
4068
  title: "Create a draft",
4049
- description: "Creates an entry from a Markdown file and saves it as a draft at version 1. Contract errors do not stop the draft: it is saved, and the verdict says what to fix before publishing. It is refused only when it cannot be stored: the file is too large, its frontmatter or syntax does not parse, or the slug or translation is taken.",
4069
+ description: "Creates an entry from a Markdown file and saves it as a draft at version 1. Contract errors do not stop the draft: it is saved, and the verdict says what to fix before publishing. It is refused only when it cannot be stored: the file is too large, its frontmatter or syntax does not parse, or the slug or translation is taken. An entry in the trash keeps its slug and translation: the answer is in_trash with that entry, and restore_entry brings it back.",
4050
4070
  input: z.strictObject({
4051
4071
  collection: collectionName,
4052
4072
  locale,
@@ -4054,15 +4074,20 @@ const createDraft = defineOperation({
4054
4074
  markdown
4055
4075
  }),
4056
4076
  output: z.object({
4057
- outcome: z.enum(["saved", "refused"]),
4058
- entry: entrySummary.optional(),
4077
+ outcome: z.enum([
4078
+ "saved",
4079
+ "refused",
4080
+ "in_trash"
4081
+ ]),
4082
+ entry: entrySummary.optional().describe("With saved, the new draft; with in_trash, the entry in the trash that holds it."),
4059
4083
  verdict: verdict.optional()
4060
4084
  }),
4061
4085
  scopes: ["content:write"],
4062
4086
  effect: "write",
4063
4087
  outcomes: {
4064
4088
  saved: { isError: false },
4065
- refused: { isError: true }
4089
+ refused: { isError: true },
4090
+ in_trash: { isError: true }
4066
4091
  }
4067
4092
  });
4068
4093
  const updateDraft = defineOperation({
@@ -4172,6 +4197,54 @@ const unpublish = defineOperation({
4172
4197
  not_found: { isError: true }
4173
4198
  }
4174
4199
  });
4200
+ const deleteEntry = defineOperation({
4201
+ name: "delete_entry",
4202
+ title: "Move an entry to the trash",
4203
+ description: "Moves a draft to the trash, if it is still at the version you read: every operation treats it as gone, its pending review is cancelled, and nothing is deleted. It keeps its slug and translation, and restore_entry brings it back as the draft it was. A published or scheduled entry answers published: unpublish it first.",
4204
+ input: z.strictObject({
4205
+ id: entryId,
4206
+ version
4207
+ }),
4208
+ output: z.object({
4209
+ outcome: z.enum([
4210
+ "trashed",
4211
+ "published",
4212
+ "conflict",
4213
+ "not_found"
4214
+ ]),
4215
+ entry: entrySummary.optional(),
4216
+ conflict: conflict.optional()
4217
+ }),
4218
+ scopes: ["content:write"],
4219
+ effect: "destructive",
4220
+ outcomes: {
4221
+ trashed: { isError: false },
4222
+ published: { isError: true },
4223
+ conflict: { isError: true },
4224
+ not_found: { isError: true }
4225
+ }
4226
+ });
4227
+ const restoreEntry = defineOperation({
4228
+ name: "restore_entry",
4229
+ title: "Bring an entry back from the trash",
4230
+ description: "Brings an entry back from the trash as the draft it was, with its revisions, at the next version. An entry that is not in the trash answers not_trashed.",
4231
+ input: z.strictObject({ id: entryId }),
4232
+ output: z.object({
4233
+ outcome: z.enum([
4234
+ "restored",
4235
+ "not_trashed",
4236
+ "not_found"
4237
+ ]),
4238
+ entry: entrySummary.optional()
4239
+ }),
4240
+ scopes: ["content:write"],
4241
+ effect: "write",
4242
+ outcomes: {
4243
+ restored: { isError: false },
4244
+ not_trashed: { isError: true },
4245
+ not_found: { isError: true }
4246
+ }
4247
+ });
4175
4248
  const listReviews = defineOperation({
4176
4249
  name: "list_reviews",
4177
4250
  title: "List the reviews",
@@ -4487,6 +4560,7 @@ const OPERATIONS = [
4487
4560
  contentStats,
4488
4561
  createDraft,
4489
4562
  createSite,
4563
+ deleteEntry,
4490
4564
  deleteSite,
4491
4565
  describeSite,
4492
4566
  getEntry,
@@ -4518,6 +4592,7 @@ const OPERATIONS = [
4518
4592
  publish,
4519
4593
  requestChanges,
4520
4594
  requestReview,
4595
+ restoreEntry,
4521
4596
  restoreSite,
4522
4597
  search,
4523
4598
  setCollection,
@@ -4638,6 +4713,63 @@ async function verifyWebhook(check) {
4638
4713
  return sameText(signature, await signWebhook(check.secret, timestamp, check.body));
4639
4714
  }
4640
4715
  //#endregion
4716
+ //#region src/events.ts
4717
+ /**
4718
+ * What Hollware sends to a site's webhooks (ADR-025), signed as `signWebhook` signs it: an event
4719
+ * about an entry that was published or unpublished, or about a change of the site's structure
4720
+ * (ADR-033). Every event carries the cache tags it invalidates (`@hollware/core`'s cache tags), so
4721
+ * a site that caches its pages purges them with no lookup. A receiver checks the signature with
4722
+ * `verifyWebhook`, then reads the body with `parseHollwareEvent`. Fields may be added in a minor
4723
+ * version, and kinds too: a receiver ignores a kind it does not know.
4724
+ */
4725
+ const base = {
4726
+ /** The id of the event: the same on every retry of its delivery. */
4727
+ id: z.string(),
4728
+ /** The key of the site. */
4729
+ site: z.string(),
4730
+ /** When the change was written, in ISO 8601. */
4731
+ at: z.string(),
4732
+ /** The cache tags the change invalidates. */
4733
+ cacheTags: z.array(z.string())
4734
+ };
4735
+ /** A publish or an unpublish: the entry, and where it lives on the site. */
4736
+ const entryEvent = z.object({
4737
+ ...base,
4738
+ kind: z.enum(["entry.published", "entry.unpublished"]),
4739
+ entry: z.object({
4740
+ id: z.string(),
4741
+ collection: z.string(),
4742
+ locale: z.string(),
4743
+ slug: z.string(),
4744
+ translationGroup: z.string().nullable()
4745
+ }),
4746
+ /** The path of the entry on the site, from the route of its collection and locale. */
4747
+ path: z.string().nullable(),
4748
+ /** The URL of the entry, when the site has a `url`. */
4749
+ url: z.string().nullable()
4750
+ });
4751
+ /** A change of the site's structure: no entry, the tags to purge. */
4752
+ const siteEvent = z.object({
4753
+ ...base,
4754
+ kind: z.literal("site.changed")
4755
+ });
4756
+ /** Every event a webhook gets. */
4757
+ const hollwareEvent = z.discriminatedUnion("kind", [entryEvent, siteEvent]);
4758
+ /**
4759
+ * The event of a webhook's body, once its signature holds, or `null` for a body that is not one
4760
+ * of the events this version knows (a newer kind, say): a receiver skips it.
4761
+ */
4762
+ function parseHollwareEvent(body) {
4763
+ let json;
4764
+ try {
4765
+ json = JSON.parse(body);
4766
+ } catch {
4767
+ return null;
4768
+ }
4769
+ const parsed = hollwareEvent.safeParse(json);
4770
+ return parsed.success ? parsed.data : null;
4771
+ }
4772
+ //#endregion
4641
4773
  //#region src/cache.ts
4642
4774
  /**
4643
4775
  * The cache tags of Hollware's content (S8): one name for each thing a page can show, so that a
@@ -4688,8 +4820,19 @@ function tagsOfChange(entry) {
4688
4820
  ...entry.translationGroup === null ? [] : [groupTag(entry.collection, entry.translationGroup)]
4689
4821
  ];
4690
4822
  }
4823
+ /**
4824
+ * The tags a change of a site's structure invalidates (ADR-033): the pages that show the structure
4825
+ * (`site`, such as a navigation), and every list of its collections in each locale and in every
4826
+ * locale, as a list may change with a schema, a route or a collection that goes. `collections` and
4827
+ * `locales` are those of the definition before and after the change, together.
4828
+ */
4829
+ function tagsOfSiteChange(collections, locales) {
4830
+ const tags = /* @__PURE__ */ new Set([SITE_TAG]);
4831
+ for (const collection of collections) for (const locale of [...locales, "*"]) tags.add(listTag(collection, locale));
4832
+ return [...tags];
4833
+ }
4691
4834
  //#endregion
4692
4835
  //#region src/index.ts
4693
4836
  const name = "@hollware/core";
4694
4837
  //#endregion
4695
- export { BUILT_IN_CONTRACT_IDS, CATALOG, CHUNKER_VERSION, CHUNK_LIMITS, COMARK_VERSION, ConfigError, DEFAULT_MAX_MARKDOWN_BYTES, ERROR_CODES, EVENT_ID_HEADER, EVERY_LOCALE, HollwareError, ID_PREFIXES, MAX_MARKDOWN_BYTES_CEILING, OPERATIONS, PERSONAL_TOKEN_PREFIX, PERSONAL_TOKEN_TTL_DAYS, PIPELINE_VERSION, PRESETS, READ_TOKEN_PREFIX, REFUSAL_CONTRACT_IDS, SCOPES, SIGNATURE_HEADER, SITE_KEY_MAX_LENGTH, SITE_KEY_PATTERN, SITE_TAG, TIMESTAMP_HEADER, approveReview, canonicalJson, catalogRule, chunksOf, collectionDefinitionData, collectionPreset, commentReview, compileSites, componentDefinitionData, contentStats, contracts, createDraft, defineCollection, defineComponent, defineConfig, defineContract, defineOperation, definitionsFromConfig, describeBuiltInContracts, describeCatalog, describeContract, describeResolvedSite, describeSite, describedSchemaOf, entryTag, generatePersonalToken, generateReadToken, getEntry, getTranslations, groupTag, hashPersonalToken, isBuiltInContract, isId, isPersonalToken, isReadToken, isScope, isSiteKey, listEntries, listReviews, listTag, name, newId, processMarkdown, publish, renderHtml, requestChanges, requestReview, resolveConfig, routesFor, ruleDefinition, runContracts, schemas_exports as schemas, search, signWebhook, siteDefinitionData, sitePreset, sizeLimitViolation, tagsOfChange, tagsOfEntries, ulid, uniqueSlugViolation, uniqueTranslationViolation, unpublish, updateDraft, validate, verifyWebhook, webhookDefinition };
4838
+ export { BUILT_IN_CONTRACT_IDS, CATALOG, CHUNKER_VERSION, CHUNK_LIMITS, COMARK_VERSION, ConfigError, DEFAULT_MAX_MARKDOWN_BYTES, ERROR_CODES, EVENT_ID_HEADER, EVERY_LOCALE, HollwareError, ID_PREFIXES, INDEXNOW_KEY, MAX_MARKDOWN_BYTES_CEILING, OPERATIONS, PERSONAL_TOKEN_PREFIX, PERSONAL_TOKEN_TTL_DAYS, PIPELINE_VERSION, PRESETS, READ_TOKEN_PREFIX, REFUSAL_CONTRACT_IDS, SCOPES, SIGNATURE_HEADER, SITE_KEY_MAX_LENGTH, SITE_KEY_PATTERN, SITE_TAG, TIMESTAMP_HEADER, approveReview, canonicalJson, catalogRule, chunksOf, collectionDefinitionData, collectionPreset, commentReview, compileSites, componentDefinitionData, contentStats, contracts, createDraft, defineCollection, defineComponent, defineConfig, defineContract, defineOperation, definitionsFromConfig, deleteEntry, describeBuiltInContracts, describeCatalog, describeContract, describeResolvedSite, describeSite, describedSchemaOf, entryEvent, entryTag, generatePersonalToken, generateReadToken, getEntry, getTranslations, groupTag, hashPersonalToken, hollwareEvent, isBuiltInContract, isId, isPersonalToken, isReadToken, isScope, isSiteKey, listEntries, listReviews, listTag, name, newId, parseHollwareEvent, processMarkdown, publish, renderHtml, requestChanges, requestReview, resolveConfig, restoreEntry, routesFor, ruleDefinition, runContracts, schemas_exports as schemas, search, signWebhook, siteDefinitionData, siteEvent, sitePreset, sizeLimitViolation, tagsOfChange, tagsOfEntries, tagsOfSiteChange, ulid, uniqueSlugViolation, uniqueTranslationViolation, unpublish, updateDraft, validate, verifyWebhook, webhookDefinition };