@glw907/cairn-cms 0.24.0 → 0.29.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 (193) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +50 -37
  3. package/dist/auth/crypto.d.ts +0 -1
  4. package/dist/auth/store.d.ts +0 -1
  5. package/dist/auth/types.d.ts +0 -1
  6. package/dist/components/AdminLayout.svelte.d.ts +0 -1
  7. package/dist/components/ComponentForm.svelte.d.ts +0 -1
  8. package/dist/components/ComponentInsertDialog.svelte.d.ts +0 -1
  9. package/dist/components/ConceptList.svelte.d.ts +0 -1
  10. package/dist/components/ConfirmPage.svelte.d.ts +0 -1
  11. package/dist/components/DeleteDialog.svelte.d.ts +0 -1
  12. package/dist/components/EditPage.svelte.d.ts +0 -1
  13. package/dist/components/EditorToolbar.svelte.d.ts +0 -1
  14. package/dist/components/IconPicker.svelte.d.ts +0 -1
  15. package/dist/components/LinkPicker.svelte.d.ts +0 -1
  16. package/dist/components/LoginPage.svelte.d.ts +0 -1
  17. package/dist/components/ManageEditors.svelte.d.ts +0 -1
  18. package/dist/components/MarkdownEditor.svelte.d.ts +0 -1
  19. package/dist/components/NavTree.svelte.d.ts +0 -1
  20. package/dist/components/RenameDialog.svelte.d.ts +0 -1
  21. package/dist/components/index.d.ts +0 -1
  22. package/dist/components/link-completion.d.ts +0 -1
  23. package/dist/components/markdown-format.d.ts +0 -1
  24. package/dist/content/adapter.d.ts +0 -1
  25. package/dist/content/compose.d.ts +15 -5
  26. package/dist/content/compose.js +9 -5
  27. package/dist/content/concepts.d.ts +7 -1
  28. package/dist/content/concepts.js +49 -1
  29. package/dist/content/frontmatter.d.ts +0 -1
  30. package/dist/content/identity.d.ts +23 -0
  31. package/dist/content/identity.js +43 -0
  32. package/dist/content/ids.d.ts +0 -1
  33. package/dist/content/links.d.ts +0 -1
  34. package/dist/content/manifest.d.ts +23 -5
  35. package/dist/content/manifest.js +55 -32
  36. package/dist/content/permalink.d.ts +0 -1
  37. package/dist/content/schema.d.ts +0 -1
  38. package/dist/content/types.d.ts +0 -1
  39. package/dist/content/validate.d.ts +4 -2
  40. package/dist/content/validate.js +4 -1
  41. package/dist/delivery/CairnHead.svelte.d.ts +0 -1
  42. package/dist/delivery/content-index.d.ts +4 -1
  43. package/dist/delivery/content-index.js +8 -25
  44. package/dist/delivery/data.d.ts +23 -0
  45. package/dist/delivery/data.js +18 -0
  46. package/dist/delivery/excerpt.d.ts +0 -1
  47. package/dist/delivery/feeds.d.ts +0 -1
  48. package/dist/delivery/head.d.ts +0 -1
  49. package/dist/delivery/index.d.ts +1 -24
  50. package/dist/delivery/index.js +5 -20
  51. package/dist/delivery/json-ld.d.ts +0 -1
  52. package/dist/delivery/manifest.d.ts +0 -1
  53. package/dist/delivery/paginate.d.ts +0 -1
  54. package/dist/delivery/responses.d.ts +0 -1
  55. package/dist/delivery/robots.d.ts +0 -1
  56. package/dist/delivery/seo-fields.d.ts +0 -1
  57. package/dist/delivery/seo.d.ts +0 -1
  58. package/dist/delivery/site-descriptors.d.ts +0 -1
  59. package/dist/delivery/site-descriptors.js +5 -6
  60. package/dist/delivery/site-index.d.ts +0 -1
  61. package/dist/delivery/site-indexes.d.ts +0 -1
  62. package/dist/delivery/sitemap.d.ts +0 -1
  63. package/dist/email.d.ts +0 -1
  64. package/dist/env.d.ts +0 -1
  65. package/dist/github/credentials.d.ts +0 -1
  66. package/dist/github/repo.d.ts +0 -1
  67. package/dist/github/signing.d.ts +0 -1
  68. package/dist/github/types.d.ts +0 -1
  69. package/dist/index.d.ts +4 -30
  70. package/dist/index.js +2 -24
  71. package/dist/nav/site-config.d.ts +0 -1
  72. package/dist/render/component-grammar.d.ts +0 -1
  73. package/dist/render/component-insert.d.ts +0 -1
  74. package/dist/render/component-reference.d.ts +0 -1
  75. package/dist/render/component-validate.d.ts +0 -1
  76. package/dist/render/glyph.d.ts +0 -1
  77. package/dist/render/index.d.ts +0 -1
  78. package/dist/render/pipeline.d.ts +2 -3
  79. package/dist/render/pipeline.js +7 -2
  80. package/dist/render/registry.d.ts +0 -1
  81. package/dist/render/rehype-dispatch.d.ts +0 -1
  82. package/dist/render/remark-directives.d.ts +0 -1
  83. package/dist/render/resolve-links.d.ts +0 -1
  84. package/dist/render/sanitize-schema.d.ts +14 -1
  85. package/dist/render/sanitize-schema.js +96 -0
  86. package/dist/sveltekit/auth-routes.d.ts +0 -1
  87. package/dist/sveltekit/content-routes.d.ts +0 -1
  88. package/dist/sveltekit/editors-routes.d.ts +0 -1
  89. package/dist/sveltekit/guard.d.ts +0 -1
  90. package/dist/sveltekit/health.d.ts +0 -1
  91. package/dist/sveltekit/index.d.ts +1 -3
  92. package/dist/sveltekit/index.js +0 -1
  93. package/dist/sveltekit/nav-routes.d.ts +0 -1
  94. package/dist/sveltekit/public-routes.d.ts +0 -1
  95. package/dist/sveltekit/types.d.ts +0 -1
  96. package/dist/vite/bin.d.ts +2 -0
  97. package/dist/vite/bin.js +9 -0
  98. package/dist/vite/index.d.ts +32 -0
  99. package/dist/vite/index.js +178 -0
  100. package/package.json +22 -4
  101. package/src/lib/content/compose.ts +19 -10
  102. package/src/lib/content/concepts.ts +61 -1
  103. package/src/lib/content/identity.ts +60 -0
  104. package/src/lib/content/manifest.ts +69 -34
  105. package/src/lib/content/validate.ts +4 -1
  106. package/src/lib/delivery/content-index.ts +12 -27
  107. package/src/lib/delivery/data.ts +26 -0
  108. package/src/lib/delivery/index.ts +5 -28
  109. package/src/lib/delivery/site-descriptors.ts +5 -6
  110. package/src/lib/index.ts +4 -57
  111. package/src/lib/render/pipeline.ts +9 -3
  112. package/src/lib/render/sanitize-schema.ts +97 -0
  113. package/src/lib/sveltekit/index.ts +2 -8
  114. package/src/lib/vite/bin.ts +10 -0
  115. package/src/lib/vite/index.ts +213 -0
  116. package/dist/auth/crypto.d.ts.map +0 -1
  117. package/dist/auth/store.d.ts.map +0 -1
  118. package/dist/auth/types.d.ts.map +0 -1
  119. package/dist/components/AdminLayout.svelte.d.ts.map +0 -1
  120. package/dist/components/ComponentForm.svelte.d.ts.map +0 -1
  121. package/dist/components/ComponentInsertDialog.svelte.d.ts.map +0 -1
  122. package/dist/components/ConceptList.svelte.d.ts.map +0 -1
  123. package/dist/components/ConfirmPage.svelte.d.ts.map +0 -1
  124. package/dist/components/DeleteDialog.svelte.d.ts.map +0 -1
  125. package/dist/components/EditPage.svelte.d.ts.map +0 -1
  126. package/dist/components/EditorToolbar.svelte.d.ts.map +0 -1
  127. package/dist/components/IconPicker.svelte.d.ts.map +0 -1
  128. package/dist/components/LinkPicker.svelte.d.ts.map +0 -1
  129. package/dist/components/LoginPage.svelte.d.ts.map +0 -1
  130. package/dist/components/ManageEditors.svelte.d.ts.map +0 -1
  131. package/dist/components/MarkdownEditor.svelte.d.ts.map +0 -1
  132. package/dist/components/NavTree.svelte.d.ts.map +0 -1
  133. package/dist/components/RenameDialog.svelte.d.ts.map +0 -1
  134. package/dist/components/index.d.ts.map +0 -1
  135. package/dist/components/link-completion.d.ts.map +0 -1
  136. package/dist/components/markdown-format.d.ts.map +0 -1
  137. package/dist/content/adapter.d.ts.map +0 -1
  138. package/dist/content/compose.d.ts.map +0 -1
  139. package/dist/content/concepts.d.ts.map +0 -1
  140. package/dist/content/frontmatter.d.ts.map +0 -1
  141. package/dist/content/ids.d.ts.map +0 -1
  142. package/dist/content/links.d.ts.map +0 -1
  143. package/dist/content/manifest.d.ts.map +0 -1
  144. package/dist/content/permalink.d.ts.map +0 -1
  145. package/dist/content/schema.d.ts.map +0 -1
  146. package/dist/content/types.d.ts.map +0 -1
  147. package/dist/content/validate.d.ts.map +0 -1
  148. package/dist/delivery/CairnHead.svelte.d.ts.map +0 -1
  149. package/dist/delivery/content-index.d.ts.map +0 -1
  150. package/dist/delivery/excerpt.d.ts.map +0 -1
  151. package/dist/delivery/feeds.d.ts.map +0 -1
  152. package/dist/delivery/head.d.ts.map +0 -1
  153. package/dist/delivery/index.d.ts.map +0 -1
  154. package/dist/delivery/json-ld.d.ts.map +0 -1
  155. package/dist/delivery/manifest.d.ts.map +0 -1
  156. package/dist/delivery/paginate.d.ts.map +0 -1
  157. package/dist/delivery/responses.d.ts.map +0 -1
  158. package/dist/delivery/robots.d.ts.map +0 -1
  159. package/dist/delivery/seo-fields.d.ts.map +0 -1
  160. package/dist/delivery/seo.d.ts.map +0 -1
  161. package/dist/delivery/site-descriptors.d.ts.map +0 -1
  162. package/dist/delivery/site-index.d.ts.map +0 -1
  163. package/dist/delivery/site-indexes.d.ts.map +0 -1
  164. package/dist/delivery/sitemap.d.ts.map +0 -1
  165. package/dist/email.d.ts.map +0 -1
  166. package/dist/env.d.ts.map +0 -1
  167. package/dist/github/credentials.d.ts.map +0 -1
  168. package/dist/github/repo.d.ts.map +0 -1
  169. package/dist/github/signing.d.ts.map +0 -1
  170. package/dist/github/types.d.ts.map +0 -1
  171. package/dist/index.d.ts.map +0 -1
  172. package/dist/nav/site-config.d.ts.map +0 -1
  173. package/dist/render/component-grammar.d.ts.map +0 -1
  174. package/dist/render/component-insert.d.ts.map +0 -1
  175. package/dist/render/component-reference.d.ts.map +0 -1
  176. package/dist/render/component-validate.d.ts.map +0 -1
  177. package/dist/render/glyph.d.ts.map +0 -1
  178. package/dist/render/index.d.ts.map +0 -1
  179. package/dist/render/pipeline.d.ts.map +0 -1
  180. package/dist/render/registry.d.ts.map +0 -1
  181. package/dist/render/rehype-dispatch.d.ts.map +0 -1
  182. package/dist/render/remark-directives.d.ts.map +0 -1
  183. package/dist/render/resolve-links.d.ts.map +0 -1
  184. package/dist/render/sanitize-schema.d.ts.map +0 -1
  185. package/dist/sveltekit/auth-routes.d.ts.map +0 -1
  186. package/dist/sveltekit/content-routes.d.ts.map +0 -1
  187. package/dist/sveltekit/editors-routes.d.ts.map +0 -1
  188. package/dist/sveltekit/guard.d.ts.map +0 -1
  189. package/dist/sveltekit/health.d.ts.map +0 -1
  190. package/dist/sveltekit/index.d.ts.map +0 -1
  191. package/dist/sveltekit/nav-routes.d.ts.map +0 -1
  192. package/dist/sveltekit/public-routes.d.ts.map +0 -1
  193. package/dist/sveltekit/types.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, compose
12
12
  // builder and the request-time resolver ship from the delivery entry; this surface is the
13
13
  // grammar, the manifest operations, and their types a migrating site adopts.
14
14
  export { parseCairnToken, extractCairnLinks, formatCairnToken, escapeLinkText } from './content/links.js';
15
- export { serializeManifest, parseManifest, emptyManifest, verifyManifest, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
15
+ export { serializeManifest, parseManifest, emptyManifest, verifyManifest, diffManifests, upsertEntry, removeEntry, manifestEntryFromFile, manifestLinkResolver, inboundLinks, } from './content/manifest.js';
16
16
  // Render engine (Plan 04): generic directive pipeline; sites own the component registry.
17
17
  export { defineRegistry, emptyValues } from './render/registry.js';
18
18
  export { serializeComponent, parseComponent } from './render/component-grammar.js';
@@ -21,30 +21,8 @@ export { buildComponentInsert } from './render/component-insert.js';
21
21
  export { generateComponentReference } from './render/component-reference.js';
22
22
  export { glyph } from './render/glyph.js';
23
23
  export { remarkDirectiveStamp } from './render/remark-directives.js';
24
- export { rehypeDispatch, isElement, strProp, iconSpan, cardShell, headRow, markFirstList, } from './render/rehype-dispatch.js';
24
+ export { rehypeDispatch, iconSpan, cardShell, headRow } from './render/rehype-dispatch.js';
25
25
  export { createRenderer } from './render/pipeline.js';
26
26
  export { CommitConflictError } from './github/types.js';
27
- export { appJwt, installationToken, signingSelfTest } from './github/signing.js';
28
- export { treeUrl, markdownFilesIn, listMarkdown, contentsUrl, readRaw, fileSha, commitFile, } from './github/repo.js';
29
- export { appCredentials } from './github/credentials.js';
30
27
  // Nav tree and site-config helpers (Plan 06).
31
28
  export { parseSiteConfig, urlPolicyFrom, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
32
- // Public content delivery (public-delivery design): the query index, syndication, and
33
- // discovery surface that sites read. Pure builders plus the one permalink resolver; the
34
- // SvelteKit loaders live under the /sveltekit subpath.
35
- export { permalink } from './content/permalink.js';
36
- export { createContentIndex, fromGlob } from './delivery/content-index.js';
37
- export { createSiteIndex } from './delivery/site-index.js';
38
- export { createSiteIndexes } from './delivery/site-indexes.js';
39
- export { deriveExcerpt, wordCount } from './delivery/excerpt.js';
40
- export { buildRssFeed, buildJsonFeed } from './delivery/feeds.js';
41
- export { buildSitemap } from './delivery/sitemap.js';
42
- export { buildRobots } from './delivery/robots.js';
43
- export { buildSeoMeta } from './delivery/seo.js';
44
- export { readSeoFields, resolveImageUrl } from './delivery/seo-fields.js';
45
- export { paginate } from './delivery/paginate.js';
46
- // Root superset of the delivery route surface: a wrong guess from root for a route loader or a
47
- // response helper now resolves. The CairnHead component stays out of root so the root barrel stays
48
- // node-importable for the unit suite; it resolves from @glw907/cairn-cms/delivery/head.
49
- export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './delivery/responses.js';
50
- export { createPublicRoutes } from './sveltekit/public-routes.js';
@@ -52,4 +52,3 @@ export declare function urlPolicyFrom(config: SiteConfig): Record<string, Concep
52
52
  * serializes without `url`/`children` keys.
53
53
  */
54
54
  export declare function setMenu(raw: string, name: string, tree: NavNode[]): string;
55
- //# sourceMappingURL=site-config.d.ts.map
@@ -14,4 +14,3 @@ export declare function parseComponentWithRawKeys(markdown: string, def: Compone
14
14
  values: ComponentValues;
15
15
  rawKeys: string[];
16
16
  }>;
17
- //# sourceMappingURL=component-grammar.d.ts.map
@@ -11,4 +11,3 @@ export type ComponentInsert = {
11
11
  /** Serialize a component's form values, then validate the result against its schema. Returns the
12
12
  * markdown to insert at the cursor, or the field errors keyed by attribute key or slot name. */
13
13
  export declare function buildComponentInsert(def: ComponentDef, values: ComponentValues): Promise<ComponentInsert>;
14
- //# sourceMappingURL=component-insert.d.ts.map
@@ -8,4 +8,3 @@ export interface ReferenceOptions {
8
8
  /** Build a self-contained markdown reference (the llms-full.txt shape) for a component registry, for
9
9
  * authors and for pointing an LLM at one curated file. */
10
10
  export declare function generateComponentReference(registry: ComponentRegistry, opts: ReferenceOptions): string;
11
- //# sourceMappingURL=component-reference.d.ts.map
@@ -7,4 +7,3 @@ export type ComponentValidation = {
7
7
  errors: Record<string, string>;
8
8
  };
9
9
  export declare function validateComponent(markdown: string, def: ComponentDef): Promise<ComponentValidation>;
10
- //# sourceMappingURL=component-validate.d.ts.map
@@ -6,4 +6,3 @@ export type IconSet = Record<string, string>;
6
6
  * a stray empty (or undefined) path. Callers always wrap the returned element, so the shell
7
7
  * keeps them safe. */
8
8
  export declare function glyph(name: string, icons: IconSet): Element;
9
- //# sourceMappingURL=glyph.d.ts.map
@@ -3,4 +3,3 @@ export * from './glyph.js';
3
3
  export * from './remark-directives.js';
4
4
  export * from './rehype-dispatch.js';
5
5
  export * from './pipeline.js';
6
- //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { type PluggableList } from 'unified';
2
2
  import type { Schema } from 'hast-util-sanitize';
3
- import type { ComponentRegistry } from './registry.js';
3
+ import { type ComponentRegistry } from './registry.js';
4
4
  import type { LinkResolve } from '../content/links.js';
5
5
  export interface RendererOptions {
6
6
  /** Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
@@ -24,11 +24,10 @@ export interface RendererOptions {
24
24
  /** Compose a site's render pipeline from its component registry: directive syntax to
25
25
  * stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
26
26
  * rehype plugin arrays (so the admin editor preview can reuse the exact same set). */
27
- export declare function createRenderer(registry: ComponentRegistry, options?: RendererOptions): {
27
+ export declare function createRenderer(registry?: ComponentRegistry, options?: RendererOptions): {
28
28
  remarkPlugins: PluggableList;
29
29
  rehypePlugins: PluggableList;
30
30
  renderMarkdown: (content: string, opts?: {
31
31
  resolve?: LinkResolve;
32
32
  }) => Promise<string>;
33
33
  };
34
- //# sourceMappingURL=pipeline.d.ts.map
@@ -8,14 +8,15 @@ import rehypeSlug from 'rehype-slug';
8
8
  import rehypeStringify from 'rehype-stringify';
9
9
  import rehypeSanitize from 'rehype-sanitize';
10
10
  import { VFile } from 'vfile';
11
- import { buildSanitizeSchema, rehypeAnchorRel } from './sanitize-schema.js';
11
+ import { buildSanitizeSchema, rehypeAnchorRel, rehypeSinkGuard } from './sanitize-schema.js';
12
12
  import { remarkDirectiveStamp } from './remark-directives.js';
13
13
  import { remarkResolveCairnLinks, CAIRN_RESOLVE } from './resolve-links.js';
14
14
  import { rehypeDispatch } from './rehype-dispatch.js';
15
+ import { defineRegistry } from './registry.js';
15
16
  /** Compose a site's render pipeline from its component registry: directive syntax to
16
17
  * stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
17
18
  * rehype plugin arrays (so the admin editor preview can reuse the exact same set). */
18
- export function createRenderer(registry, options = {}) {
19
+ export function createRenderer(registry = defineRegistry({ components: [] }), options = {}) {
19
20
  const remarkPlugins = [remarkDirective, [remarkDirectiveStamp, registry], remarkResolveCairnLinks];
20
21
  // The sanitize floor runs after rehype-raw (so author raw HTML is parsed, then cleaned) and
21
22
  // before the dispatch (so the site's trusted build() output and its inline SVG icons are never
@@ -32,6 +33,10 @@ export function createRenderer(registry, options = {}) {
32
33
  ];
33
34
  if (rel !== false)
34
35
  rehypePlugins.push([rehypeAnchorRel, rel]);
36
+ // The sink guard runs last, over the fully-built tree, so it neutralizes a sink a component
37
+ // build() emitted after the floor. Gated by the same switch as the floor.
38
+ if (!options.unsafeDisableSanitize)
39
+ rehypePlugins.push(rehypeSinkGuard);
35
40
  const processor = unified()
36
41
  .use(remarkParse)
37
42
  .use(remarkGfm)
@@ -91,4 +91,3 @@ export interface ComponentValues {
91
91
  /** Seed an empty {@link ComponentValues} from a component's schema: attribute defaults (or '' / false)
92
92
  * and empty slot values ([] for repeatable, '' otherwise). */
93
93
  export declare function emptyValues(def: ComponentDef): ComponentValues;
94
- //# sourceMappingURL=registry.d.ts.map
@@ -23,4 +23,3 @@ export declare function markFirstList(children: ElementContent[]): Element | und
23
23
  * `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
24
24
  * content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
25
25
  export declare function rehypeDispatch(registry: ComponentRegistry, stagger?: boolean): (tree: Root) => void;
26
- //# sourceMappingURL=rehype-dispatch.d.ts.map
@@ -1,4 +1,3 @@
1
1
  import type { Root } from 'mdast';
2
2
  import { type ComponentRegistry } from './registry.js';
3
3
  export declare function remarkDirectiveStamp(registry: ComponentRegistry): (tree: Root) => void;
4
- //# sourceMappingURL=remark-directives.d.ts.map
@@ -5,4 +5,3 @@ export declare const CAIRN_RESOLVE = "cairnResolve";
5
5
  * pass through. A missing target is marked with the cairn-broken-link class (the resolver returns
6
6
  * undefined) or, when the resolver throws, the error propagates and fails the build. */
7
7
  export declare function remarkResolveCairnLinks(): (tree: unknown, file: VFile) => void;
8
- //# sourceMappingURL=resolve-links.d.ts.map
@@ -18,4 +18,17 @@ export declare function buildSanitizeSchema(registry: ComponentRegistry, extend?
18
18
  * `anchorRel` option (default `noopener noreferrer`); a site can override it or disable it entirely.
19
19
  */
20
20
  export declare function rehypeAnchorRel(rel: string): (tree: Root) => void;
21
- //# sourceMappingURL=sanitize-schema.d.ts.map
21
+ /**
22
+ * Post-dispatch safety floor over the fully-built tree. The pre-dispatch rehype-sanitize floor
23
+ * cleans author content, but a component build() runs after it and can route a raw author
24
+ * attribute value into a sink. This guard runs last and neutralizes those sinks on every element
25
+ * no matter which plugin or which build() produced it: an unsafe URL scheme in a URL-bearing
26
+ * attribute, an inline on* event handler, or an inline style (stripped wholesale, matching the
27
+ * floor and cairn's class-driven styling). It is gated by the same unsafeDisableSanitize switch as
28
+ * the floor.
29
+ *
30
+ * The guard's boundary is the URL scheme check plus the on* and style strip. It does not remove a
31
+ * build()-emitted raw script, style, or iframe srcdoc element node. A build() that emits those is
32
+ * running site-developer code, and author markdown is cleaned by the pre-dispatch floor.
33
+ */
34
+ export declare function rehypeSinkGuard(): (tree: Root) => void;
@@ -56,3 +56,99 @@ export function rehypeAnchorRel(rel) {
56
56
  });
57
57
  };
58
58
  }
59
+ // URL-bearing hast properties the post-dispatch guard scheme-checks. hast camelCases attribute
60
+ // names through property-information (srcset -> srcSet, xlink:href -> xLinkHref with a capital L,
61
+ // formaction -> formAction). data is the <object data> URL attribute; data-* attributes camelCase
62
+ // to dataFoo and are not matched here.
63
+ const URL_PROPS = new Set([
64
+ 'href',
65
+ 'src',
66
+ 'srcSet',
67
+ 'xLinkHref',
68
+ 'poster',
69
+ 'formAction',
70
+ 'action',
71
+ 'data',
72
+ 'background',
73
+ ]);
74
+ // The safe URL schemes: the union of every protocol list in defaultSchema, plus cairn. The
75
+ // floor admits these and strips the rest, so deriving from the same source keeps the floor and
76
+ // this guard from drifting on what a safe scheme is. javascript:/data:/vbscript: are never in
77
+ // defaultSchema, so they are never safe.
78
+ const SAFE_SCHEMES = (() => {
79
+ const protocols = defaultSchema.protocols ?? {};
80
+ const schemes = new Set(['cairn']);
81
+ for (const list of Object.values(protocols)) {
82
+ for (const scheme of list ?? [])
83
+ schemes.add(String(scheme).toLowerCase());
84
+ }
85
+ return schemes;
86
+ })();
87
+ // Read a URL value's scheme for the safety check, defeating the whitespace and control-character
88
+ // tricks a browser ignores inside a scheme (java\tscript:, a leading space). A value with no
89
+ // scheme (relative, anchor, query) returns undefined and is always safe.
90
+ function urlScheme(value) {
91
+ const cleaned = value.replace(/[\x00-\x20]+/g, '');
92
+ const match = /^([a-z][a-z0-9+.-]*):/i.exec(cleaned);
93
+ return match ? match[1].toLowerCase() : undefined;
94
+ }
95
+ function isSafeUrl(value) {
96
+ const scheme = urlScheme(value);
97
+ return scheme === undefined || SAFE_SCHEMES.has(scheme);
98
+ }
99
+ // srcset is "url descriptor, url descriptor, …". hast may store it as a string or, because
100
+ // property-information marks it comma-separated, as a string array. One unsafe candidate makes
101
+ // the whole attribute unsafe.
102
+ function isSafeSrcset(value) {
103
+ const candidates = Array.isArray(value)
104
+ ? value.map(String)
105
+ : typeof value === 'string'
106
+ ? value.split(',')
107
+ : [];
108
+ return candidates.every((candidate) => {
109
+ const url = candidate.trim().split(/\s+/)[0];
110
+ return url === '' || isSafeUrl(url);
111
+ });
112
+ }
113
+ // Decide whether one URL-bearing property value is safe to keep. srcset has its own
114
+ // multi-candidate grammar. A non-string value carries no scheme to abuse, so the floor's own
115
+ // handling stands and the guard leaves it alone.
116
+ function isSafeUrlProp(key, value) {
117
+ if (key === 'srcSet')
118
+ return isSafeSrcset(value);
119
+ if (typeof value !== 'string')
120
+ return true;
121
+ return isSafeUrl(value);
122
+ }
123
+ /**
124
+ * Post-dispatch safety floor over the fully-built tree. The pre-dispatch rehype-sanitize floor
125
+ * cleans author content, but a component build() runs after it and can route a raw author
126
+ * attribute value into a sink. This guard runs last and neutralizes those sinks on every element
127
+ * no matter which plugin or which build() produced it: an unsafe URL scheme in a URL-bearing
128
+ * attribute, an inline on* event handler, or an inline style (stripped wholesale, matching the
129
+ * floor and cairn's class-driven styling). It is gated by the same unsafeDisableSanitize switch as
130
+ * the floor.
131
+ *
132
+ * The guard's boundary is the URL scheme check plus the on* and style strip. It does not remove a
133
+ * build()-emitted raw script, style, or iframe srcdoc element node. A build() that emits those is
134
+ * running site-developer code, and author markdown is cleaned by the pre-dispatch floor.
135
+ */
136
+ export function rehypeSinkGuard() {
137
+ return (tree) => {
138
+ visit(tree, 'element', (node) => {
139
+ const props = node.properties;
140
+ if (!props)
141
+ return;
142
+ for (const key of Object.keys(props)) {
143
+ if (/^on/i.test(key) || key === 'style') {
144
+ delete props[key];
145
+ continue;
146
+ }
147
+ if (!URL_PROPS.has(key))
148
+ continue;
149
+ if (!isSafeUrlProp(key, props[key]))
150
+ delete props[key];
151
+ }
152
+ });
153
+ };
154
+ }
@@ -20,4 +20,3 @@ export declare function createAuthRoutes(config: AuthRoutesConfig): {
20
20
  confirmAction: (event: RequestContext) => Promise<never>;
21
21
  logoutAction: (event: RequestContext) => Promise<never>;
22
22
  };
23
- //# sourceMappingURL=auth-routes.d.ts.map
@@ -89,4 +89,3 @@ export declare function createContentRoutes(runtime: CairnRuntime, deps?: Conten
89
89
  renameAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
90
90
  mintToken: (env: GithubKeyEnv) => Promise<string>;
91
91
  };
92
- //# sourceMappingURL=content-routes.d.ts.map
@@ -21,4 +21,3 @@ export declare function createEditorRoutes(): {
21
21
  ok: true;
22
22
  }>;
23
23
  };
24
- //# sourceMappingURL=editors-routes.d.ts.map
@@ -6,4 +6,3 @@ export declare function createAuthGuard(): ({ event, resolve }: HandleInput) =>
6
6
  export declare function requireSession(event: RequestContext): Editor;
7
7
  /** For the management surface: a signed-in owner, or 403 for an editor. */
8
8
  export declare function requireOwner(event: RequestContext): Editor;
9
- //# sourceMappingURL=guard.d.ts.map
@@ -16,4 +16,3 @@ export declare function healthLoad(event: {
16
16
  env?: GithubKeyEnv;
17
17
  };
18
18
  }, runtime: CairnRuntime): Promise<HealthData>;
19
- //# sourceMappingURL=health.d.ts.map
@@ -7,6 +7,4 @@ export { createNavRoutes } from './nav-routes.js';
7
7
  export type { NavLoadData, NavPageOption, NavRoutesDeps } from './nav-routes.js';
8
8
  export { healthLoad, type HealthData } from './health.js';
9
9
  export type { RequestContext, CookieJar, HandleInput } from './types.js';
10
- export { createPublicRoutes } from './public-routes.js';
11
- export type { PublicRoutesDeps, ListData as PublicListData, TagData, TagIndexData, EntryData, } from './public-routes.js';
12
- //# sourceMappingURL=index.d.ts.map
10
+ export type { GithubKeyEnv } from '../github/credentials.js';
@@ -6,4 +6,3 @@ export { createEditorRoutes } from './editors-routes.js';
6
6
  export { createContentRoutes } from './content-routes.js';
7
7
  export { createNavRoutes } from './nav-routes.js';
8
8
  export { healthLoad } from './health.js';
9
- export { createPublicRoutes } from './public-routes.js';
@@ -27,4 +27,3 @@ export declare function createNavRoutes(runtime: CairnRuntime, deps?: NavRoutesD
27
27
  navLoad: (event: ContentEvent) => Promise<NavLoadData>;
28
28
  navSave: (event: ContentEvent) => Promise<never>;
29
29
  };
30
- //# sourceMappingURL=nav-routes.d.ts.map
@@ -64,4 +64,3 @@ export declare function createPublicRoutes(deps: PublicRoutesDeps): {
64
64
  path: string;
65
65
  }[];
66
66
  };
67
- //# sourceMappingURL=public-routes.d.ts.map
@@ -35,4 +35,3 @@ export interface HandleInput {
35
35
  event: RequestContext;
36
36
  resolve(event: RequestContext): Promise<Response> | Response;
37
37
  }
38
- //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ // cairn-manifest: the regenerate command. It evaluates the cairnManifest virtual module in write mode
3
+ // through the consumer's own Vite resolution and writes the canonical content manifest. A thin shell
4
+ // over writeManifest so the write logic stays testable apart from the CLI.
5
+ import { writeManifest } from './index.js';
6
+ writeManifest(process.cwd()).catch((err) => {
7
+ console.error(err instanceof Error ? err.message : String(err));
8
+ process.exit(1);
9
+ });
@@ -0,0 +1,32 @@
1
+ import type { Plugin, PluginOption } from 'vite';
2
+ /** Options for {@link cairnManifest}. Paths are app-root-absolute (the form `import.meta.glob` wants),
3
+ * so they match the build's own resolution. */
4
+ export interface CairnManifestOptions {
5
+ /** The module exporting the `cairn` adapter and the parsed `siteConfig`, app-root-absolute. */
6
+ configModule: string;
7
+ /** Per-concept content globs, keyed by concept id, app-root-absolute. */
8
+ content: Record<string, string>;
9
+ /** The committed manifest path, app-root-absolute. Defaults to `/src/content/.cairn/index.json`. */
10
+ manifestPath?: string;
11
+ }
12
+ /** Flatten the consumer's plugins option and drop the cairnManifest plugin at any nesting depth, so
13
+ * the nested verify server can never re-enter its buildStart. Vite supports (and flattens) nested
14
+ * plugin arrays, and findCairnOptions recurses into them, so a flat single-level filter would miss a
15
+ * cairnManifest nested inside a shared preset's sub-array and let it survive into the nested server.
16
+ * This mirrors findCairnOptions's recursion. Falsy slots pass through, which Vite tolerates. */
17
+ export declare function stripCairnManifest(plugins: PluginOption | PluginOption[]): PluginOption[];
18
+ /** Verify the committed manifest against the corpus from a Vite context, throwing on drift. The bin
19
+ * and the plugin share this; the spike proved it runs cleanly inside the consumer's config. */
20
+ export declare function verifyManifestFromVite(opts: CairnManifestOptions, root: string): Promise<void>;
21
+ /** Regenerate the serialized manifest from the corpus in a Vite context, sharing the build's
22
+ * resolution. The cairn-manifest bin (a later task) will call this and write the result. */
23
+ export declare function buildManifestFromVite(opts: CairnManifestOptions, root: string): Promise<string>;
24
+ /** The cairnManifest plugin. It serves the verify virtual module to the app graph and, in
25
+ * buildStart, evaluates it through a nested Vite SSR load so a manifest drift fails the build. */
26
+ export declare function cairnManifest(opts: CairnManifestOptions): Plugin;
27
+ /** Regenerate the committed manifest from the consumer's corpus and write it to the configured
28
+ * manifestPath. It loads the consumer's Vite config from `cwd`, reads the cairnManifest plugin's
29
+ * options off the instance, evaluates the write-mode virtual module through the build's own
30
+ * resolution, and writes the serialized manifest. The cairn-manifest bin calls this; it is exported
31
+ * so the write logic is testable apart from the CLI shell. */
32
+ export declare function writeManifest(cwd?: string): Promise<void>;
@@ -0,0 +1,178 @@
1
+ import { writeFile, mkdir } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ /** The key the cairnManifest plugin stashes its options under, so the write path can read them off the
4
+ * plugin instance in the consumer's loaded config without re-parsing the config file. */
5
+ const CAIRN_OPTIONS = Symbol.for('cairn-cms.manifest-options');
6
+ const VIRTUAL_ID = 'virtual:cairn-manifest';
7
+ const RESOLVED_ID = '\0' + VIRTUAL_ID;
8
+ /** The default committed manifest path, app-root-absolute. */
9
+ const DEFAULT_MANIFEST_PATH = '/src/content/.cairn/index.json';
10
+ /** Build the virtual module source. In verify mode it throws on drift; in write mode it exports the
11
+ * serialized manifest as `result`. The module runs in the app graph, so its `import.meta.glob`,
12
+ * package, and `?raw` resolution is the build's own. */
13
+ function virtualSource(opts, mode) {
14
+ const manifestPath = opts.manifestPath ?? DEFAULT_MANIFEST_PATH;
15
+ const globEntries = Object.entries(opts.content)
16
+ .map(([id, pattern]) => ` ${JSON.stringify(id)}: import.meta.glob(${JSON.stringify(pattern)}, { query: '?raw', import: 'default', eager: true }),`)
17
+ .join('\n');
18
+ // In write mode the committed file may not exist yet, so do not import it.
19
+ const committedImport = mode === 'verify' ? `import committed from ${JSON.stringify(manifestPath + '?raw')};` : '';
20
+ const resultExpr = mode === 'write' ? 'serializeManifest(built)' : '(verifyManifest(built, committed), "ok")';
21
+ return `
22
+ import { buildSiteManifest } from '@glw907/cairn-cms/delivery/data';
23
+ import { serializeManifest, verifyManifest } from '@glw907/cairn-cms';
24
+ import { cairn, siteConfig } from ${JSON.stringify(opts.configModule)};
25
+ ${committedImport}
26
+ const globs = {
27
+ ${globEntries}
28
+ };
29
+ const built = buildSiteManifest(cairn, siteConfig, globs);
30
+ export const result = ${resultExpr};
31
+ `;
32
+ }
33
+ /** Evaluate the virtual module in the given mode inside the consumer's own Vite resolution, then
34
+ * return the module's `result`. It reuses the consumer's loaded config (so `$lib`, the config
35
+ * module, `import.meta.glob`, and `?raw` resolve exactly as the build does) and strips the
36
+ * cairnManifest plugin from the nested server's plugin list, so its buildStart never recurses.
37
+ * This runs at build time and in the bin, never in the request lifecycle. */
38
+ async function evalVirtual(opts, mode, root) {
39
+ const { createServer, loadConfigFromFile } = await import('vite');
40
+ // Load the consumer's real Vite config so the nested server inherits SvelteKit's resolution
41
+ // (the $lib alias, the app root, the ?raw and import.meta.glob handling). Drop cairnManifest from
42
+ // it so the nested server's buildStart does not recurse, and add a plugin that serves only the
43
+ // virtual module in the requested mode.
44
+ const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' }, undefined, root);
45
+ const inlineConfig = loaded?.config ?? {};
46
+ const server = await createServer({
47
+ ...inlineConfig,
48
+ root,
49
+ configFile: false,
50
+ logLevel: 'silent',
51
+ server: { middlewareMode: true, hmr: false, watch: null },
52
+ plugins: [...stripCairnManifest(inlineConfig.plugins ?? []), cairnVirtualOnly(opts, mode)],
53
+ });
54
+ try {
55
+ const mod = (await server.ssrLoadModule(VIRTUAL_ID));
56
+ return mod.result;
57
+ }
58
+ finally {
59
+ await server.close();
60
+ }
61
+ }
62
+ /** True for any plugin object whose name is the cairnManifest plugin, so the nested server drops it
63
+ * and cannot recurse into another buildStart. The consumer's plugin list may nest arrays and hold
64
+ * falsy slots, so guard the shape. */
65
+ function isCairnManifestPlugin(p) {
66
+ return !!p && typeof p === 'object' && 'name' in p && p.name === 'cairn-manifest';
67
+ }
68
+ /** Flatten the consumer's plugins option and drop the cairnManifest plugin at any nesting depth, so
69
+ * the nested verify server can never re-enter its buildStart. Vite supports (and flattens) nested
70
+ * plugin arrays, and findCairnOptions recurses into them, so a flat single-level filter would miss a
71
+ * cairnManifest nested inside a shared preset's sub-array and let it survive into the nested server.
72
+ * This mirrors findCairnOptions's recursion. Falsy slots pass through, which Vite tolerates. */
73
+ export function stripCairnManifest(plugins) {
74
+ if (Array.isArray(plugins))
75
+ return plugins.flatMap(stripCairnManifest);
76
+ if (isCairnManifestPlugin(plugins))
77
+ return [];
78
+ return [plugins];
79
+ }
80
+ /** Verify the committed manifest against the corpus from a Vite context, throwing on drift. The bin
81
+ * and the plugin share this; the spike proved it runs cleanly inside the consumer's config. */
82
+ export async function verifyManifestFromVite(opts, root) {
83
+ await evalVirtual(opts, 'verify', root);
84
+ }
85
+ /** Regenerate the serialized manifest from the corpus in a Vite context, sharing the build's
86
+ * resolution. The cairn-manifest bin (a later task) will call this and write the result. */
87
+ export async function buildManifestFromVite(opts, root) {
88
+ return evalVirtual(opts, 'write', root);
89
+ }
90
+ /** The cairnManifest plugin. It serves the verify virtual module to the app graph and, in
91
+ * buildStart, evaluates it through a nested Vite SSR load so a manifest drift fails the build. */
92
+ export function cairnManifest(opts) {
93
+ let root = process.cwd();
94
+ const plugin = {
95
+ name: 'cairn-manifest',
96
+ configResolved(config) {
97
+ // Capture the resolved app root so the nested server loads the same config the build did.
98
+ root = config.root;
99
+ },
100
+ resolveId(id) {
101
+ if (id === VIRTUAL_ID)
102
+ return RESOLVED_ID;
103
+ },
104
+ load(id) {
105
+ if (id === RESOLVED_ID)
106
+ return virtualSource(opts, 'verify');
107
+ },
108
+ async buildStart() {
109
+ try {
110
+ await verifyManifestFromVite(opts, root);
111
+ }
112
+ catch (err) {
113
+ this.error(err instanceof Error ? err.message : String(err));
114
+ }
115
+ },
116
+ };
117
+ // Stash the options on the instance so the cairn-manifest bin's writeManifest can read the content
118
+ // globs, config module, and manifest path off the plugin in the consumer's loaded config, sharing
119
+ // exactly the options the build verifies with.
120
+ plugin[CAIRN_OPTIONS] = opts;
121
+ return plugin;
122
+ }
123
+ /** Regenerate the committed manifest from the consumer's corpus and write it to the configured
124
+ * manifestPath. It loads the consumer's Vite config from `cwd`, reads the cairnManifest plugin's
125
+ * options off the instance, evaluates the write-mode virtual module through the build's own
126
+ * resolution, and writes the serialized manifest. The cairn-manifest bin calls this; it is exported
127
+ * so the write logic is testable apart from the CLI shell. */
128
+ export async function writeManifest(cwd = process.cwd()) {
129
+ const { loadConfigFromFile } = await import('vite');
130
+ const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' }, undefined, cwd);
131
+ if (!loaded) {
132
+ throw new Error(`cairn-manifest: no Vite config found in ${cwd}`);
133
+ }
134
+ const opts = findCairnOptions(loaded.config.plugins);
135
+ if (!opts) {
136
+ throw new Error('cairn-manifest: the Vite config has no cairnManifest() plugin. Add it so the bin shares the build options.');
137
+ }
138
+ const serialized = await buildManifestFromVite(opts, cwd);
139
+ const manifestPath = opts.manifestPath ?? DEFAULT_MANIFEST_PATH;
140
+ // The manifest path is app-root-absolute (a leading slash relative to the project), so resolve it
141
+ // against cwd, not the filesystem root.
142
+ const outPath = join(cwd, manifestPath.replace(/^\//, ''));
143
+ await mkdir(dirname(outPath), { recursive: true });
144
+ await writeFile(outPath, serialized);
145
+ }
146
+ /** Walk a Vite plugins option (which may nest arrays, hold falsy slots, or be a thenable) and return
147
+ * the stashed cairnManifest options from the first matching plugin, or null if there is none. */
148
+ function findCairnOptions(plugins) {
149
+ if (!plugins)
150
+ return null;
151
+ if (Array.isArray(plugins)) {
152
+ for (const p of plugins) {
153
+ const found = findCairnOptions(p);
154
+ if (found)
155
+ return found;
156
+ }
157
+ return null;
158
+ }
159
+ if (typeof plugins === 'object' && CAIRN_OPTIONS in plugins) {
160
+ return plugins[CAIRN_OPTIONS] ?? null;
161
+ }
162
+ return null;
163
+ }
164
+ /** A minimal plugin that serves only the virtual module in one mode, for the nested SSR load. It
165
+ * carries no buildStart, so the nested server never recurses into the verify. */
166
+ function cairnVirtualOnly(opts, mode) {
167
+ return {
168
+ name: 'cairn-manifest-virtual',
169
+ resolveId(id) {
170
+ if (id === VIRTUAL_ID)
171
+ return RESOLVED_ID;
172
+ },
173
+ load(id) {
174
+ if (id === RESOLVED_ID)
175
+ return virtualSource(opts, mode);
176
+ },
177
+ };
178
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glw907/cairn-cms",
3
- "version": "0.24.0",
3
+ "version": "0.29.0",
4
4
  "description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -13,6 +13,10 @@
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/glw907/cairn-cms.git"
15
15
  },
16
+ "homepage": "https://github.com/glw907/cairn-cms#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/glw907/cairn-cms/issues"
19
+ },
16
20
  "keywords": [
17
21
  "cms",
18
22
  "sveltekit",
@@ -22,9 +26,10 @@
22
26
  "markdown"
23
27
  ],
24
28
  "scripts": {
25
- "package": "svelte-package",
29
+ "package": "svelte-package && chmod +x dist/vite/bin.js",
26
30
  "check:package": "npm run package && publint --strict && attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm internal-resolution-error",
27
- "prepare": "svelte-package",
31
+ "check:reference": "npm run package && node scripts/reference-coverage.mjs",
32
+ "prepare": "npm run package",
28
33
  "check": "svelte-check --tsconfig ./tsconfig.json",
29
34
  "test": "vitest run",
30
35
  "test:watch": "vitest",
@@ -58,11 +63,24 @@
58
63
  "svelte": "./dist/delivery/head.js",
59
64
  "default": "./dist/delivery/head.js"
60
65
  },
66
+ "./delivery/data": {
67
+ "types": "./dist/delivery/data.d.ts",
68
+ "svelte": "./dist/delivery/data.js",
69
+ "default": "./dist/delivery/data.js"
70
+ },
71
+ "./vite": {
72
+ "types": "./dist/vite/index.d.ts",
73
+ "default": "./dist/vite/index.js"
74
+ },
61
75
  "./package.json": "./package.json"
62
76
  },
77
+ "bin": {
78
+ "cairn-manifest": "./dist/vite/bin.js"
79
+ },
63
80
  "files": [
64
81
  "dist",
65
- "src/lib"
82
+ "src/lib",
83
+ "CHANGELOG.md"
66
84
  ],
67
85
  "peerDependencies": {
68
86
  "@sveltejs/kit": "^2",