@glw907/cairn-cms 0.14.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/auth/crypto.d.ts +8 -2
  2. package/dist/auth/crypto.d.ts.map +1 -1
  3. package/dist/auth/crypto.js +12 -2
  4. package/dist/auth/store.d.ts +2 -0
  5. package/dist/auth/store.d.ts.map +1 -1
  6. package/dist/auth/store.js +17 -5
  7. package/dist/components/EditPage.svelte +13 -8
  8. package/dist/components/EditPage.svelte.d.ts +3 -1
  9. package/dist/components/EditPage.svelte.d.ts.map +1 -1
  10. package/dist/content/compose.d.ts.map +1 -1
  11. package/dist/content/compose.js +1 -0
  12. package/dist/content/links.d.ts +14 -0
  13. package/dist/content/links.d.ts.map +1 -0
  14. package/dist/content/links.js +41 -0
  15. package/dist/content/manifest.d.ts +55 -0
  16. package/dist/content/manifest.d.ts.map +1 -0
  17. package/dist/content/manifest.js +98 -0
  18. package/dist/content/types.d.ts +10 -1
  19. package/dist/content/types.d.ts.map +1 -1
  20. package/dist/delivery/content-index.d.ts.map +1 -1
  21. package/dist/delivery/content-index.js +11 -9
  22. package/dist/delivery/feeds.d.ts +1 -1
  23. package/dist/delivery/feeds.d.ts.map +1 -1
  24. package/dist/delivery/feeds.js +31 -16
  25. package/dist/delivery/index.d.ts +1 -0
  26. package/dist/delivery/index.d.ts.map +1 -1
  27. package/dist/delivery/index.js +1 -0
  28. package/dist/delivery/manifest.d.ts +13 -0
  29. package/dist/delivery/manifest.d.ts.map +1 -0
  30. package/dist/delivery/manifest.js +31 -0
  31. package/dist/delivery/site-indexes.d.ts.map +1 -1
  32. package/dist/delivery/site-indexes.js +9 -1
  33. package/dist/env.d.ts.map +1 -1
  34. package/dist/env.js +14 -0
  35. package/dist/github/repo.d.ts +21 -0
  36. package/dist/github/repo.d.ts.map +1 -1
  37. package/dist/github/repo.js +79 -0
  38. package/dist/github/signing.d.ts +12 -0
  39. package/dist/github/signing.d.ts.map +1 -1
  40. package/dist/github/signing.js +22 -0
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +5 -0
  44. package/dist/render/pipeline.d.ts +14 -1
  45. package/dist/render/pipeline.d.ts.map +1 -1
  46. package/dist/render/pipeline.js +22 -3
  47. package/dist/render/resolve-links.d.ts +8 -0
  48. package/dist/render/resolve-links.d.ts.map +1 -0
  49. package/dist/render/resolve-links.js +36 -0
  50. package/dist/render/sanitize-schema.d.ts +20 -0
  51. package/dist/render/sanitize-schema.d.ts.map +1 -0
  52. package/dist/render/sanitize-schema.js +57 -0
  53. package/dist/sveltekit/auth-routes.d.ts.map +1 -1
  54. package/dist/sveltekit/auth-routes.js +29 -11
  55. package/dist/sveltekit/content-routes.d.ts +3 -0
  56. package/dist/sveltekit/content-routes.d.ts.map +1 -1
  57. package/dist/sveltekit/content-routes.js +31 -4
  58. package/dist/sveltekit/guard.d.ts +1 -1
  59. package/dist/sveltekit/guard.d.ts.map +1 -1
  60. package/dist/sveltekit/guard.js +25 -10
  61. package/dist/sveltekit/nav-routes.js +2 -2
  62. package/dist/sveltekit/public-routes.d.ts +2 -0
  63. package/dist/sveltekit/public-routes.d.ts.map +1 -1
  64. package/dist/sveltekit/public-routes.js +3 -2
  65. package/dist/sveltekit/types.d.ts +6 -0
  66. package/dist/sveltekit/types.d.ts.map +1 -1
  67. package/package.json +3 -2
  68. package/src/lib/auth/crypto.ts +14 -2
  69. package/src/lib/auth/store.ts +18 -5
  70. package/src/lib/components/EditPage.svelte +13 -8
  71. package/src/lib/content/compose.ts +1 -0
  72. package/src/lib/content/links.ts +48 -0
  73. package/src/lib/content/manifest.ts +138 -0
  74. package/src/lib/content/types.ts +10 -3
  75. package/src/lib/delivery/content-index.ts +12 -9
  76. package/src/lib/delivery/feeds.ts +34 -19
  77. package/src/lib/delivery/index.ts +1 -0
  78. package/src/lib/delivery/manifest.ts +38 -0
  79. package/src/lib/delivery/site-indexes.ts +13 -1
  80. package/src/lib/env.ts +13 -0
  81. package/src/lib/github/repo.ts +103 -0
  82. package/src/lib/github/signing.ts +32 -0
  83. package/src/lib/index.ts +16 -0
  84. package/src/lib/render/pipeline.ts +33 -3
  85. package/src/lib/render/resolve-links.ts +42 -0
  86. package/src/lib/render/sanitize-schema.ts +66 -0
  87. package/src/lib/sveltekit/auth-routes.ts +30 -11
  88. package/src/lib/sveltekit/content-routes.ts +38 -6
  89. package/src/lib/sveltekit/guard.ts +25 -10
  90. package/src/lib/sveltekit/nav-routes.ts +2 -2
  91. package/src/lib/sveltekit/public-routes.ts +5 -3
  92. package/src/lib/sveltekit/types.ts +5 -1
  93. package/dist/render/sanitize.d.ts +0 -8
  94. package/dist/render/sanitize.d.ts.map +0 -1
  95. package/dist/render/sanitize.js +0 -26
  96. package/src/lib/render/sanitize.ts +0 -27
@@ -13,30 +13,41 @@ function escapeXml(value) {
13
13
  function cdataSafe(value) {
14
14
  return value.replace(/]]>/g, ']]]]><![CDATA[>');
15
15
  }
16
- /** Format a YYYY-MM-DD (or ISO) string as an RFC-822 date in UTC, as RSS wants. */
16
+ /** Parse a YYYY-MM-DD (or ISO) string as a UTC instant. Returns undefined for an absent or
17
+ * unparseable date, so a feed omits the date field rather than emit Invalid Date or throw. */
18
+ function parseFeedDate(date) {
19
+ if (!date)
20
+ return undefined;
21
+ const at = new Date(`${date.slice(0, 10)}T00:00:00.000Z`);
22
+ return Number.isNaN(at.getTime()) ? undefined : at;
23
+ }
24
+ /** Format a date as an RFC-822 string in UTC, as RSS wants, or undefined when it cannot parse. */
17
25
  function rfc822(date) {
18
- return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toUTCString();
26
+ return parseFeedDate(date)?.toUTCString();
19
27
  }
20
- /** Format a YYYY-MM-DD (or ISO) string as an ISO-8601 instant in UTC. */
28
+ /** Format a date as an ISO-8601 instant in UTC, or undefined when it cannot parse. */
21
29
  function iso(date) {
22
- return new Date(`${date.slice(0, 10)}T00:00:00.000Z`).toISOString();
30
+ return parseFeedDate(date)?.toISOString();
23
31
  }
24
32
  /** Build an RSS 2.0 document. */
25
33
  export function buildRssFeed(channel, items) {
26
34
  const entries = items
27
35
  .map((item) => {
28
36
  const content = item.contentHtml ?? item.summary;
37
+ const pubDate = rfc822(item.date);
29
38
  return [
30
39
  ' <item>',
31
40
  ` <title>${escapeXml(item.title)}</title>`,
32
41
  ` <link>${escapeXml(item.url)}</link>`,
33
42
  ` <guid isPermaLink="true">${escapeXml(item.url)}</guid>`,
34
- ` <pubDate>${rfc822(item.date)}</pubDate>`,
43
+ pubDate ? ` <pubDate>${pubDate}</pubDate>` : '',
35
44
  ` <description>${escapeXml(item.summary)}</description>`,
36
45
  // CDATA cannot contain `]]>`, so split that one sequence rather than escape the body.
37
46
  ` <content:encoded><![CDATA[${cdataSafe(content)}]]></content:encoded>`,
38
47
  ' </item>',
39
- ].join('\n');
48
+ ]
49
+ .filter((line) => line !== '')
50
+ .join('\n');
40
51
  })
41
52
  .join('\n');
42
53
  return [
@@ -66,15 +77,19 @@ export function buildJsonFeed(channel, items) {
66
77
  feed_url: channel.feedUrl,
67
78
  ...(channel.language ? { language: channel.language } : {}),
68
79
  ...(channel.author ? { authors: [channel.author] } : {}),
69
- items: items.map((item) => ({
70
- id: item.url,
71
- url: item.url,
72
- title: item.title,
73
- summary: item.summary,
74
- date_published: iso(item.date),
75
- ...(item.updated ? { date_modified: iso(item.updated) } : {}),
76
- ...(item.contentHtml ? { content_html: item.contentHtml } : { content_text: item.summary }),
77
- ...(item.tags && item.tags.length ? { tags: item.tags } : {}),
78
- })),
80
+ items: items.map((item) => {
81
+ const datePublished = iso(item.date);
82
+ const dateModified = iso(item.updated);
83
+ return {
84
+ id: item.url,
85
+ url: item.url,
86
+ title: item.title,
87
+ summary: item.summary,
88
+ ...(datePublished ? { date_published: datePublished } : {}),
89
+ ...(dateModified ? { date_modified: dateModified } : {}),
90
+ ...(item.contentHtml ? { content_html: item.contentHtml } : { content_text: item.summary }),
91
+ ...(item.tags && item.tags.length ? { tags: item.tags } : {}),
92
+ };
93
+ }),
79
94
  }, null, 2);
80
95
  }
@@ -20,6 +20,7 @@ export type { Page } from './paginate.js';
20
20
  export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
21
21
  export { jsonLdScript } from './json-ld.js';
22
22
  export { permalink } from '../content/permalink.js';
23
+ export { buildSiteManifest, buildLinkResolver } from './manifest.js';
23
24
  export { createPublicRoutes } from '../sveltekit/public-routes.js';
24
25
  export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData, } from '../sveltekit/public-routes.js';
25
26
  export { default as CairnHead } from './CairnHead.svelte';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC9G,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACjE,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,YAAY,EACV,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,SAAS,GACV,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC9G,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACjE,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,YAAY,EACV,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,SAAS,GACV,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC"}
@@ -17,5 +17,6 @@ export { paginate } from './paginate.js';
17
17
  export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
18
18
  export { jsonLdScript } from './json-ld.js';
19
19
  export { permalink } from '../content/permalink.js';
20
+ export { buildSiteManifest, buildLinkResolver } from './manifest.js';
20
21
  export { createPublicRoutes } from '../sveltekit/public-routes.js';
21
22
  export { default as CairnHead } from './CairnHead.svelte';
@@ -0,0 +1,13 @@
1
+ import type { Manifest } from '../content/manifest.js';
2
+ import type { LinkResolve } from '../content/links.js';
3
+ import type { SiteIndex } from './site-index.js';
4
+ import type { SiteConfig } from '../nav/site-config.js';
5
+ import type { CairnAdapter } from '../content/types.js';
6
+ import type { SiteGlobs } from './site-indexes.js';
7
+ /** Build the whole-corpus manifest from a site's adapter, config, and per-concept globs. Drafts are
8
+ * included and flagged, so the admin picker and the guards see the full graph. */
9
+ export declare function buildSiteManifest<A extends CairnAdapter>(adapter: A, config: SiteConfig, globs: SiteGlobs<A>): Manifest;
10
+ /** A resolver backed by the site index, for the build. A miss throws, so a dangling cairn: token
11
+ * fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
12
+ export declare function buildLinkResolver(site: SiteIndex): LinkResolve;
13
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/manifest.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD;mFACmF;AACnF,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,YAAY,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,QAAQ,CAUvH;AAED;oGACoG;AACpG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,WAAW,CAM9D"}
@@ -0,0 +1,31 @@
1
+ // cairn-cms: the build-side manifest builder and the build link resolver (content-graph design).
2
+ // buildSiteManifest mirrors createSiteIndexes: it maps the site descriptors over the per-concept
3
+ // globs and projects each file to a manifest row. buildLinkResolver reads the site index, which is
4
+ // fresh from the files at build, and throws on a missing target so a dangling cairn: token fails
5
+ // the build (the backstop). The admin preview uses manifestLinkResolver instead.
6
+ import { siteDescriptors } from './site-descriptors.js';
7
+ import { fromGlob } from './content-index.js';
8
+ import { emptyManifest, manifestEntryFromFile } from '../content/manifest.js';
9
+ /** Build the whole-corpus manifest from a site's adapter, config, and per-concept globs. Drafts are
10
+ * included and flagged, so the admin picker and the guards see the full graph. */
11
+ export function buildSiteManifest(adapter, config, globs) {
12
+ const globRecord = globs;
13
+ const manifest = emptyManifest();
14
+ for (const descriptor of siteDescriptors(adapter, config)) {
15
+ const record = globRecord[descriptor.id] ?? {};
16
+ for (const file of fromGlob(record)) {
17
+ manifest.entries.push(manifestEntryFromFile(descriptor, file));
18
+ }
19
+ }
20
+ return manifest;
21
+ }
22
+ /** A resolver backed by the site index, for the build. A miss throws, so a dangling cairn: token
23
+ * fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
24
+ export function buildLinkResolver(site) {
25
+ return (ref) => {
26
+ const url = site.concept(ref.concept)?.byId(ref.id)?.permalink;
27
+ if (!url)
28
+ throw new Error(`cairn link target not found: cairn:${ref.concept}/${ref.id}`);
29
+ return url;
30
+ };
31
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"site-indexes.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-indexes.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAgB,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE/D,oGAAoG;AACpG,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,YAAY,IAAI;KAC7C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CACnD,CAAC;AAEF;0EAC0E;AAC1E,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,YAAY,IAAI;KAC/C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,GAAG,YAAY,CACrC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACjG;CACF,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EAC5D,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EACnB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,WAAW,CAAC,CAAC,CAAC,CAYhB"}
1
+ {"version":3,"file":"site-indexes.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-indexes.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAgB,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE/D,oGAAoG;AACpG,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,YAAY,IAAI;KAC7C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CACnD,CAAC;AAEF;0EAC0E;AAC1E,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,YAAY,IAAI;KAC/C,CAAC,IAAI,MAAM,CAAC,CAAC,SAAS,CAAC,GAAG,YAAY,CACrC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACjG;CACF,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EAC5D,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EACnB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,WAAW,CAAC,CAAC,CAAC,CAwBhB"}
@@ -9,10 +9,18 @@ import { createSiteIndex } from './site-index.js';
9
9
  */
10
10
  export function createSiteIndexes(adapter, config, globs, opts = {}) {
11
11
  const descriptors = siteDescriptors(adapter, config);
12
+ const globRecord = globs;
12
13
  const byConcept = {};
13
14
  const conceptIndexes = [];
14
15
  for (const descriptor of descriptors) {
15
- const record = globs[descriptor.id] ?? {};
16
+ if (descriptor.id === 'site') {
17
+ throw new Error('createSiteIndexes: a concept cannot be named "site", which is the reserved cross-concept resolver key');
18
+ }
19
+ if (!Object.prototype.hasOwnProperty.call(globRecord, descriptor.id)) {
20
+ const passed = Object.keys(globRecord);
21
+ throw new Error(`createSiteIndexes: no glob passed for concept "${descriptor.id}"; pass its import.meta.glob (an empty {} for an intentionally empty concept). Globs passed: ${passed.length ? passed.join(', ') : '(none)'}`);
22
+ }
23
+ const record = globRecord[descriptor.id] ?? {};
16
24
  const index = createContentIndex(fromGlob(record), descriptor);
17
25
  byConcept[descriptor.id] = index;
18
26
  conceptIndexes.push({ descriptor, index });
package/dist/env.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/lib/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMrE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,UAAU,CAKnE"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/lib/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmBrE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,UAAU,CAKnE"}
package/dist/env.js CHANGED
@@ -11,6 +11,20 @@ export function requireOrigin(env) {
11
11
  if (!origin) {
12
12
  throw new Error('PUBLIC_ORIGIN is not configured');
13
13
  }
14
+ let hostname;
15
+ try {
16
+ hostname = new URL(origin).hostname;
17
+ }
18
+ catch {
19
+ throw new Error(`PUBLIC_ORIGIN is not a valid URL, got ${origin}`);
20
+ }
21
+ // The magic-link origin must be https in production so the link and the __Host- cookie are
22
+ // origin-bound. http is allowed only for local dev on localhost or 127.0.0.1, matched exactly so
23
+ // a lookalike host like localhost.example.com cannot skip the https requirement.
24
+ const isLocal = hostname === 'localhost' || hostname === '127.0.0.1';
25
+ if (!origin.startsWith('https://') && !isLocal) {
26
+ throw new Error(`PUBLIC_ORIGIN must be https in production, got ${origin}`);
27
+ }
14
28
  return origin;
15
29
  }
16
30
  /**
@@ -45,5 +45,26 @@ export declare function commitFile(repo: RepoRef, path: string, content: string,
45
45
  message: string;
46
46
  author: CommitAuthor;
47
47
  }, token: string): Promise<string>;
48
+ /** A path change for an atomic commit: write `content`, or delete the path when `content` is null. */
49
+ export interface FileChange {
50
+ path: string;
51
+ content: string | null;
52
+ }
53
+ /**
54
+ * Commit several path changes in one commit over the Git Data API. The author is the editor; the
55
+ * committer is omitted, so GitHub attributes the commit to the App. Returns the new commit sha.
56
+ * Builds the new tree on the current head's tree, so paths not named here are preserved.
57
+ *
58
+ * Caller preconditions this layer cannot enforce (the save and lifecycle paths must): every
59
+ * `path` is confined to the site's content directories (the App token can write anywhere in the
60
+ * repo), and `author` is derived from the verified server-side session, never request input.
61
+ *
62
+ * An empty change set is rejected, since it would otherwise push an empty commit that triggers a
63
+ * site redeploy for no content change.
64
+ */
65
+ export declare function commitFiles(repo: RepoRef, changes: FileChange[], opts: {
66
+ message: string;
67
+ author: CommitAuthor;
68
+ }, token: string): Promise<string>;
48
69
  export {};
49
70
  //# sourceMappingURL=repo.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/lib/github/repo.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAelE,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAE7C;AAED,qFAAqF;AACrF,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAOD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE,CAW1E;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAMlG;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKjG;AAOD,+EAA+E;AAC/E,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKhG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,EAC/C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAiBjB"}
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/lib/github/repo.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAelE,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAE7C;AAED,qFAAqF;AACrF,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAOD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE,CAW1E;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAMlG;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKjG;AAOD,+EAA+E;AAC/E,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKhG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,EAC/C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAED,sGAAsG;AACtG,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AA8CD;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,UAAU,EAAE,EACrB,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,EAC/C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAkCjB"}
@@ -121,3 +121,82 @@ export async function commitFile(repo, path, content, opts, token) {
121
121
  throw new Error(`GitHub commit ${path} failed: ${res.status} ${await res.text()}`);
122
122
  return (await res.json()).commit.sha;
123
123
  }
124
+ /** A Git Data API URL under the repo's `git/` namespace. */
125
+ function gitUrl(repo, suffix) {
126
+ return `${API}/repos/${repo.owner}/${repo.repo}/git/${suffix}`;
127
+ }
128
+ /** The branch head commit sha, through the Git Data API single-ref read. */
129
+ async function headCommitSha(repo, token) {
130
+ const res = await fetch(gitUrl(repo, `ref/heads/${encodeURIComponent(repo.branch)}`), {
131
+ headers: ghHeaders('application/vnd.github+json', token),
132
+ });
133
+ if (!res.ok)
134
+ throw new Error(`GitHub ref ${repo.branch} failed: ${res.status}`);
135
+ return (await res.json()).object.sha;
136
+ }
137
+ /** The base tree sha of a commit. */
138
+ async function commitTreeSha(repo, commitSha, token) {
139
+ const res = await fetch(gitUrl(repo, `commits/${commitSha}`), {
140
+ headers: ghHeaders('application/vnd.github+json', token),
141
+ });
142
+ if (!res.ok)
143
+ throw new Error(`GitHub commit ${commitSha} failed: ${res.status}`);
144
+ return (await res.json()).tree.sha;
145
+ }
146
+ /** Map file changes to Git Trees API entries, encoding a null content as a delete. */
147
+ function treeChanges(changes) {
148
+ return changes.map((c) => c.content === null
149
+ ? { path: c.path, mode: '100644', type: 'blob', sha: null }
150
+ : { path: c.path, mode: '100644', type: 'blob', content: c.content });
151
+ }
152
+ /** Retries after the initial attempt when the branch moves under an atomic commit. */
153
+ const COMMIT_RETRIES = 3;
154
+ /**
155
+ * Commit several path changes in one commit over the Git Data API. The author is the editor; the
156
+ * committer is omitted, so GitHub attributes the commit to the App. Returns the new commit sha.
157
+ * Builds the new tree on the current head's tree, so paths not named here are preserved.
158
+ *
159
+ * Caller preconditions this layer cannot enforce (the save and lifecycle paths must): every
160
+ * `path` is confined to the site's content directories (the App token can write anywhere in the
161
+ * repo), and `author` is derived from the verified server-side session, never request input.
162
+ *
163
+ * An empty change set is rejected, since it would otherwise push an empty commit that triggers a
164
+ * site redeploy for no content change.
165
+ */
166
+ export async function commitFiles(repo, changes, opts, token) {
167
+ if (changes.length === 0)
168
+ throw new Error('commitFiles: no changes to commit');
169
+ const tree = treeChanges(changes);
170
+ for (let attempt = 0; attempt <= COMMIT_RETRIES; attempt++) {
171
+ const parent = await headCommitSha(repo, token);
172
+ const baseTree = await commitTreeSha(repo, parent, token);
173
+ const treeRes = await fetch(gitUrl(repo, 'trees'), {
174
+ method: 'POST',
175
+ headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
176
+ body: JSON.stringify({ base_tree: baseTree, tree }),
177
+ });
178
+ if (!treeRes.ok)
179
+ throw new Error(`GitHub tree create failed: ${treeRes.status} ${await treeRes.text()}`);
180
+ const newTree = (await treeRes.json()).sha;
181
+ const commitRes = await fetch(gitUrl(repo, 'commits'), {
182
+ method: 'POST',
183
+ headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
184
+ body: JSON.stringify({ message: opts.message, tree: newTree, parents: [parent], author: opts.author }),
185
+ });
186
+ if (!commitRes.ok)
187
+ throw new Error(`GitHub commit create failed: ${commitRes.status} ${await commitRes.text()}`);
188
+ const newCommit = (await commitRes.json()).sha;
189
+ const refRes = await fetch(gitUrl(repo, `refs/heads/${encodeURIComponent(repo.branch)}`), {
190
+ method: 'PATCH',
191
+ headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
192
+ body: JSON.stringify({ sha: newCommit, force: false }),
193
+ });
194
+ if (refRes.ok)
195
+ return newCommit;
196
+ // A non-fast-forward means the branch moved; retry on the new head so a concurrent commit
197
+ // is preserved. Any other failure is not a race, so surface it.
198
+ if (refRes.status !== 422)
199
+ throw new Error(`GitHub ref update failed: ${refRes.status} ${await refRes.text()}`);
200
+ }
201
+ throw new CommitConflictError(`${repo.branch} (atomic commit)`);
202
+ }
@@ -3,6 +3,18 @@ import type { AppCredentials } from './types.js';
3
3
  export declare function appJwt(appId: string, privateKeyPem: string): Promise<string>;
4
4
  /** Exchange the App JWT for a short-lived installation access token. */
5
5
  export declare function installationToken(creds: AppCredentials): Promise<string>;
6
+ /**
7
+ * Build an installation-token cache. A module-global instance memoizes the minted token per
8
+ * installation for most of its one-hour life, so a warm Worker isolate reuses it across requests
9
+ * instead of re-signing and re-calling GitHub on every list and commit. A cold isolate re-mints,
10
+ * which is always safe. This mirrors the default of @octokit/auth-app, which caches installation
11
+ * tokens in memory and returns them until expiry. The TTL stays under GitHub's documented one-hour
12
+ * lifetime, so a fixed margin avoids parsing the API expiry. `mint` and `now` are injected so the
13
+ * cache is testable with no network call and no real clock.
14
+ */
15
+ export declare function createInstallationTokenCache(mint?: (creds: AppCredentials) => Promise<string>, now?: () => number, ttlMs?: number): (creds: AppCredentials) => Promise<string>;
16
+ /** The shared installation-token cache, one instance per Worker isolate. */
17
+ export declare const cachedInstallationToken: (creds: AppCredentials) => Promise<string>;
6
18
  /**
7
19
  * Deploy-time self-test for the App signer: sign a dummy JWT with the configured key. It
8
20
  * exercises the brittle PKCS#1-to-PKCS#8 conversion and the Web Crypto import and sign with
@@ -1 +1 @@
1
- {"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../src/lib/github/signing.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA0CjD,wFAAwF;AACxF,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclF;AAED,wEAAwE;AACxE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAa9E;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH"}
1
+ {"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../src/lib/github/signing.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA0CjD,wFAAwF;AACxF,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclF;AAED,wEAAwE;AACxE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAa9E;AAOD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,GAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAqB,EACpE,GAAG,GAAE,MAAM,MAAyB,EACpC,KAAK,SAAiB,GACrB,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAS5C;AAED,4EAA4E;AAC5E,eAAO,MAAM,uBAAuB,UAZzB,cAAc,KAAK,OAAO,CAAC,MAAM,CAYyB,CAAC;AAEtE;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH"}
@@ -59,6 +59,28 @@ export async function installationToken(creds) {
59
59
  throw new Error(`GitHub installation token failed: ${res.status}`);
60
60
  return (await res.json()).token;
61
61
  }
62
+ /**
63
+ * Build an installation-token cache. A module-global instance memoizes the minted token per
64
+ * installation for most of its one-hour life, so a warm Worker isolate reuses it across requests
65
+ * instead of re-signing and re-calling GitHub on every list and commit. A cold isolate re-mints,
66
+ * which is always safe. This mirrors the default of @octokit/auth-app, which caches installation
67
+ * tokens in memory and returns them until expiry. The TTL stays under GitHub's documented one-hour
68
+ * lifetime, so a fixed margin avoids parsing the API expiry. `mint` and `now` are injected so the
69
+ * cache is testable with no network call and no real clock.
70
+ */
71
+ export function createInstallationTokenCache(mint = installationToken, now = () => Date.now(), ttlMs = 55 * 60 * 1000) {
72
+ const cache = new Map();
73
+ return async function get(creds) {
74
+ const hit = cache.get(creds.installationId);
75
+ if (hit && hit.expiresAt > now())
76
+ return hit.token;
77
+ const token = await mint(creds);
78
+ cache.set(creds.installationId, { token, expiresAt: now() + ttlMs });
79
+ return token;
80
+ };
81
+ }
82
+ /** The shared installation-token cache, one instance per Worker isolate. */
83
+ export const cachedInstallationToken = createInstallationTokenCache();
62
84
  /**
63
85
  * Deploy-time self-test for the App signer: sign a dummy JWT with the configured key. It
64
86
  * exercises the brittle PKCS#1-to-PKCS#8 conversion and the Web Crypto import and sign with
package/dist/index.d.ts CHANGED
@@ -11,6 +11,10 @@ export { defineAdapter } from './content/adapter.js';
11
11
  export type { ConceptSchema, Infer, InferFields, DefineFieldsOptions, StandardInput, StandardSchemaV1 } from './content/schema.js';
12
12
  export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, composeDatedId, } from './content/ids.js';
13
13
  export type { DatePrefix } from './content/ids.js';
14
+ export { parseCairnToken, extractCairnLinks } from './content/links.js';
15
+ export type { CairnRef, LinkResolve } from './content/links.js';
16
+ export { serializeManifest, parseManifest, emptyManifest, verifyManifest, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, } from './content/manifest.js';
17
+ export type { Manifest, ManifestEntry, LinkTarget } from './content/manifest.js';
14
18
  export { defineRegistry, emptyValues } from './render/registry.js';
15
19
  export type { ComponentDef, ComponentRegistry, FieldType, AttributeField, SlotKind, SlotDef, ComponentValues, } from './render/registry.js';
16
20
  export { serializeComponent, parseComponent } from './render/component-grammar.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -8,6 +8,11 @@ export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown,
8
8
  export { defineFields } from './content/schema.js';
9
9
  export { defineAdapter } from './content/adapter.js';
10
10
  export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, composeDatedId, } from './content/ids.js';
11
+ // Internal-link token and the committed content manifest (content-graph design). The corpus
12
+ // builder and the request-time resolver ship from the delivery entry; this surface is the
13
+ // grammar, the manifest operations, and their types a migrating site adopts.
14
+ export { parseCairnToken, extractCairnLinks } from './content/links.js';
15
+ export { serializeManifest, parseManifest, emptyManifest, verifyManifest, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, } from './content/manifest.js';
11
16
  // Render engine (Plan 04): generic directive pipeline; sites own the component registry.
12
17
  export { defineRegistry, emptyValues } from './render/registry.js';
13
18
  export { serializeComponent, parseComponent } from './render/component-grammar.js';
@@ -1,10 +1,21 @@
1
1
  import { type PluggableList } from 'unified';
2
+ import type { Schema } from 'hast-util-sanitize';
2
3
  import type { ComponentRegistry } from './registry.js';
4
+ import type { LinkResolve } from '../content/links.js';
3
5
  export interface RendererOptions {
4
6
  /** Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
5
7
  * CSS can drive an entrance-cascade delay off it. Omit for no stagger. The ordinal
6
8
  * is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`. */
7
9
  stagger?: boolean;
10
+ /** Extend the sanitize allowlist. Receives cairn's default schema (defaultSchema plus the
11
+ * directive markers and the common benign tags) and returns the schema to use. Add to the
12
+ * allowlist for the benign HTML a site's content needs; start from the argument so the
13
+ * dangerous strip is preserved. */
14
+ sanitizeSchema?: (defaults: Schema) => Schema;
15
+ /** Developer-only escape hatch: disable the sanitize floor entirely. This reintroduces the XSS
16
+ * vector the floor closes, so it is only for a site whose content is fully developer-controlled.
17
+ * It is a code-level adapter decision, never an editor-facing setting. */
18
+ unsafeDisableSanitize?: boolean;
8
19
  }
9
20
  /** Compose a site's render pipeline from its component registry: directive syntax to
10
21
  * stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
@@ -12,6 +23,8 @@ export interface RendererOptions {
12
23
  export declare function createRenderer(registry: ComponentRegistry, options?: RendererOptions): {
13
24
  remarkPlugins: PluggableList;
14
25
  rehypePlugins: PluggableList;
15
- renderMarkdown: (content: string) => Promise<string>;
26
+ renderMarkdown: (content: string, opts?: {
27
+ resolve?: LinkResolve;
28
+ }) => Promise<string>;
16
29
  };
17
30
  //# sourceMappingURL=pipeline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAUtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;;0FAEsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;uFAEuF;AACvF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,GAAE,eAAoB;;;8BAarD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;EAE3D"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAStD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAMjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;;0FAEsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;wCAGoC;IACpC,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9C;;+EAE2E;IAC3E,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;uFAEuF;AACvF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,GAAE,eAAoB;;;8BAyBrD,MAAM,SAAQ;QAAE,OAAO,CAAC,EAAE,WAAW,CAAA;KAAE,KAAQ,OAAO,CAAC,MAAM,CAAC;EAKjG"}
@@ -6,14 +6,30 @@ import remarkRehype from 'remark-rehype';
6
6
  import rehypeRaw from 'rehype-raw';
7
7
  import rehypeSlug from 'rehype-slug';
8
8
  import rehypeStringify from 'rehype-stringify';
9
+ import rehypeSanitize from 'rehype-sanitize';
10
+ import { VFile } from 'vfile';
11
+ import { buildSanitizeSchema, rehypeAnchorRel } from './sanitize-schema.js';
9
12
  import { remarkDirectiveStamp } from './remark-directives.js';
13
+ import { remarkResolveCairnLinks, CAIRN_RESOLVE } from './resolve-links.js';
10
14
  import { rehypeDispatch } from './rehype-dispatch.js';
11
15
  /** Compose a site's render pipeline from its component registry: directive syntax to
12
16
  * stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
13
17
  * rehype plugin arrays (so the admin editor preview can reuse the exact same set). */
14
18
  export function createRenderer(registry, options = {}) {
15
- const remarkPlugins = [remarkDirective, [remarkDirectiveStamp, registry]];
16
- const rehypePlugins = [rehypeRaw, [rehypeDispatch, registry, options.stagger], rehypeSlug];
19
+ const remarkPlugins = [remarkDirective, [remarkDirectiveStamp, registry], remarkResolveCairnLinks];
20
+ // The sanitize floor runs after rehype-raw (so author raw HTML is parsed, then cleaned) and
21
+ // before the dispatch (so the site's trusted build() output and its inline SVG icons are never
22
+ // sanitized). The anchor-rel hardening runs last so it also covers component-built anchors.
23
+ const floor = options.unsafeDisableSanitize
24
+ ? []
25
+ : [[rehypeSanitize, buildSanitizeSchema(registry, options.sanitizeSchema)]];
26
+ const rehypePlugins = [
27
+ rehypeRaw,
28
+ ...floor,
29
+ [rehypeDispatch, registry, options.stagger],
30
+ rehypeSlug,
31
+ rehypeAnchorRel,
32
+ ];
17
33
  const processor = unified()
18
34
  .use(remarkParse)
19
35
  .use(remarkGfm)
@@ -24,6 +40,9 @@ export function createRenderer(registry, options = {}) {
24
40
  return {
25
41
  remarkPlugins,
26
42
  rehypePlugins,
27
- renderMarkdown: async (content) => String(await processor.process(content)),
43
+ renderMarkdown: async (content, opts = {}) => {
44
+ const file = new VFile({ value: content, data: { [CAIRN_RESOLVE]: opts.resolve } });
45
+ return String(await processor.process(file));
46
+ },
28
47
  };
29
48
  }
@@ -0,0 +1,8 @@
1
+ import type { VFile } from 'vfile';
2
+ /** The VFile data key the renderer sets the per-call resolver under. */
3
+ export declare const CAIRN_RESOLVE = "cairnResolve";
4
+ /** Resolve cairn: link nodes against the VFile's resolver. A non-cairn href and a malformed token
5
+ * pass through. A missing target is marked with the cairn-broken-link class (the resolver returns
6
+ * undefined) or, when the resolver throws, the error propagates and fails the build. */
7
+ export declare function remarkResolveCairnLinks(): (tree: unknown, file: VFile) => void;
8
+ //# sourceMappingURL=resolve-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-links.d.ts","sourceRoot":"","sources":["../../src/lib/render/resolve-links.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAGnC,wEAAwE;AACxE,eAAO,MAAM,aAAa,iBAAiB,CAAC;AAO5C;;yFAEyF;AACzF,wBAAgB,uBAAuB,KAC7B,MAAM,OAAO,EAAE,MAAM,KAAK,KAAG,IAAI,CAoB1C"}
@@ -0,0 +1,36 @@
1
+ // cairn-cms: the cairn: link resolver, an mdast step in the render pipeline (content-graph design).
2
+ // It runs before remark-rehype, so the rewritten href passes through the sanitize floor exactly as
3
+ // any other anchor. The per-call resolver is read off the VFile (set by renderMarkdown), so the
4
+ // processor is still built once. A miss either marks the link broken (preview) or throws (build),
5
+ // decided by the injected resolver.
6
+ import { visit } from 'unist-util-visit';
7
+ import { parseCairnToken } from '../content/links.js';
8
+ /** The VFile data key the renderer sets the per-call resolver under. */
9
+ export const CAIRN_RESOLVE = 'cairnResolve';
10
+ /** Resolve cairn: link nodes against the VFile's resolver. A non-cairn href and a malformed token
11
+ * pass through. A missing target is marked with the cairn-broken-link class (the resolver returns
12
+ * undefined) or, when the resolver throws, the error propagates and fails the build. */
13
+ export function remarkResolveCairnLinks() {
14
+ return (tree, file) => {
15
+ const resolve = file.data[CAIRN_RESOLVE];
16
+ if (!resolve)
17
+ return;
18
+ visit(tree, 'link', (node) => {
19
+ const ref = parseCairnToken(node.url);
20
+ if (!ref)
21
+ return;
22
+ const url = resolve(ref); // may throw (build backstop); propagates out of render
23
+ if (url) {
24
+ node.url = url;
25
+ return;
26
+ }
27
+ // Missing target in the preview: mark it broken and neutralize the href, keeping the text.
28
+ node.url = '#';
29
+ node.data = node.data ?? {};
30
+ const props = (node.data.hProperties = node.data.hProperties ?? {});
31
+ const existing = Array.isArray(props.className) ? props.className : [];
32
+ props.className = [...existing, 'cairn-broken-link'];
33
+ props.title = 'Broken internal link';
34
+ });
35
+ };
36
+ }
@@ -0,0 +1,20 @@
1
+ import { type Schema } from 'hast-util-sanitize';
2
+ import type { Root } from 'hast';
3
+ import { type ComponentRegistry } from './registry.js';
4
+ /**
5
+ * Build the delivery sanitize schema. Starts from hast-util-sanitize's defaultSchema, the
6
+ * GitHub-lineage allowlist that strips scripts, inline event handlers, and javascript:/data: URLs,
7
+ * then adds exactly what cairn's render needs. The directive markers (the fixed ones plus the
8
+ * dataAttr<Key> markers derived from the registry) survive so the dispatch reads its stamps after
9
+ * the floor. The benign author tags real content uses (nav, details, summary) and class/target/rel
10
+ * on anchors are admitted. A site extends the result through `extend`, always starting from this
11
+ * safe base, so it can add to the allowlist but not weaken the core strip.
12
+ */
13
+ export declare function buildSanitizeSchema(registry: ComponentRegistry, extend?: (defaults: Schema) => Schema): Schema;
14
+ /**
15
+ * Force rel="noopener noreferrer" on every target="_blank" anchor, to prevent reverse-tabnabbing.
16
+ * hast-util-sanitize runs no per-node hook, so this small transform carries the behavior the old
17
+ * DOMPurify preview pass enforced, now on the delivered output as well.
18
+ */
19
+ export declare function rehypeAnchorRel(): (tree: Root) => void;
20
+ //# sourceMappingURL=sanitize-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-schema.d.ts","sourceRoot":"","sources":["../../src/lib/render/sanitize-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,KAAK,EAAE,IAAI,EAAW,MAAM,MAAM,CAAC;AAE1C,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAMrE;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GACpC,MAAM,CA6BR;AAED;;;;GAIG;AACH,wBAAgB,eAAe,KACrB,MAAM,IAAI,UAOnB"}