@hollware/core 0.1.0 → 0.3.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.d.mts CHANGED
@@ -288,6 +288,21 @@ interface SiteDefinition {
288
288
  readonly collections: Readonly<Record<string, CollectionDefinition>>;
289
289
  /** Guidance for whoever writes anywhere in the site, in Markdown. */
290
290
  readonly styleGuide?: string;
291
+ /** The public origin of the site, such as `https://example.com`: entries' URLs start with it. */
292
+ readonly url?: string;
293
+ /** Who hears about publications (ADR-025). */
294
+ readonly events?: {
295
+ /** Each gets every event, signed with HMAC-SHA256 by the Worker secret named in `secret`. */
296
+ readonly webhooks?: readonly {
297
+ readonly name: string;
298
+ readonly url: string;
299
+ readonly secret: string;
300
+ }[];
301
+ /** Tells IndexNow the URLs that changed; `key` names the Worker secret with the key. */
302
+ readonly indexNow?: {
303
+ readonly key: string;
304
+ };
305
+ };
291
306
  }
292
307
  /** What `hollware.config.ts` exports. */
293
308
  interface HollwareConfig {
@@ -296,6 +311,17 @@ interface HollwareConfig {
296
311
  * content exists. v0.1 serves exactly one site per deployment.
297
312
  */
298
313
  readonly sites: Readonly<Record<string, SiteDefinition>>;
314
+ /** How MCP clients and the admin get tokens (ADR-026). */
315
+ readonly auth?: AuthDefinition;
316
+ }
317
+ /** The OAuth server of a deployment. */
318
+ interface AuthDefinition {
319
+ /**
320
+ * Whether a client may register itself (RFC 7591), for MCP clients that cannot present a
321
+ * Client ID Metadata Document. Off by default: a registration is a write to KV that anyone can
322
+ * make, and MCP 2026-07-28 prefers metadata documents.
323
+ */
324
+ readonly dynamicClientRegistration?: boolean;
299
325
  }
300
326
  /** A collection once the config is resolved: checked, with every default filled in. */
301
327
  interface ResolvedCollection {
@@ -318,10 +344,31 @@ interface ResolvedSite {
318
344
  readonly defaultLocale: string;
319
345
  readonly styleGuide: string | null;
320
346
  readonly collections: ReadonlyMap<string, ResolvedCollection>;
347
+ /** The public origin of the site, or `null`. */
348
+ readonly url: string | null;
349
+ readonly events: ResolvedEvents;
350
+ }
351
+ /** A webhook of a site; `secret` names the Worker secret that signs its deliveries. */
352
+ interface ResolvedWebhook {
353
+ readonly name: string;
354
+ readonly url: string;
355
+ readonly secret: string;
356
+ }
357
+ /** Who hears about the publications of a site. */
358
+ interface ResolvedEvents {
359
+ readonly webhooks: readonly ResolvedWebhook[];
360
+ readonly indexNow: {
361
+ readonly key: string;
362
+ } | null;
321
363
  }
322
364
  /** The config once resolved, at the start of the Worker. v0.1 holds exactly one site. */
323
365
  interface ResolvedConfig {
324
366
  readonly sites: ReadonlyMap<string, ResolvedSite>;
367
+ readonly auth: ResolvedAuth;
368
+ }
369
+ /** The OAuth server once the config is resolved. */
370
+ interface ResolvedAuth {
371
+ readonly dynamicClientRegistration: boolean;
325
372
  }
326
373
  /**
327
374
  * Checks a config and fills in its defaults. It throws one error that lists every problem, each
@@ -381,7 +428,7 @@ export declare const DEFAULT_MAX_MARKDOWN_BYTES = 524288;
381
428
  */
382
429
  export declare const MAX_MARKDOWN_BYTES_CEILING = 786432;
383
430
  declare namespace schemas_d_exports {
384
- export { acknowledgedWarning, collectionDescription, collectionName, componentDescription, conflict, contractDescription, document, entry, entryId, entryStatus, entrySummary, format, locale, markdown, phase, publication, revisionId, severity, siteDescription, slug, verdict, version, violation };
431
+ export { acknowledgedWarning, collectionDescription, collectionName, componentDescription, conflict, contractDescription, document, entry, entryId, entryStatus, entrySummary, format, locale, markdown, phase, publication, review, reviewId, reviewStatus, revisionId, severity, siteDescription, slug, verdict, version, violation };
385
432
  }
386
433
  /**
387
434
  * The pieces the operation schemas share. Their descriptions become the JSON Schema that agents
@@ -608,6 +655,34 @@ declare const entry: z.ZodObject<{
608
655
  }, z.core.$strip>>;
609
656
  html: z.ZodOptional<z.ZodString>;
610
657
  }, z.core.$strip>;
658
+ declare const reviewId: z.ZodString;
659
+ declare const reviewStatus: z.ZodEnum<{
660
+ approved: "approved";
661
+ cancelled: "cancelled";
662
+ changes_requested: "changes_requested";
663
+ pending: "pending";
664
+ }>;
665
+ /**
666
+ * A request to publish a revision, made by someone who may write but not publish, and what a
667
+ * person who may publish decided about it.
668
+ */
669
+ declare const review: z.ZodObject<{
670
+ id: z.ZodString;
671
+ entryId: z.ZodString;
672
+ revision: z.ZodString;
673
+ status: z.ZodEnum<{
674
+ approved: "approved";
675
+ cancelled: "cancelled";
676
+ changes_requested: "changes_requested";
677
+ pending: "pending";
678
+ }>;
679
+ requestedBy: z.ZodString;
680
+ requestedByName: z.ZodNullable<z.ZodString>;
681
+ requestedAt: z.ZodString;
682
+ decidedBy: z.ZodNullable<z.ZodString>;
683
+ decidedAt: z.ZodNullable<z.ZodString>;
684
+ note: z.ZodNullable<z.ZodString>;
685
+ }, z.core.$strip>;
611
686
  declare const conflict: z.ZodObject<{
612
687
  currentVersion: z.ZodNumber;
613
688
  headRevision: z.ZodString;
@@ -960,10 +1035,19 @@ export declare class HollwareError extends Error {
960
1035
  */
961
1036
  /** The kinds of ids and their prefixes. A new kind adds its prefix here. */
962
1037
  export declare const ID_PREFIXES: {
1038
+ readonly challenge: 'chl';
1039
+ readonly delivery: 'dlv';
963
1040
  readonly entry: 'ent';
1041
+ readonly event: 'evt';
1042
+ readonly invitation: 'inv';
1043
+ readonly member: 'mbr';
1044
+ readonly passkey: 'pky';
964
1045
  readonly publication: 'pub';
965
1046
  readonly revision: 'rev';
966
1047
  readonly revisionTag: 'tag';
1048
+ readonly review: 'rvw';
1049
+ readonly schedule: 'sch';
1050
+ readonly session: 'ses';
967
1051
  readonly token: 'tok';
968
1052
  readonly user: 'usr';
969
1053
  };
@@ -1152,6 +1236,7 @@ export declare const getEntry: OperationSpec<"get_entry", z.ZodObject<{
1152
1236
  html: "html";
1153
1237
  markdown: "markdown";
1154
1238
  }>>>;
1239
+ published: z.ZodDefault<z.ZodBoolean>;
1155
1240
  }, z.core.$strict>, z.ZodObject<{
1156
1241
  outcome: z.ZodEnum<{
1157
1242
  not_found: "not_found";
@@ -1450,13 +1535,20 @@ export declare const publish: OperationSpec<"publish", z.ZodObject<{
1450
1535
  contract: z.ZodString;
1451
1536
  path: z.ZodString;
1452
1537
  }, z.core.$strip>>>;
1538
+ group: z.ZodDefault<z.ZodArray<z.ZodObject<{
1539
+ id: z.ZodString;
1540
+ version: z.ZodNumber;
1541
+ }, z.core.$strict>>>;
1542
+ publishAt: z.ZodOptional<z.ZodISODateTime>;
1453
1543
  }, z.core.$strict>, z.ZodObject<{
1454
1544
  outcome: z.ZodEnum<{
1455
1545
  blocked: "blocked";
1456
1546
  conflict: "conflict";
1457
1547
  needs_acknowledgement: "needs_acknowledgement";
1458
1548
  not_found: "not_found";
1549
+ pending_review: "pending_review";
1459
1550
  published: "published";
1551
+ scheduled: "scheduled";
1460
1552
  }>;
1461
1553
  entry: z.ZodOptional<z.ZodObject<{
1462
1554
  id: z.ZodString;
@@ -1510,6 +1602,24 @@ export declare const publish: OperationSpec<"publish", z.ZodObject<{
1510
1602
  currentVersion: z.ZodNumber;
1511
1603
  headRevision: z.ZodString;
1512
1604
  }, z.core.$strip>>;
1605
+ review: z.ZodOptional<z.ZodObject<{
1606
+ id: z.ZodString;
1607
+ entryId: z.ZodString;
1608
+ revision: z.ZodString;
1609
+ status: z.ZodEnum<{
1610
+ approved: "approved";
1611
+ cancelled: "cancelled";
1612
+ changes_requested: "changes_requested";
1613
+ pending: "pending";
1614
+ }>;
1615
+ requestedBy: z.ZodString;
1616
+ requestedByName: z.ZodNullable<z.ZodString>;
1617
+ requestedAt: z.ZodString;
1618
+ decidedBy: z.ZodNullable<z.ZodString>;
1619
+ decidedAt: z.ZodNullable<z.ZodString>;
1620
+ note: z.ZodNullable<z.ZodString>;
1621
+ }, z.core.$strip>>;
1622
+ failedEntry: z.ZodOptional<z.ZodString>;
1513
1623
  }, z.core.$strip>>;
1514
1624
  export declare const unpublish: OperationSpec<"unpublish", z.ZodObject<{
1515
1625
  id: z.ZodString;
@@ -1537,8 +1647,242 @@ export declare const unpublish: OperationSpec<"unpublish", z.ZodObject<{
1537
1647
  updatedAt: z.ZodString;
1538
1648
  }, z.core.$strip>>;
1539
1649
  }, z.core.$strip>>;
1540
- /** Every operation of v0.1, sorted by name: the order of `tools/list`. */
1541
- export declare const OPERATIONS: readonly [OperationSpec<"create_draft", z.ZodObject<{
1650
+ export declare const listReviews: OperationSpec<"list_reviews", z.ZodObject<{
1651
+ status: z.ZodDefault<z.ZodEnum<{
1652
+ approved: "approved";
1653
+ cancelled: "cancelled";
1654
+ changes_requested: "changes_requested";
1655
+ pending: "pending";
1656
+ }>>;
1657
+ limit: z.ZodDefault<z.ZodNumber>;
1658
+ }, z.core.$strict>, z.ZodObject<{
1659
+ outcome: z.ZodEnum<{
1660
+ ok: "ok";
1661
+ }>;
1662
+ reviews: z.ZodArray<z.ZodObject<{
1663
+ id: z.ZodString;
1664
+ entryId: z.ZodString;
1665
+ revision: z.ZodString;
1666
+ status: z.ZodEnum<{
1667
+ approved: "approved";
1668
+ cancelled: "cancelled";
1669
+ changes_requested: "changes_requested";
1670
+ pending: "pending";
1671
+ }>;
1672
+ requestedBy: z.ZodString;
1673
+ requestedByName: z.ZodNullable<z.ZodString>;
1674
+ requestedAt: z.ZodString;
1675
+ decidedBy: z.ZodNullable<z.ZodString>;
1676
+ decidedAt: z.ZodNullable<z.ZodString>;
1677
+ note: z.ZodNullable<z.ZodString>;
1678
+ entry: z.ZodObject<{
1679
+ id: z.ZodString;
1680
+ collection: z.ZodString;
1681
+ locale: z.ZodString;
1682
+ slug: z.ZodString;
1683
+ translationGroup: z.ZodNullable<z.ZodString>;
1684
+ status: z.ZodEnum<{
1685
+ draft: "draft";
1686
+ published: "published";
1687
+ scheduled: "scheduled";
1688
+ }>;
1689
+ version: z.ZodNumber;
1690
+ frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
1691
+ tags: z.ZodArray<z.ZodString>;
1692
+ hasUnpublishedChanges: z.ZodBoolean;
1693
+ publishedAt: z.ZodNullable<z.ZodString>;
1694
+ updatedAt: z.ZodString;
1695
+ }, z.core.$strip>;
1696
+ }, z.core.$strip>>;
1697
+ }, z.core.$strip>>;
1698
+ export declare const approveReview: OperationSpec<"approve_review", z.ZodObject<{
1699
+ id: z.ZodString;
1700
+ acknowledgeWarnings: z.ZodDefault<z.ZodArray<z.ZodObject<{
1701
+ contract: z.ZodString;
1702
+ path: z.ZodString;
1703
+ }, z.core.$strip>>>;
1704
+ }, z.core.$strict>, z.ZodObject<{
1705
+ outcome: z.ZodEnum<{
1706
+ blocked: "blocked";
1707
+ needs_acknowledgement: "needs_acknowledgement";
1708
+ not_found: "not_found";
1709
+ not_pending: "not_pending";
1710
+ published: "published";
1711
+ }>;
1712
+ review: z.ZodOptional<z.ZodObject<{
1713
+ id: z.ZodString;
1714
+ entryId: z.ZodString;
1715
+ revision: z.ZodString;
1716
+ status: z.ZodEnum<{
1717
+ approved: "approved";
1718
+ cancelled: "cancelled";
1719
+ changes_requested: "changes_requested";
1720
+ pending: "pending";
1721
+ }>;
1722
+ requestedBy: z.ZodString;
1723
+ requestedByName: z.ZodNullable<z.ZodString>;
1724
+ requestedAt: z.ZodString;
1725
+ decidedBy: z.ZodNullable<z.ZodString>;
1726
+ decidedAt: z.ZodNullable<z.ZodString>;
1727
+ note: z.ZodNullable<z.ZodString>;
1728
+ }, z.core.$strip>>;
1729
+ entry: z.ZodOptional<z.ZodObject<{
1730
+ id: z.ZodString;
1731
+ collection: z.ZodString;
1732
+ locale: z.ZodString;
1733
+ slug: z.ZodString;
1734
+ translationGroup: z.ZodNullable<z.ZodString>;
1735
+ status: z.ZodEnum<{
1736
+ draft: "draft";
1737
+ published: "published";
1738
+ scheduled: "scheduled";
1739
+ }>;
1740
+ version: z.ZodNumber;
1741
+ frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
1742
+ tags: z.ZodArray<z.ZodString>;
1743
+ hasUnpublishedChanges: z.ZodBoolean;
1744
+ publishedAt: z.ZodNullable<z.ZodString>;
1745
+ updatedAt: z.ZodString;
1746
+ }, z.core.$strip>>;
1747
+ verdict: z.ZodOptional<z.ZodObject<{
1748
+ ok: z.ZodBoolean;
1749
+ phase: z.ZodEnum<{
1750
+ publish: "publish";
1751
+ write: "write";
1752
+ }>;
1753
+ errors: z.ZodArray<z.ZodObject<{
1754
+ contract: z.ZodString;
1755
+ severity: z.ZodEnum<{
1756
+ error: "error";
1757
+ warning: "warning";
1758
+ }>;
1759
+ path: z.ZodString;
1760
+ message: z.ZodString;
1761
+ fix: z.ZodString;
1762
+ line: z.ZodOptional<z.ZodNumber>;
1763
+ }, z.core.$strip>>;
1764
+ warnings: z.ZodArray<z.ZodObject<{
1765
+ contract: z.ZodString;
1766
+ severity: z.ZodEnum<{
1767
+ error: "error";
1768
+ warning: "warning";
1769
+ }>;
1770
+ path: z.ZodString;
1771
+ message: z.ZodString;
1772
+ fix: z.ZodString;
1773
+ line: z.ZodOptional<z.ZodNumber>;
1774
+ }, z.core.$strip>>;
1775
+ contractsHash: z.ZodString;
1776
+ }, z.core.$strip>>;
1777
+ }, z.core.$strip>>;
1778
+ export declare const requestChanges: OperationSpec<"request_changes", z.ZodObject<{
1779
+ id: z.ZodString;
1780
+ note: z.ZodString;
1781
+ }, z.core.$strict>, z.ZodObject<{
1782
+ outcome: z.ZodEnum<{
1783
+ changes_requested: "changes_requested";
1784
+ not_found: "not_found";
1785
+ not_pending: "not_pending";
1786
+ }>;
1787
+ review: z.ZodOptional<z.ZodObject<{
1788
+ id: z.ZodString;
1789
+ entryId: z.ZodString;
1790
+ revision: z.ZodString;
1791
+ status: z.ZodEnum<{
1792
+ approved: "approved";
1793
+ cancelled: "cancelled";
1794
+ changes_requested: "changes_requested";
1795
+ pending: "pending";
1796
+ }>;
1797
+ requestedBy: z.ZodString;
1798
+ requestedByName: z.ZodNullable<z.ZodString>;
1799
+ requestedAt: z.ZodString;
1800
+ decidedBy: z.ZodNullable<z.ZodString>;
1801
+ decidedAt: z.ZodNullable<z.ZodString>;
1802
+ note: z.ZodNullable<z.ZodString>;
1803
+ }, z.core.$strip>>;
1804
+ }, z.core.$strip>>;
1805
+ /** Every operation, sorted by name: the order of `tools/list`. */
1806
+ export declare const OPERATIONS: readonly [OperationSpec<"approve_review", z.ZodObject<{
1807
+ id: z.ZodString;
1808
+ acknowledgeWarnings: z.ZodDefault<z.ZodArray<z.ZodObject<{
1809
+ contract: z.ZodString;
1810
+ path: z.ZodString;
1811
+ }, z.core.$strip>>>;
1812
+ }, z.core.$strict>, z.ZodObject<{
1813
+ outcome: z.ZodEnum<{
1814
+ blocked: "blocked";
1815
+ needs_acknowledgement: "needs_acknowledgement";
1816
+ not_found: "not_found";
1817
+ not_pending: "not_pending";
1818
+ published: "published";
1819
+ }>;
1820
+ review: z.ZodOptional<z.ZodObject<{
1821
+ id: z.ZodString;
1822
+ entryId: z.ZodString;
1823
+ revision: z.ZodString;
1824
+ status: z.ZodEnum<{
1825
+ approved: "approved";
1826
+ cancelled: "cancelled";
1827
+ changes_requested: "changes_requested";
1828
+ pending: "pending";
1829
+ }>;
1830
+ requestedBy: z.ZodString;
1831
+ requestedByName: z.ZodNullable<z.ZodString>;
1832
+ requestedAt: z.ZodString;
1833
+ decidedBy: z.ZodNullable<z.ZodString>;
1834
+ decidedAt: z.ZodNullable<z.ZodString>;
1835
+ note: z.ZodNullable<z.ZodString>;
1836
+ }, z.core.$strip>>;
1837
+ entry: z.ZodOptional<z.ZodObject<{
1838
+ id: z.ZodString;
1839
+ collection: z.ZodString;
1840
+ locale: z.ZodString;
1841
+ slug: z.ZodString;
1842
+ translationGroup: z.ZodNullable<z.ZodString>;
1843
+ status: z.ZodEnum<{
1844
+ draft: "draft";
1845
+ published: "published";
1846
+ scheduled: "scheduled";
1847
+ }>;
1848
+ version: z.ZodNumber;
1849
+ frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
1850
+ tags: z.ZodArray<z.ZodString>;
1851
+ hasUnpublishedChanges: z.ZodBoolean;
1852
+ publishedAt: z.ZodNullable<z.ZodString>;
1853
+ updatedAt: z.ZodString;
1854
+ }, z.core.$strip>>;
1855
+ verdict: z.ZodOptional<z.ZodObject<{
1856
+ ok: z.ZodBoolean;
1857
+ phase: z.ZodEnum<{
1858
+ publish: "publish";
1859
+ write: "write";
1860
+ }>;
1861
+ errors: z.ZodArray<z.ZodObject<{
1862
+ contract: z.ZodString;
1863
+ severity: z.ZodEnum<{
1864
+ error: "error";
1865
+ warning: "warning";
1866
+ }>;
1867
+ path: z.ZodString;
1868
+ message: z.ZodString;
1869
+ fix: z.ZodString;
1870
+ line: z.ZodOptional<z.ZodNumber>;
1871
+ }, z.core.$strip>>;
1872
+ warnings: z.ZodArray<z.ZodObject<{
1873
+ contract: z.ZodString;
1874
+ severity: z.ZodEnum<{
1875
+ error: "error";
1876
+ warning: "warning";
1877
+ }>;
1878
+ path: z.ZodString;
1879
+ message: z.ZodString;
1880
+ fix: z.ZodString;
1881
+ line: z.ZodOptional<z.ZodNumber>;
1882
+ }, z.core.$strip>>;
1883
+ contractsHash: z.ZodString;
1884
+ }, z.core.$strip>>;
1885
+ }, z.core.$strip>>, OperationSpec<"create_draft", z.ZodObject<{
1542
1886
  collection: z.ZodString;
1543
1887
  locale: z.ZodString;
1544
1888
  slug: z.ZodString;
@@ -1643,6 +1987,7 @@ export declare const OPERATIONS: readonly [OperationSpec<"create_draft", z.ZodOb
1643
1987
  html: "html";
1644
1988
  markdown: "markdown";
1645
1989
  }>>>;
1990
+ published: z.ZodDefault<z.ZodBoolean>;
1646
1991
  }, z.core.$strict>, z.ZodObject<{
1647
1992
  outcome: z.ZodEnum<{
1648
1993
  not_found: "not_found";
@@ -1802,6 +2147,53 @@ export declare const OPERATIONS: readonly [OperationSpec<"create_draft", z.ZodOb
1802
2147
  updatedAt: z.ZodString;
1803
2148
  }, z.core.$strip>>;
1804
2149
  nextCursor: z.ZodNullable<z.ZodString>;
2150
+ }, z.core.$strip>>, OperationSpec<"list_reviews", z.ZodObject<{
2151
+ status: z.ZodDefault<z.ZodEnum<{
2152
+ approved: "approved";
2153
+ cancelled: "cancelled";
2154
+ changes_requested: "changes_requested";
2155
+ pending: "pending";
2156
+ }>>;
2157
+ limit: z.ZodDefault<z.ZodNumber>;
2158
+ }, z.core.$strict>, z.ZodObject<{
2159
+ outcome: z.ZodEnum<{
2160
+ ok: "ok";
2161
+ }>;
2162
+ reviews: z.ZodArray<z.ZodObject<{
2163
+ id: z.ZodString;
2164
+ entryId: z.ZodString;
2165
+ revision: z.ZodString;
2166
+ status: z.ZodEnum<{
2167
+ approved: "approved";
2168
+ cancelled: "cancelled";
2169
+ changes_requested: "changes_requested";
2170
+ pending: "pending";
2171
+ }>;
2172
+ requestedBy: z.ZodString;
2173
+ requestedByName: z.ZodNullable<z.ZodString>;
2174
+ requestedAt: z.ZodString;
2175
+ decidedBy: z.ZodNullable<z.ZodString>;
2176
+ decidedAt: z.ZodNullable<z.ZodString>;
2177
+ note: z.ZodNullable<z.ZodString>;
2178
+ entry: z.ZodObject<{
2179
+ id: z.ZodString;
2180
+ collection: z.ZodString;
2181
+ locale: z.ZodString;
2182
+ slug: z.ZodString;
2183
+ translationGroup: z.ZodNullable<z.ZodString>;
2184
+ status: z.ZodEnum<{
2185
+ draft: "draft";
2186
+ published: "published";
2187
+ scheduled: "scheduled";
2188
+ }>;
2189
+ version: z.ZodNumber;
2190
+ frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
2191
+ tags: z.ZodArray<z.ZodString>;
2192
+ hasUnpublishedChanges: z.ZodBoolean;
2193
+ publishedAt: z.ZodNullable<z.ZodString>;
2194
+ updatedAt: z.ZodString;
2195
+ }, z.core.$strip>;
2196
+ }, z.core.$strip>>;
1805
2197
  }, z.core.$strip>>, OperationSpec<"publish", z.ZodObject<{
1806
2198
  id: z.ZodString;
1807
2199
  version: z.ZodNumber;
@@ -1809,13 +2201,20 @@ export declare const OPERATIONS: readonly [OperationSpec<"create_draft", z.ZodOb
1809
2201
  contract: z.ZodString;
1810
2202
  path: z.ZodString;
1811
2203
  }, z.core.$strip>>>;
2204
+ group: z.ZodDefault<z.ZodArray<z.ZodObject<{
2205
+ id: z.ZodString;
2206
+ version: z.ZodNumber;
2207
+ }, z.core.$strict>>>;
2208
+ publishAt: z.ZodOptional<z.ZodISODateTime>;
1812
2209
  }, z.core.$strict>, z.ZodObject<{
1813
2210
  outcome: z.ZodEnum<{
1814
2211
  blocked: "blocked";
1815
2212
  conflict: "conflict";
1816
2213
  needs_acknowledgement: "needs_acknowledgement";
1817
2214
  not_found: "not_found";
2215
+ pending_review: "pending_review";
1818
2216
  published: "published";
2217
+ scheduled: "scheduled";
1819
2218
  }>;
1820
2219
  entry: z.ZodOptional<z.ZodObject<{
1821
2220
  id: z.ZodString;
@@ -1869,6 +2268,50 @@ export declare const OPERATIONS: readonly [OperationSpec<"create_draft", z.ZodOb
1869
2268
  currentVersion: z.ZodNumber;
1870
2269
  headRevision: z.ZodString;
1871
2270
  }, z.core.$strip>>;
2271
+ review: z.ZodOptional<z.ZodObject<{
2272
+ id: z.ZodString;
2273
+ entryId: z.ZodString;
2274
+ revision: z.ZodString;
2275
+ status: z.ZodEnum<{
2276
+ approved: "approved";
2277
+ cancelled: "cancelled";
2278
+ changes_requested: "changes_requested";
2279
+ pending: "pending";
2280
+ }>;
2281
+ requestedBy: z.ZodString;
2282
+ requestedByName: z.ZodNullable<z.ZodString>;
2283
+ requestedAt: z.ZodString;
2284
+ decidedBy: z.ZodNullable<z.ZodString>;
2285
+ decidedAt: z.ZodNullable<z.ZodString>;
2286
+ note: z.ZodNullable<z.ZodString>;
2287
+ }, z.core.$strip>>;
2288
+ failedEntry: z.ZodOptional<z.ZodString>;
2289
+ }, z.core.$strip>>, OperationSpec<"request_changes", z.ZodObject<{
2290
+ id: z.ZodString;
2291
+ note: z.ZodString;
2292
+ }, z.core.$strict>, z.ZodObject<{
2293
+ outcome: z.ZodEnum<{
2294
+ changes_requested: "changes_requested";
2295
+ not_found: "not_found";
2296
+ not_pending: "not_pending";
2297
+ }>;
2298
+ review: z.ZodOptional<z.ZodObject<{
2299
+ id: z.ZodString;
2300
+ entryId: z.ZodString;
2301
+ revision: z.ZodString;
2302
+ status: z.ZodEnum<{
2303
+ approved: "approved";
2304
+ cancelled: "cancelled";
2305
+ changes_requested: "changes_requested";
2306
+ pending: "pending";
2307
+ }>;
2308
+ requestedBy: z.ZodString;
2309
+ requestedByName: z.ZodNullable<z.ZodString>;
2310
+ requestedAt: z.ZodString;
2311
+ decidedBy: z.ZodNullable<z.ZodString>;
2312
+ decidedAt: z.ZodNullable<z.ZodString>;
2313
+ note: z.ZodNullable<z.ZodString>;
2314
+ }, z.core.$strip>>;
1872
2315
  }, z.core.$strip>>, OperationSpec<"unpublish", z.ZodObject<{
1873
2316
  id: z.ZodString;
1874
2317
  }, z.core.$strict>, z.ZodObject<{
@@ -2026,7 +2469,34 @@ export declare function isPersonalToken(value: string): boolean;
2026
2469
  /** The SHA-256 of a token, in lowercase hex: what the `tokens` table stores and looks up. */
2027
2470
  export declare function hashPersonalToken(token: string): Promise<string>;
2028
2471
  //#endregion
2472
+ //#region src/webhooks.d.ts
2473
+ /**
2474
+ * The signature of Hollware's webhooks (ADR-025), for the Worker that sends them and for whoever
2475
+ * receives them: HMAC-SHA256 of `<timestamp>.<body>` with the webhook's secret, sent as
2476
+ * `hollware-signature: v1=<hex>` with `hollware-timestamp` (seconds since the epoch) and
2477
+ * `hollware-event-id`. Web Crypto only, so it runs in any runtime.
2478
+ */
2479
+ export declare const SIGNATURE_HEADER = "hollware-signature";
2480
+ export declare const TIMESTAMP_HEADER = "hollware-timestamp";
2481
+ export declare const EVENT_ID_HEADER = "hollware-event-id";
2482
+ /** The value of `hollware-signature` for a body sent at `timestamp` (seconds). */
2483
+ export declare function signWebhook(secret: string, timestamp: number, body: string): Promise<string>;
2484
+ export interface WebhookCheck {
2485
+ readonly secret: string;
2486
+ /** The raw body, as received: a parsed and re-serialized body no longer matches. */
2487
+ readonly body: string;
2488
+ readonly headers: Headers;
2489
+ /** Now, in seconds since the epoch; the system clock when absent. */
2490
+ readonly now?: number;
2491
+ readonly toleranceSeconds?: number;
2492
+ }
2493
+ /**
2494
+ * Whether a request is a webhook of Hollware: its signature matches the body and the secret, and
2495
+ * its timestamp is within the tolerance, so a captured request cannot be replayed later.
2496
+ */
2497
+ export declare function verifyWebhook(check: WebhookCheck): Promise<boolean>;
2498
+ //#endregion
2029
2499
  //#region src/index.d.ts
2030
2500
  export declare const name = "@hollware/core";
2031
2501
  //#endregion
2032
- export { type AcknowledgedWarning, type BuiltInContractId, type BuiltInOptions, type CollectionDefinition, type ComponentDefinition, type ComponentUse, type ConfigProblem, type Contract, type ContractContext, type ContractDescription, type ContractFinding, type ContractText, type CustomContractOptions, type DescribedSchema, type DisallowedMarker, type Effect, type EntryDocument, type EntryFacts, type EntryLookup, type EntryRef, type ErrorCode, type ExtractedContent, type Heading, type HollwareConfig, type IdPrefix, type ImageRef, type LinkRef, type LinkTarget, type Operation, type OperationByName, type OperationName, type OperationSpec, type OutcomeInfo, type OutcomeOf, type Phase, type ProcessMarkdown, type ProcessMarkdownOptions, type ProcessResult, type ProcessedMarkdown, type RenderHtml, type ResolveConfig, type ResolvedCollection, type ResolvedConfig, type ResolvedSite, type RunContractsInput, type SchemaOutput, type Scope, type Severity, type SiteDefinition, type SiteDescription, type Verdict, type Violation, schemas_d_exports as schemas };
2502
+ export { type AcknowledgedWarning, type AuthDefinition, type BuiltInContractId, type BuiltInOptions, type CollectionDefinition, type ComponentDefinition, type ComponentUse, type ConfigProblem, type Contract, type ContractContext, type ContractDescription, type ContractFinding, type ContractText, type CustomContractOptions, type DescribedSchema, type DisallowedMarker, type Effect, type EntryDocument, type EntryFacts, type EntryLookup, type EntryRef, type ErrorCode, type ExtractedContent, type Heading, type HollwareConfig, type IdPrefix, type ImageRef, type LinkRef, type LinkTarget, type Operation, type OperationByName, type OperationName, type OperationSpec, type OutcomeInfo, type OutcomeOf, type Phase, type ProcessMarkdown, type ProcessMarkdownOptions, type ProcessResult, type ProcessedMarkdown, type RenderHtml, type ResolveConfig, type ResolvedAuth, type ResolvedCollection, type ResolvedConfig, type ResolvedSite, type RunContractsInput, type SchemaOutput, type Scope, type Severity, type SiteDefinition, type SiteDescription, type Verdict, type Violation, schemas_d_exports as schemas };
package/dist/index.mjs CHANGED
@@ -69,7 +69,7 @@ const MAX_MARKDOWN_BYTES_CEILING = 786432;
69
69
  * letter, at most 63 characters. A component name is what Markdown writes after the colons
70
70
  * (`::callout`). A site key has its own rule (`site-key.ts`).
71
71
  */
72
- const NAME = /^[a-z][a-z0-9-]{0,62}$/;
72
+ const NAME$1 = /^[a-z][a-z0-9-]{0,62}$/;
73
73
  /** A language tag: a language, then an optional script and region: `es`, `pt-BR`, `zh-Hant-TW`. */
74
74
  const LOCALE_TAG = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?$/;
75
75
  /** The id of a contract: lowercase words joined by single hyphens. */
@@ -1224,7 +1224,7 @@ function readTranslations(path, raw, schema, problems) {
1224
1224
  }
1225
1225
  const COMPONENT_FIX = "Declare it with defineComponent({ props: z.strictObject({ … }) }).";
1226
1226
  function readComponent(path, name, raw, problems) {
1227
- if (!NAME.test(name)) problems.add(path, `${quote(name)} is not a valid component name`, nameFix("callout"));
1227
+ if (!NAME$1.test(name)) problems.add(path, `${quote(name)} is not a valid component name`, nameFix("callout"));
1228
1228
  if (!isRecord$1(raw)) {
1229
1229
  problems.add(path, wrongType(raw, "a component"), COMPONENT_FIX);
1230
1230
  return null;
@@ -1347,7 +1347,7 @@ function readMaxMarkdownBytes(path, raw, problems) {
1347
1347
  * problem already: `resolveConfig` throws when there is any, so nothing else keeps track of them.
1348
1348
  */
1349
1349
  function readCollection(path, name, raw, site, problems) {
1350
- if (!NAME.test(name)) problems.add(path, `${quote(name)} is not a valid collection name`, nameFix("blog"));
1350
+ if (!NAME$1.test(name)) problems.add(path, `${quote(name)} is not a valid collection name`, nameFix("blog"));
1351
1351
  if (!isRecord$1(raw)) {
1352
1352
  problems.add(path, wrongType(raw, "a collection"), "Declare it with defineCollection({ schema, routes, … }).");
1353
1353
  return null;
@@ -1376,6 +1376,90 @@ function readCollection(path, name, raw, site, problems) {
1376
1376
  };
1377
1377
  }
1378
1378
  //#endregion
1379
+ //#region src/config/events.ts
1380
+ /** The name of a Worker secret or variable: what `wrangler secret put` takes. */
1381
+ const BINDING_NAME = /^[A-Z][A-Z0-9_]*$/;
1382
+ /** A subscriber's name: lowercase words joined by hyphens. */
1383
+ const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1384
+ /** The public URL of a site: an absolute http or https URL with no path, query or fragment. */
1385
+ function readSiteUrl(path, raw, problems) {
1386
+ if (raw === void 0) return null;
1387
+ const fix = "Write the public origin of the site, such as \"https://example.com\".";
1388
+ if (typeof raw !== "string" || !URL.canParse(raw)) {
1389
+ problems.add(path, wrongType(raw, "an absolute URL"), fix);
1390
+ return null;
1391
+ }
1392
+ const url = new URL(raw);
1393
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
1394
+ problems.add(path, `${quote(raw)} is not an origin`, fix);
1395
+ return null;
1396
+ }
1397
+ return url.origin;
1398
+ }
1399
+ function readBindingName(path, raw, problems) {
1400
+ if (typeof raw === "string" && BINDING_NAME.test(raw)) return raw;
1401
+ 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.");
1402
+ return null;
1403
+ }
1404
+ function readWebhook(path, raw, problems) {
1405
+ if (!isRecord$1(raw)) {
1406
+ problems.add(path, wrongType(raw, "a webhook"), "Declare it as { name: \"newsletter\", url: \"https://…\", secret: \"WEBHOOK_SECRET\" }.");
1407
+ return null;
1408
+ }
1409
+ const name = raw["name"];
1410
+ 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\".");
1411
+ const url = raw["url"];
1412
+ const urlOk = typeof url === "string" && URL.canParse(url) && new URL(url).protocol === "https:";
1413
+ if (!urlOk) problems.add(keyPath(path, "url"), wrongType(url, "an https URL"), "Give the https URL that receives the events.");
1414
+ const secret = readBindingName(keyPath(path, "secret"), raw["secret"], problems);
1415
+ if (typeof name !== "string" || !NAME.test(name) || !urlOk || secret === null) return null;
1416
+ return {
1417
+ name,
1418
+ url: String(url),
1419
+ secret
1420
+ };
1421
+ }
1422
+ /** Who hears about publications (ADR-025): webhooks, and IndexNow when the site has a URL. */
1423
+ function readEvents(path, raw, siteUrl, problems) {
1424
+ const events = {
1425
+ webhooks: [],
1426
+ indexNow: null
1427
+ };
1428
+ if (raw === void 0) return events;
1429
+ if (!isRecord$1(raw)) {
1430
+ problems.add(path, wrongType(raw, "an object"), "Declare { webhooks: [ … ], indexNow: { … } }.");
1431
+ return events;
1432
+ }
1433
+ const webhooks = raw["webhooks"];
1434
+ if (webhooks !== void 0) {
1435
+ if (!Array.isArray(webhooks)) problems.add(keyPath(path, "webhooks"), wrongType(webhooks, "a list"), "List the webhooks.");
1436
+ else {
1437
+ const seen = /* @__PURE__ */ new Set();
1438
+ webhooks.forEach((item, index) => {
1439
+ const webhook = readWebhook(itemPath(keyPath(path, "webhooks"), index), item, problems);
1440
+ if (webhook === null) return;
1441
+ if (seen.has(webhook.name)) {
1442
+ problems.add(itemPath(keyPath(path, "webhooks"), index), `repeats the name ${quote(webhook.name)}`, "Give each webhook a name of its own.");
1443
+ return;
1444
+ }
1445
+ seen.add(webhook.name);
1446
+ events.webhooks.push(webhook);
1447
+ });
1448
+ }
1449
+ }
1450
+ const indexNow = raw["indexNow"];
1451
+ if (indexNow !== void 0) {
1452
+ const indexPath = keyPath(path, "indexNow");
1453
+ if (!isRecord$1(indexNow)) problems.add(indexPath, wrongType(indexNow, "an object"), "Declare { key: \"INDEXNOW_KEY\" }.");
1454
+ else {
1455
+ const key = readBindingName(keyPath(indexPath, "key"), indexNow["key"], problems);
1456
+ 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.");
1457
+ else if (key !== null) events.indexNow = { key };
1458
+ }
1459
+ }
1460
+ return events;
1461
+ }
1462
+ //#endregion
1379
1463
  //#region src/config/site-key.ts
1380
1464
  /**
1381
1465
  * The key of a site, which is the id of its row in the database: lowercase letters and digits
@@ -1480,12 +1564,16 @@ function readSite(key, raw, problems) {
1480
1564
  localesOk: ok,
1481
1565
  routes: /* @__PURE__ */ new Map()
1482
1566
  }, problems);
1567
+ const styleGuide = readOptionalString(keyPath(path, "styleGuide"), raw["styleGuide"], "Write the guidance as a Markdown string, or remove it.", problems);
1568
+ const url = readSiteUrl(keyPath(path, "url"), raw["url"], problems);
1483
1569
  return {
1484
1570
  key,
1485
1571
  locales,
1486
1572
  defaultLocale,
1487
- styleGuide: readOptionalString(keyPath(path, "styleGuide"), raw["styleGuide"], "Write the guidance as a Markdown string, or remove it.", problems),
1488
- collections
1573
+ styleGuide,
1574
+ collections,
1575
+ url,
1576
+ events: readEvents(keyPath(path, "events"), raw["events"], url, problems)
1489
1577
  };
1490
1578
  }
1491
1579
  function readSites(raw, problems) {
@@ -1503,6 +1591,21 @@ function readSites(raw, problems) {
1503
1591
  }
1504
1592
  return sites;
1505
1593
  }
1594
+ /** `auth` of the config: absent, or an object whose `dynamicClientRegistration` is a boolean. */
1595
+ function readAuth(raw, problems) {
1596
+ if (raw === void 0) return { dynamicClientRegistration: false };
1597
+ if (!isRecord$1(raw)) {
1598
+ problems.add("auth", wrongType(raw, "an object"), "Write auth: { dynamicClientRegistration: true }, or leave auth out.");
1599
+ return { dynamicClientRegistration: false };
1600
+ }
1601
+ const value = raw["dynamicClientRegistration"];
1602
+ if (value === void 0) return { dynamicClientRegistration: false };
1603
+ if (typeof value !== "boolean") {
1604
+ problems.add("auth.dynamicClientRegistration", wrongType(value, "true or false"), "Write true to let MCP clients register themselves, or leave it out.");
1605
+ return { dynamicClientRegistration: false };
1606
+ }
1607
+ return { dynamicClientRegistration: value };
1608
+ }
1506
1609
  /**
1507
1610
  * Checks the config of the deployment and fills in its defaults, or throws a `ConfigError` with
1508
1611
  * every problem it found, so that a broken `hollware.config.ts` stops the Worker at startup with
@@ -1532,10 +1635,16 @@ function readSites(raw, problems) {
1532
1635
  function resolveConfig(config) {
1533
1636
  const problems = new Problems();
1534
1637
  let sites = /* @__PURE__ */ new Map();
1535
- if (isRecord$1(config)) sites = readSites(config["sites"], problems);
1536
- else problems.add("config", wrongType(config, "an object"), "Export the result of defineConfig({ sites: { … } }) from hollware.config.ts.");
1638
+ let auth = { dynamicClientRegistration: false };
1639
+ if (isRecord$1(config)) {
1640
+ sites = readSites(config["sites"], problems);
1641
+ auth = readAuth(config["auth"], problems);
1642
+ } else problems.add("config", wrongType(config, "an object"), "Export the result of defineConfig({ sites: { … } }) from hollware.config.ts.");
1537
1643
  if (problems.list.length > 0) throw new ConfigError(problems.list);
1538
- return { sites };
1644
+ return {
1645
+ sites,
1646
+ auth
1647
+ };
1539
1648
  }
1540
1649
  //#endregion
1541
1650
  //#region src/errors.ts
@@ -1577,10 +1686,19 @@ var HollwareError = class extends Error {
1577
1686
  const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1578
1687
  /** The kinds of ids and their prefixes. A new kind adds its prefix here. */
1579
1688
  const ID_PREFIXES = {
1689
+ challenge: "chl",
1690
+ delivery: "dlv",
1580
1691
  entry: "ent",
1692
+ event: "evt",
1693
+ invitation: "inv",
1694
+ member: "mbr",
1695
+ passkey: "pky",
1581
1696
  publication: "pub",
1582
1697
  revision: "rev",
1583
1698
  revisionTag: "tag",
1699
+ review: "rvw",
1700
+ schedule: "sch",
1701
+ session: "ses",
1584
1702
  token: "tok",
1585
1703
  user: "usr"
1586
1704
  };
@@ -2916,6 +3034,9 @@ var schemas_exports = /* @__PURE__ */ __exportAll({
2916
3034
  markdown: () => markdown,
2917
3035
  phase: () => phase,
2918
3036
  publication: () => publication,
3037
+ review: () => review,
3038
+ reviewId: () => reviewId,
3039
+ reviewStatus: () => reviewStatus,
2919
3040
  revisionId: () => revisionId,
2920
3041
  severity: () => severity,
2921
3042
  siteDescription: () => siteDescription,
@@ -2995,6 +3116,29 @@ const entry = entrySummary.extend({
2995
3116
  document: document.optional(),
2996
3117
  html: z.string().optional()
2997
3118
  });
3119
+ const reviewId = z.string().regex(/^rvw_[0-9A-HJKMNP-TV-Z]{26}$/).describe("The id of a review (rvw_…), as publish and list_reviews return it.");
3120
+ const reviewStatus = z.enum([
3121
+ "pending",
3122
+ "approved",
3123
+ "changes_requested",
3124
+ "cancelled"
3125
+ ]);
3126
+ /**
3127
+ * A request to publish a revision, made by someone who may write but not publish, and what a
3128
+ * person who may publish decided about it.
3129
+ */
3130
+ const review = z.object({
3131
+ id: reviewId,
3132
+ entryId,
3133
+ revision: revisionId.describe("The revision asked for."),
3134
+ status: reviewStatus.describe("pending until someone decides; cancelled when a new revision of the entry is saved."),
3135
+ requestedBy: z.string().describe("The user who asked."),
3136
+ requestedByName: z.string().nullable().describe("The name of the user who asked, if they gave one."),
3137
+ requestedAt: z.string(),
3138
+ decidedBy: z.string().nullable(),
3139
+ decidedAt: z.string().nullable(),
3140
+ note: z.string().nullable().describe("What the reviewer asked to change, or null.")
3141
+ });
2998
3142
  const conflict = z.object({
2999
3143
  currentVersion: z.number().int().min(1),
3000
3144
  headRevision: revisionId
@@ -3090,7 +3234,8 @@ const getEntry = defineOperation({
3090
3234
  collection: collectionName,
3091
3235
  locale,
3092
3236
  slug,
3093
- formats: z.array(format).min(1).default(["markdown"]).describe("What to return of the revision: markdown, the Comark document, or html.")
3237
+ formats: z.array(format).min(1).default(["markdown"]).describe("What to return of the revision: markdown, the Comark document, or html."),
3238
+ published: z.boolean().default(false).describe("Read the revision that is live instead of the latest one: not_found when the entry is not published.")
3094
3239
  }),
3095
3240
  output: z.object({
3096
3241
  outcome: z.enum(["ok", "not_found"]),
@@ -3196,30 +3341,41 @@ const validate = defineOperation({
3196
3341
  const publish = defineOperation({
3197
3342
  name: "publish",
3198
3343
  title: "Publish an entry",
3199
- description: "Publishes the latest revision of an entry, if the entry is still at the version you read. It is blocked while the contracts report errors. Warnings have to be acknowledged: an answer of needs_acknowledgement lists them, and publish again with them in acknowledgeWarnings.",
3344
+ description: "Publishes the latest revision of an entry, if the entry is still at the version you read, and with group, other entries in the same step. It is blocked while the contracts report errors. Warnings have to be acknowledged: an answer of needs_acknowledgement lists them, and publish again with them in acknowledgeWarnings. Publish entries that link to each other as one group. Without content:publish, the same checks run and the answer is pending_review: a person who may publish approves the revision or asks for changes, and a new revision of the entry cancels the request. A review is for one entry, now: never with group or publishAt.",
3200
3345
  input: z.strictObject({
3201
3346
  id: entryId,
3202
3347
  version,
3203
- acknowledgeWarnings: z.array(acknowledgedWarning).default([]).describe("The warnings you accept, as needs_acknowledgement listed them.")
3348
+ acknowledgeWarnings: z.array(acknowledgedWarning).default([]).describe("The warnings you accept, as needs_acknowledgement listed them."),
3349
+ group: z.array(z.strictObject({
3350
+ id: entryId,
3351
+ version
3352
+ })).max(99).default([]).describe("Other entries to publish in the same step, each with the version you read: all of them are published, or none. A link between two of them counts as published, and an acknowledged warning holds for every entry that reports it."),
3353
+ publishAt: z.iso.datetime({ offset: true }).optional().describe("Publish at this time instead of now, as an ISO 8601 date and time: the entry is checked now and waits as scheduled, and it is published within a minute of the time, if nobody changes it before. One entry at a time, never with group.")
3204
3354
  }),
3205
3355
  output: z.object({
3206
3356
  outcome: z.enum([
3207
3357
  "published",
3358
+ "scheduled",
3208
3359
  "blocked",
3209
3360
  "needs_acknowledgement",
3361
+ "pending_review",
3210
3362
  "conflict",
3211
3363
  "not_found"
3212
3364
  ]),
3213
3365
  entry: entrySummary.optional(),
3214
3366
  verdict: verdict.optional(),
3215
- conflict: conflict.optional()
3367
+ conflict: conflict.optional(),
3368
+ review: review.optional().describe("With pending_review: the request, as list_reviews shows it."),
3369
+ failedEntry: entryId.optional().describe("In a group, the entry that kept the group from being published: blocked, stale, missing, or with a warning to acknowledge.")
3216
3370
  }),
3217
- scopes: ["content:publish"],
3371
+ scopes: ["content:write"],
3218
3372
  effect: "write",
3219
3373
  outcomes: {
3220
3374
  published: { isError: false },
3375
+ scheduled: { isError: false },
3221
3376
  blocked: { isError: true },
3222
3377
  needs_acknowledgement: { isError: false },
3378
+ pending_review: { isError: false },
3223
3379
  conflict: { isError: true },
3224
3380
  not_found: { isError: true }
3225
3381
  }
@@ -3240,14 +3396,87 @@ const unpublish = defineOperation({
3240
3396
  not_found: { isError: true }
3241
3397
  }
3242
3398
  });
3243
- /** Every operation of v0.1, sorted by name: the order of `tools/list`. */
3399
+ const listReviews = defineOperation({
3400
+ name: "list_reviews",
3401
+ title: "List the reviews",
3402
+ description: "Lists the requests to publish, newest first, with the entry each one is for: the pending ones unless you name another status.",
3403
+ input: z.strictObject({
3404
+ status: reviewStatus.default("pending"),
3405
+ limit: z.number().int().min(1).max(100).default(50)
3406
+ }),
3407
+ output: z.object({
3408
+ outcome: z.enum(["ok"]),
3409
+ reviews: z.array(review.extend({ entry: entrySummary }))
3410
+ }),
3411
+ scopes: ["content:read"],
3412
+ effect: "read",
3413
+ outcomes: { ok: { isError: false } }
3414
+ });
3415
+ const approveReview = defineOperation({
3416
+ name: "approve_review",
3417
+ title: "Approve a review",
3418
+ description: "Publishes the revision a review asked for, in your name, after the contracts run again as publish runs them. The warnings the request acknowledged count as acknowledged; a new one answers needs_acknowledgement, and you approve again with it in acknowledgeWarnings.",
3419
+ input: z.strictObject({
3420
+ id: reviewId,
3421
+ acknowledgeWarnings: z.array(acknowledgedWarning).default([]).describe("Warnings you accept besides those the request acknowledged.")
3422
+ }),
3423
+ output: z.object({
3424
+ outcome: z.enum([
3425
+ "published",
3426
+ "blocked",
3427
+ "needs_acknowledgement",
3428
+ "not_pending",
3429
+ "not_found"
3430
+ ]),
3431
+ review: review.optional(),
3432
+ entry: entrySummary.optional(),
3433
+ verdict: verdict.optional()
3434
+ }),
3435
+ scopes: ["content:publish"],
3436
+ effect: "write",
3437
+ outcomes: {
3438
+ published: { isError: false },
3439
+ blocked: { isError: true },
3440
+ needs_acknowledgement: { isError: false },
3441
+ not_pending: { isError: true },
3442
+ not_found: { isError: true }
3443
+ }
3444
+ });
3445
+ const requestChanges = defineOperation({
3446
+ name: "request_changes",
3447
+ title: "Ask for changes",
3448
+ description: "Closes a pending review without publishing, with a note that says what to change. The author saves a new revision and asks again.",
3449
+ input: z.strictObject({
3450
+ id: reviewId,
3451
+ note: z.string().trim().min(1).max(4e3).describe("What to change, for the author.")
3452
+ }),
3453
+ output: z.object({
3454
+ outcome: z.enum([
3455
+ "changes_requested",
3456
+ "not_pending",
3457
+ "not_found"
3458
+ ]),
3459
+ review: review.optional()
3460
+ }),
3461
+ scopes: ["content:publish"],
3462
+ effect: "edit",
3463
+ outcomes: {
3464
+ changes_requested: { isError: false },
3465
+ not_pending: { isError: true },
3466
+ not_found: { isError: true }
3467
+ }
3468
+ });
3469
+ /** Every operation, sorted by name: the order of `tools/list`. */
3244
3470
  const OPERATIONS = [
3471
+ approveReview,
3245
3472
  createDraft,
3246
3473
  describeSite,
3247
3474
  getEntry,
3248
3475
  getTranslations,
3249
3476
  listEntries,
3477
+ listReviews,
3250
3478
  publish,
3479
+ requestChanges,
3251
3480
  unpublish,
3252
3481
  updateDraft,
3253
3482
  validate
@@ -3303,7 +3532,51 @@ async function hashPersonalToken(token) {
3303
3532
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
3304
3533
  }
3305
3534
  //#endregion
3535
+ //#region src/webhooks.ts
3536
+ /**
3537
+ * The signature of Hollware's webhooks (ADR-025), for the Worker that sends them and for whoever
3538
+ * receives them: HMAC-SHA256 of `<timestamp>.<body>` with the webhook's secret, sent as
3539
+ * `hollware-signature: v1=<hex>` with `hollware-timestamp` (seconds since the epoch) and
3540
+ * `hollware-event-id`. Web Crypto only, so it runs in any runtime.
3541
+ */
3542
+ const SIGNATURE_HEADER = "hollware-signature";
3543
+ const TIMESTAMP_HEADER = "hollware-timestamp";
3544
+ const EVENT_ID_HEADER = "hollware-event-id";
3545
+ /** How far a timestamp may be from the receiver's clock, by default: five minutes. */
3546
+ const TOLERANCE_SECONDS = 300;
3547
+ const encoder = new TextEncoder();
3548
+ async function hmac(secret, text) {
3549
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
3550
+ name: "HMAC",
3551
+ hash: "SHA-256"
3552
+ }, false, ["sign"]);
3553
+ return [...new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(text)))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
3554
+ }
3555
+ /** The value of `hollware-signature` for a body sent at `timestamp` (seconds). */
3556
+ async function signWebhook(secret, timestamp, body) {
3557
+ return `v1=${await hmac(secret, `${timestamp}.${body}`)}`;
3558
+ }
3559
+ /** Compares two strings in time that does not depend on where they differ. */
3560
+ function sameText(a, b) {
3561
+ if (a.length !== b.length) return false;
3562
+ let difference = 0;
3563
+ for (let index = 0; index < a.length; index++) difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
3564
+ return difference === 0;
3565
+ }
3566
+ /**
3567
+ * Whether a request is a webhook of Hollware: its signature matches the body and the secret, and
3568
+ * its timestamp is within the tolerance, so a captured request cannot be replayed later.
3569
+ */
3570
+ async function verifyWebhook(check) {
3571
+ const signature = check.headers.get(SIGNATURE_HEADER);
3572
+ const timestamp = Number(check.headers.get(TIMESTAMP_HEADER));
3573
+ if (signature === null || !Number.isInteger(timestamp)) return false;
3574
+ const now = check.now ?? Math.floor(Date.now() / 1e3);
3575
+ if (Math.abs(now - timestamp) > (check.toleranceSeconds ?? TOLERANCE_SECONDS)) return false;
3576
+ return sameText(signature, await signWebhook(check.secret, timestamp, check.body));
3577
+ }
3578
+ //#endregion
3306
3579
  //#region src/index.ts
3307
3580
  const name = "@hollware/core";
3308
3581
  //#endregion
3309
- export { BUILT_IN_CONTRACT_IDS, COMARK_VERSION, ConfigError, DEFAULT_MAX_MARKDOWN_BYTES, ERROR_CODES, HollwareError, ID_PREFIXES, MAX_MARKDOWN_BYTES_CEILING, OPERATIONS, PERSONAL_TOKEN_PREFIX, PERSONAL_TOKEN_TTL_DAYS, PIPELINE_VERSION, REFUSAL_CONTRACT_IDS, SCOPES, SITE_KEY_MAX_LENGTH, SITE_KEY_PATTERN, contracts, createDraft, defineCollection, defineComponent, defineConfig, defineContract, defineOperation, describeBuiltInContracts, describeContract, describeResolvedSite, describeSite, generatePersonalToken, getEntry, getTranslations, hashPersonalToken, isBuiltInContract, isId, isPersonalToken, isScope, isSiteKey, listEntries, name, newId, processMarkdown, publish, renderHtml, resolveConfig, runContracts, schemas_exports as schemas, sizeLimitViolation, ulid, uniqueSlugViolation, uniqueTranslationViolation, unpublish, updateDraft, validate };
3582
+ export { BUILT_IN_CONTRACT_IDS, COMARK_VERSION, ConfigError, DEFAULT_MAX_MARKDOWN_BYTES, ERROR_CODES, EVENT_ID_HEADER, HollwareError, ID_PREFIXES, MAX_MARKDOWN_BYTES_CEILING, OPERATIONS, PERSONAL_TOKEN_PREFIX, PERSONAL_TOKEN_TTL_DAYS, PIPELINE_VERSION, REFUSAL_CONTRACT_IDS, SCOPES, SIGNATURE_HEADER, SITE_KEY_MAX_LENGTH, SITE_KEY_PATTERN, TIMESTAMP_HEADER, approveReview, contracts, createDraft, defineCollection, defineComponent, defineConfig, defineContract, defineOperation, describeBuiltInContracts, describeContract, describeResolvedSite, describeSite, generatePersonalToken, getEntry, getTranslations, hashPersonalToken, isBuiltInContract, isId, isPersonalToken, isScope, isSiteKey, listEntries, listReviews, name, newId, processMarkdown, publish, renderHtml, requestChanges, resolveConfig, runContracts, schemas_exports as schemas, signWebhook, sizeLimitViolation, ulid, uniqueSlugViolation, uniqueTranslationViolation, unpublish, updateDraft, validate, verifyWebhook };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hollware/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Contracts, operation specs, schemas and the Markdown pipeline of Hollware. No Cloudflare dependencies.",
5
5
  "homepage": "https://hollware.com",
6
6
  "bugs": {
@@ -33,7 +33,7 @@
33
33
  "comark": "0.7.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@cloudflare/vitest-plugin": "1.4.0",
36
+ "@cloudflare/vitest-plugin": "1.4.1",
37
37
  "@types/node": "24.19.2",
38
38
  "publint": "0.3.25",
39
39
  "typescript": "7.0.2",