@glw907/cairn-cms 0.26.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 (185) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/auth/crypto.d.ts +0 -1
  3. package/dist/auth/store.d.ts +0 -1
  4. package/dist/auth/types.d.ts +0 -1
  5. package/dist/components/AdminLayout.svelte.d.ts +0 -1
  6. package/dist/components/ComponentForm.svelte.d.ts +0 -1
  7. package/dist/components/ComponentInsertDialog.svelte.d.ts +0 -1
  8. package/dist/components/ConceptList.svelte.d.ts +0 -1
  9. package/dist/components/ConfirmPage.svelte.d.ts +0 -1
  10. package/dist/components/DeleteDialog.svelte.d.ts +0 -1
  11. package/dist/components/EditPage.svelte.d.ts +0 -1
  12. package/dist/components/EditorToolbar.svelte.d.ts +0 -1
  13. package/dist/components/IconPicker.svelte.d.ts +0 -1
  14. package/dist/components/LinkPicker.svelte.d.ts +0 -1
  15. package/dist/components/LoginPage.svelte.d.ts +0 -1
  16. package/dist/components/ManageEditors.svelte.d.ts +0 -1
  17. package/dist/components/MarkdownEditor.svelte.d.ts +0 -1
  18. package/dist/components/NavTree.svelte.d.ts +0 -1
  19. package/dist/components/RenameDialog.svelte.d.ts +0 -1
  20. package/dist/components/index.d.ts +0 -1
  21. package/dist/components/link-completion.d.ts +0 -1
  22. package/dist/components/markdown-format.d.ts +0 -1
  23. package/dist/content/adapter.d.ts +0 -1
  24. package/dist/content/compose.d.ts +1 -2
  25. package/dist/content/compose.js +2 -3
  26. package/dist/content/concepts.d.ts +7 -1
  27. package/dist/content/concepts.js +49 -1
  28. package/dist/content/frontmatter.d.ts +0 -1
  29. package/dist/content/identity.d.ts +23 -0
  30. package/dist/content/identity.js +43 -0
  31. package/dist/content/ids.d.ts +0 -1
  32. package/dist/content/links.d.ts +0 -1
  33. package/dist/content/manifest.d.ts +3 -2
  34. package/dist/content/manifest.js +6 -26
  35. package/dist/content/permalink.d.ts +0 -1
  36. package/dist/content/schema.d.ts +0 -1
  37. package/dist/content/types.d.ts +0 -1
  38. package/dist/content/validate.d.ts +0 -1
  39. package/dist/delivery/CairnHead.svelte.d.ts +0 -1
  40. package/dist/delivery/content-index.d.ts +0 -1
  41. package/dist/delivery/content-index.js +8 -25
  42. package/dist/delivery/data.d.ts +0 -1
  43. package/dist/delivery/excerpt.d.ts +0 -1
  44. package/dist/delivery/feeds.d.ts +0 -1
  45. package/dist/delivery/head.d.ts +0 -1
  46. package/dist/delivery/index.d.ts +0 -1
  47. package/dist/delivery/json-ld.d.ts +0 -1
  48. package/dist/delivery/manifest.d.ts +0 -1
  49. package/dist/delivery/paginate.d.ts +0 -1
  50. package/dist/delivery/responses.d.ts +0 -1
  51. package/dist/delivery/robots.d.ts +0 -1
  52. package/dist/delivery/seo-fields.d.ts +0 -1
  53. package/dist/delivery/seo.d.ts +0 -1
  54. package/dist/delivery/site-descriptors.d.ts +0 -1
  55. package/dist/delivery/site-descriptors.js +5 -6
  56. package/dist/delivery/site-index.d.ts +0 -1
  57. package/dist/delivery/site-indexes.d.ts +0 -1
  58. package/dist/delivery/sitemap.d.ts +0 -1
  59. package/dist/email.d.ts +0 -1
  60. package/dist/env.d.ts +0 -1
  61. package/dist/github/credentials.d.ts +0 -1
  62. package/dist/github/repo.d.ts +0 -1
  63. package/dist/github/signing.d.ts +0 -1
  64. package/dist/github/types.d.ts +0 -1
  65. package/dist/index.d.ts +1 -28
  66. package/dist/index.js +1 -23
  67. package/dist/nav/site-config.d.ts +0 -1
  68. package/dist/render/component-grammar.d.ts +0 -1
  69. package/dist/render/component-insert.d.ts +0 -1
  70. package/dist/render/component-reference.d.ts +0 -1
  71. package/dist/render/component-validate.d.ts +0 -1
  72. package/dist/render/glyph.d.ts +0 -1
  73. package/dist/render/index.d.ts +0 -1
  74. package/dist/render/pipeline.d.ts +0 -1
  75. package/dist/render/pipeline.js +5 -1
  76. package/dist/render/registry.d.ts +0 -1
  77. package/dist/render/rehype-dispatch.d.ts +0 -1
  78. package/dist/render/remark-directives.d.ts +0 -1
  79. package/dist/render/resolve-links.d.ts +0 -1
  80. package/dist/render/sanitize-schema.d.ts +14 -1
  81. package/dist/render/sanitize-schema.js +96 -0
  82. package/dist/sveltekit/auth-routes.d.ts +0 -1
  83. package/dist/sveltekit/content-routes.d.ts +0 -1
  84. package/dist/sveltekit/editors-routes.d.ts +0 -1
  85. package/dist/sveltekit/guard.d.ts +0 -1
  86. package/dist/sveltekit/health.d.ts +0 -1
  87. package/dist/sveltekit/index.d.ts +1 -3
  88. package/dist/sveltekit/index.js +0 -1
  89. package/dist/sveltekit/nav-routes.d.ts +0 -1
  90. package/dist/sveltekit/public-routes.d.ts +0 -1
  91. package/dist/sveltekit/types.d.ts +0 -1
  92. package/dist/vite/bin.d.ts +0 -1
  93. package/dist/vite/index.d.ts +0 -1
  94. package/package.json +2 -1
  95. package/src/lib/content/compose.ts +3 -3
  96. package/src/lib/content/concepts.ts +61 -1
  97. package/src/lib/content/identity.ts +60 -0
  98. package/src/lib/content/manifest.ts +6 -27
  99. package/src/lib/delivery/content-index.ts +8 -27
  100. package/src/lib/delivery/site-descriptors.ts +5 -6
  101. package/src/lib/index.ts +1 -56
  102. package/src/lib/render/pipeline.ts +4 -1
  103. package/src/lib/render/sanitize-schema.ts +97 -0
  104. package/src/lib/sveltekit/index.ts +2 -8
  105. package/dist/auth/crypto.d.ts.map +0 -1
  106. package/dist/auth/store.d.ts.map +0 -1
  107. package/dist/auth/types.d.ts.map +0 -1
  108. package/dist/components/AdminLayout.svelte.d.ts.map +0 -1
  109. package/dist/components/ComponentForm.svelte.d.ts.map +0 -1
  110. package/dist/components/ComponentInsertDialog.svelte.d.ts.map +0 -1
  111. package/dist/components/ConceptList.svelte.d.ts.map +0 -1
  112. package/dist/components/ConfirmPage.svelte.d.ts.map +0 -1
  113. package/dist/components/DeleteDialog.svelte.d.ts.map +0 -1
  114. package/dist/components/EditPage.svelte.d.ts.map +0 -1
  115. package/dist/components/EditorToolbar.svelte.d.ts.map +0 -1
  116. package/dist/components/IconPicker.svelte.d.ts.map +0 -1
  117. package/dist/components/LinkPicker.svelte.d.ts.map +0 -1
  118. package/dist/components/LoginPage.svelte.d.ts.map +0 -1
  119. package/dist/components/ManageEditors.svelte.d.ts.map +0 -1
  120. package/dist/components/MarkdownEditor.svelte.d.ts.map +0 -1
  121. package/dist/components/NavTree.svelte.d.ts.map +0 -1
  122. package/dist/components/RenameDialog.svelte.d.ts.map +0 -1
  123. package/dist/components/index.d.ts.map +0 -1
  124. package/dist/components/link-completion.d.ts.map +0 -1
  125. package/dist/components/markdown-format.d.ts.map +0 -1
  126. package/dist/content/adapter.d.ts.map +0 -1
  127. package/dist/content/compose.d.ts.map +0 -1
  128. package/dist/content/concepts.d.ts.map +0 -1
  129. package/dist/content/frontmatter.d.ts.map +0 -1
  130. package/dist/content/ids.d.ts.map +0 -1
  131. package/dist/content/links.d.ts.map +0 -1
  132. package/dist/content/manifest.d.ts.map +0 -1
  133. package/dist/content/permalink.d.ts.map +0 -1
  134. package/dist/content/schema.d.ts.map +0 -1
  135. package/dist/content/types.d.ts.map +0 -1
  136. package/dist/content/validate.d.ts.map +0 -1
  137. package/dist/delivery/CairnHead.svelte.d.ts.map +0 -1
  138. package/dist/delivery/content-index.d.ts.map +0 -1
  139. package/dist/delivery/data.d.ts.map +0 -1
  140. package/dist/delivery/excerpt.d.ts.map +0 -1
  141. package/dist/delivery/feeds.d.ts.map +0 -1
  142. package/dist/delivery/head.d.ts.map +0 -1
  143. package/dist/delivery/index.d.ts.map +0 -1
  144. package/dist/delivery/json-ld.d.ts.map +0 -1
  145. package/dist/delivery/manifest.d.ts.map +0 -1
  146. package/dist/delivery/paginate.d.ts.map +0 -1
  147. package/dist/delivery/responses.d.ts.map +0 -1
  148. package/dist/delivery/robots.d.ts.map +0 -1
  149. package/dist/delivery/seo-fields.d.ts.map +0 -1
  150. package/dist/delivery/seo.d.ts.map +0 -1
  151. package/dist/delivery/site-descriptors.d.ts.map +0 -1
  152. package/dist/delivery/site-index.d.ts.map +0 -1
  153. package/dist/delivery/site-indexes.d.ts.map +0 -1
  154. package/dist/delivery/sitemap.d.ts.map +0 -1
  155. package/dist/email.d.ts.map +0 -1
  156. package/dist/env.d.ts.map +0 -1
  157. package/dist/github/credentials.d.ts.map +0 -1
  158. package/dist/github/repo.d.ts.map +0 -1
  159. package/dist/github/signing.d.ts.map +0 -1
  160. package/dist/github/types.d.ts.map +0 -1
  161. package/dist/index.d.ts.map +0 -1
  162. package/dist/nav/site-config.d.ts.map +0 -1
  163. package/dist/render/component-grammar.d.ts.map +0 -1
  164. package/dist/render/component-insert.d.ts.map +0 -1
  165. package/dist/render/component-reference.d.ts.map +0 -1
  166. package/dist/render/component-validate.d.ts.map +0 -1
  167. package/dist/render/glyph.d.ts.map +0 -1
  168. package/dist/render/index.d.ts.map +0 -1
  169. package/dist/render/pipeline.d.ts.map +0 -1
  170. package/dist/render/registry.d.ts.map +0 -1
  171. package/dist/render/rehype-dispatch.d.ts.map +0 -1
  172. package/dist/render/remark-directives.d.ts.map +0 -1
  173. package/dist/render/resolve-links.d.ts.map +0 -1
  174. package/dist/render/sanitize-schema.d.ts.map +0 -1
  175. package/dist/sveltekit/auth-routes.d.ts.map +0 -1
  176. package/dist/sveltekit/content-routes.d.ts.map +0 -1
  177. package/dist/sveltekit/editors-routes.d.ts.map +0 -1
  178. package/dist/sveltekit/guard.d.ts.map +0 -1
  179. package/dist/sveltekit/health.d.ts.map +0 -1
  180. package/dist/sveltekit/index.d.ts.map +0 -1
  181. package/dist/sveltekit/nav-routes.d.ts.map +0 -1
  182. package/dist/sveltekit/public-routes.d.ts.map +0 -1
  183. package/dist/sveltekit/types.d.ts.map +0 -1
  184. package/dist/vite/bin.d.ts.map +0 -1
  185. package/dist/vite/index.d.ts.map +0 -1
@@ -10,4 +10,3 @@ export interface Page<T> {
10
10
  }
11
11
  /** Slice `items` into the 1-based `page` of size `perPage`, clamping the page into bounds. */
12
12
  export declare function paginate<T>(items: T[], page: number, perPage: number): Page<T>;
13
- //# sourceMappingURL=paginate.d.ts.map
@@ -11,4 +11,3 @@ export declare function robotsResponse(opts: {
11
11
  sitemapUrl: string;
12
12
  disallow?: string[];
13
13
  }): Response;
14
- //# sourceMappingURL=responses.d.ts.map
@@ -3,4 +3,3 @@ export declare function buildRobots(opts: {
3
3
  sitemapUrl: string;
4
4
  disallow?: string[];
5
5
  }): string;
6
- //# sourceMappingURL=robots.d.ts.map
@@ -19,4 +19,3 @@ export declare function readSeoFields(frontmatter: Record<string, unknown>): Seo
19
19
  * bare path also anchors to the origin root; against a sub-path origin it would resolve relative to
20
20
  * that path, per the WHATWG URL rules. */
21
21
  export declare function resolveImageUrl(image: string, origin: string): string | undefined;
22
- //# sourceMappingURL=seo-fields.d.ts.map
@@ -35,4 +35,3 @@ export interface SeoMeta {
35
35
  }
36
36
  /** Build the head data for a page. */
37
37
  export declare function buildSeoMeta(input: SeoInput): SeoMeta;
38
- //# sourceMappingURL=seo.d.ts.map
@@ -2,4 +2,3 @@ import type { CairnAdapter, ConceptDescriptor } from '../content/types.js';
2
2
  import type { SiteConfig } from '../nav/site-config.js';
3
3
  /** Per-concept descriptors for a site, from its adapter content and its parsed site config. */
4
4
  export declare function siteDescriptors(adapter: CairnAdapter, siteConfig: SiteConfig): ConceptDescriptor[];
5
- //# sourceMappingURL=site-descriptors.d.ts.map
@@ -1,9 +1,8 @@
1
- // cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept
2
- // descriptors the admin runtime uses; this wraps the two calls that derive them so the
3
- // pairing is not tribal knowledge. The YAML URL policy stays the single source of truth.
4
- import { normalizeConcepts } from '../content/concepts.js';
5
- import { urlPolicyFrom } from '../nav/site-config.js';
1
+ // cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept descriptors
2
+ // the admin runtime uses; this delegates to the shared resolveConcepts so the pairing is one path, not
3
+ // tribal knowledge. The YAML URL policy stays the single source of truth.
4
+ import { resolveConcepts } from '../content/concepts.js';
6
5
  /** Per-concept descriptors for a site, from its adapter content and its parsed site config. */
7
6
  export function siteDescriptors(adapter, siteConfig) {
8
- return normalizeConcepts(adapter.content, urlPolicyFrom(siteConfig));
7
+ return resolveConcepts(adapter.content, siteConfig);
9
8
  }
@@ -31,4 +31,3 @@ export interface SiteIndex {
31
31
  export declare function createSiteIndex(concepts: ConceptIndex[], opts?: {
32
32
  validate?: boolean;
33
33
  }): SiteIndex;
34
- //# sourceMappingURL=site-index.d.ts.map
@@ -23,4 +23,3 @@ export type SiteIndexes<A extends CairnAdapter> = {
23
23
  export declare function createSiteIndexes<const A extends CairnAdapter>(adapter: A, config: SiteConfig, globs: SiteGlobs<A>, opts?: {
24
24
  validate?: boolean;
25
25
  }): SiteIndexes<A>;
26
- //# sourceMappingURL=site-indexes.d.ts.map
@@ -5,4 +5,3 @@ export interface SitemapUrl {
5
5
  }
6
6
  /** Build a sitemap XML document from a list of URLs. */
7
7
  export declare function buildSitemap(urls: SitemapUrl[]): string;
8
- //# sourceMappingURL=sitemap.d.ts.map
package/dist/email.d.ts CHANGED
@@ -24,4 +24,3 @@ export declare function buildMagicLinkMessage(input: {
24
24
  }): MagicLinkMessage;
25
25
  /** The production send: Cloudflare Email Sending through the EMAIL binding. */
26
26
  export declare const cloudflareSend: SendMagicLink;
27
- //# sourceMappingURL=email.d.ts.map
package/dist/env.d.ts CHANGED
@@ -21,4 +21,3 @@ export declare function requireOrigin(env: {
21
21
  export declare function requireDb(env: {
22
22
  AUTH_DB?: D1Database;
23
23
  }): D1Database;
24
- //# sourceMappingURL=env.d.ts.map
@@ -9,4 +9,3 @@ export interface GithubKeyEnv {
9
9
  * installation) and the Worker's private-key secret. Throws when the secret is unset.
10
10
  */
11
11
  export declare function appCredentials(backend: Pick<BackendConfig, 'appId' | 'installationId'>, env: GithubKeyEnv): AppCredentials;
12
- //# sourceMappingURL=credentials.d.ts.map
@@ -67,4 +67,3 @@ export declare function commitFiles(repo: RepoRef, changes: FileChange[], opts:
67
67
  author: CommitAuthor;
68
68
  }, token: string): Promise<string>;
69
69
  export {};
70
- //# sourceMappingURL=repo.d.ts.map
@@ -26,4 +26,3 @@ export declare function signingSelfTest(appId: string, privateKeyB64: string): P
26
26
  ok: boolean;
27
27
  detail?: string;
28
28
  }>;
29
- //# sourceMappingURL=signing.d.ts.map
@@ -32,4 +32,3 @@ export declare class CommitConflictError extends Error {
32
32
  readonly path: string;
33
33
  constructor(path: string);
34
34
  }
35
- //# sourceMappingURL=types.d.ts.map
package/dist/index.d.ts CHANGED
@@ -27,38 +27,11 @@ export type { ReferenceOptions } from './render/component-reference.js';
27
27
  export { glyph } from './render/glyph.js';
28
28
  export type { IconSet } from './render/glyph.js';
29
29
  export { remarkDirectiveStamp } from './render/remark-directives.js';
30
- export { rehypeDispatch, isElement, strProp, iconSpan, cardShell, headRow, markFirstList, } from './render/rehype-dispatch.js';
30
+ export { rehypeDispatch, iconSpan, cardShell, headRow } from './render/rehype-dispatch.js';
31
31
  export type { MakeIcon } from './render/rehype-dispatch.js';
32
32
  export { createRenderer } from './render/pipeline.js';
33
33
  export type { RendererOptions } from './render/pipeline.js';
34
34
  export type { RepoRef, RepoFile, CommitAuthor, AppCredentials } from './github/types.js';
35
35
  export { CommitConflictError } from './github/types.js';
36
- export { appJwt, installationToken, signingSelfTest } from './github/signing.js';
37
- export { treeUrl, markdownFilesIn, listMarkdown, contentsUrl, readRaw, fileSha, commitFile, } from './github/repo.js';
38
- export { appCredentials } from './github/credentials.js';
39
- export type { GithubKeyEnv } from './github/credentials.js';
40
36
  export { parseSiteConfig, urlPolicyFrom, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
41
37
  export type { NavNode, SiteConfig } from './nav/site-config.js';
42
- export { permalink } from './content/permalink.js';
43
- export { createContentIndex, fromGlob } from './delivery/content-index.js';
44
- export type { RawFile, ContentSummary, ContentEntry, ContentIndex, ContentProblem, } from './delivery/content-index.js';
45
- export { createSiteIndex } from './delivery/site-index.js';
46
- export type { SiteIndex, ConceptIndex } from './delivery/site-index.js';
47
- export { createSiteIndexes } from './delivery/site-indexes.js';
48
- export type { SiteIndexes, SiteGlobs } from './delivery/site-indexes.js';
49
- export { deriveExcerpt, wordCount } from './delivery/excerpt.js';
50
- export { buildRssFeed, buildJsonFeed } from './delivery/feeds.js';
51
- export type { FeedChannel, FeedItem } from './delivery/feeds.js';
52
- export { buildSitemap } from './delivery/sitemap.js';
53
- export type { SitemapUrl } from './delivery/sitemap.js';
54
- export { buildRobots } from './delivery/robots.js';
55
- export { buildSeoMeta } from './delivery/seo.js';
56
- export type { SeoInput, SeoMeta } from './delivery/seo.js';
57
- export { readSeoFields, resolveImageUrl } from './delivery/seo-fields.js';
58
- export type { SeoFields } from './delivery/seo-fields.js';
59
- export { paginate } from './delivery/paginate.js';
60
- export type { Page } from './delivery/paginate.js';
61
- export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './delivery/responses.js';
62
- export { createPublicRoutes } from './sveltekit/public-routes.js';
63
- export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData } from './sveltekit/public-routes.js';
64
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -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
@@ -31,4 +31,3 @@ export declare function createRenderer(registry?: ComponentRegistry, options?: R
31
31
  resolve?: LinkResolve;
32
32
  }) => Promise<string>;
33
33
  };
34
- //# sourceMappingURL=pipeline.d.ts.map
@@ -8,7 +8,7 @@ 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';
@@ -33,6 +33,10 @@ export function createRenderer(registry = defineRegistry({ components: [] }), op
33
33
  ];
34
34
  if (rel !== false)
35
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);
36
40
  const processor = unified()
37
41
  .use(remarkParse)
38
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
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=bin.d.ts.map
@@ -30,4 +30,3 @@ export declare function cairnManifest(opts: CairnManifestOptions): Plugin;
30
30
  * resolution, and writes the serialized manifest. The cairn-manifest bin calls this; it is exported
31
31
  * so the write logic is testable apart from the CLI shell. */
32
32
  export declare function writeManifest(cwd?: string): Promise<void>;
33
- //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glw907/cairn-cms",
3
- "version": "0.26.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": [
@@ -28,6 +28,7 @@
28
28
  "scripts": {
29
29
  "package": "svelte-package && chmod +x dist/vite/bin.js",
30
30
  "check:package": "npm run package && publint --strict && attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm internal-resolution-error",
31
+ "check:reference": "npm run package && node scripts/reference-coverage.mjs",
31
32
  "prepare": "npm run package",
32
33
  "check": "svelte-check --tsconfig ./tsconfig.json",
33
34
  "test": "vitest run",
@@ -4,8 +4,8 @@
4
4
  // concepts, components, field types, and save hooks. Shaped now so the extension contract
5
5
  // is additive later.
6
6
  import type { AdminPanel, CairnAdapter, CairnExtension, CairnRuntime, ConceptConfig, FieldTypeDef } from './types.js';
7
- import { normalizeConcepts } from './concepts.js';
8
- import { urlPolicyFrom, type SiteConfig } from '../nav/site-config.js';
7
+ import { resolveConcepts } from './concepts.js';
8
+ import type { SiteConfig } from '../nav/site-config.js';
9
9
 
10
10
  /** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
11
11
  * always derived from one source and can never be silently dropped. `extensions` fold in after the
@@ -36,7 +36,7 @@ export function composeRuntime({ adapter, siteConfig, extensions = [] }: Compose
36
36
  }
37
37
  return {
38
38
  siteName: adapter.siteName,
39
- concepts: normalizeConcepts(content, urlPolicyFrom(siteConfig)),
39
+ concepts: resolveConcepts(content, siteConfig),
40
40
  backend: adapter.backend,
41
41
  sender: adapter.sender,
42
42
  render: adapter.render,
@@ -4,6 +4,7 @@
4
4
  // future Fragments concept attaches by adding one key under `content` and one routing
5
5
  // entry, with no reshape here.
6
6
  import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
7
+ import { urlPolicyFrom, type SiteConfig } from '../nav/site-config.js';
7
8
 
8
9
  /**
9
10
  * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
@@ -28,6 +29,43 @@ function defaultPermalink(id: string): string {
28
29
  return id === 'pages' ? '/:slug' : `/${id}/:slug`;
29
30
  }
30
31
 
32
+ /** Permalink tokens the resolver understands. */
33
+ const KNOWN_TOKENS = new Set(['slug', 'year', 'month', 'day']);
34
+ /** The date-bearing tokens; valid only for a dated concept. */
35
+ const DATE_TOKENS = new Set(['year', 'month', 'day']);
36
+ /** The valid date-prefix granularities. A runtime check, since the YAML is untyped. */
37
+ const DATE_PREFIXES = new Set<string>(['year', 'month', 'day']);
38
+
39
+ /**
40
+ * Validate one concept's URL policy at build, so a misconfigured permalink or datePrefix fails loudly
41
+ * here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
42
+ * use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
43
+ */
44
+ function validateUrlPolicy(id: string, policy: ConceptUrlPolicy, dated: boolean): void {
45
+ if (policy.permalink !== undefined) {
46
+ const pattern = policy.permalink;
47
+ if (!pattern.startsWith('/')) {
48
+ throw new Error(`cairn: concept "${id}" permalink "${pattern}" must start with "/"`);
49
+ }
50
+ for (const match of pattern.matchAll(/:(\w+)/g)) {
51
+ const token = match[1];
52
+ if (!KNOWN_TOKENS.has(token)) {
53
+ throw new Error(`cairn: concept "${id}" permalink "${pattern}" uses unknown token ":${token}"`);
54
+ }
55
+ if (DATE_TOKENS.has(token) && !dated) {
56
+ throw new Error(
57
+ `cairn: concept "${id}" is not dated, so permalink "${pattern}" cannot use the date token ":${token}"`,
58
+ );
59
+ }
60
+ }
61
+ }
62
+ if (policy.datePrefix !== undefined && !DATE_PREFIXES.has(policy.datePrefix)) {
63
+ throw new Error(
64
+ `cairn: concept "${id}" datePrefix "${policy.datePrefix}" must be one of year, month, day`,
65
+ );
66
+ }
67
+ }
68
+
31
69
  /**
32
70
  * Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
33
71
  * (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
@@ -41,6 +79,14 @@ export function normalizeConcepts(
41
79
  routing: Readonly<Record<string, RoutingRule>> = CONCEPT_ROUTING,
42
80
  ): ConceptDescriptor[] {
43
81
  const descriptors: ConceptDescriptor[] = [];
82
+ const declaredConcepts = new Set(
83
+ Object.keys(content).filter((key) => content[key] !== undefined),
84
+ );
85
+ for (const key of Object.keys(urlPolicy)) {
86
+ if (!declaredConcepts.has(key)) {
87
+ throw new Error(`cairn: URL policy names concept "${key}", which is not declared under content`);
88
+ }
89
+ }
44
90
  for (const [id, config] of Object.entries(content)) {
45
91
  if (!config) continue;
46
92
  const summaryFields = config.summaryFields ?? [];
@@ -51,12 +97,14 @@ export function normalizeConcepts(
51
97
  `cairn: concept "${id}" summaryFields key "${undeclared}" is not a declared field`,
52
98
  );
53
99
  }
100
+ const conceptRouting = routing[id] ?? DEFAULT_ROUTING;
54
101
  const policy = urlPolicy[id] ?? {};
102
+ validateUrlPolicy(id, policy, conceptRouting.dated);
55
103
  descriptors.push({
56
104
  id,
57
105
  label: config.label ?? defaultLabel(id),
58
106
  dir: config.dir,
59
- routing: routing[id] ?? DEFAULT_ROUTING,
107
+ routing: conceptRouting,
60
108
  permalink: policy.permalink ?? defaultPermalink(id),
61
109
  datePrefix: policy.datePrefix ?? 'day',
62
110
  fields: config.schema.fields,
@@ -67,6 +115,18 @@ export function normalizeConcepts(
67
115
  return descriptors;
68
116
  }
69
117
 
118
+ /**
119
+ * Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
120
+ * (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
121
+ * policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
122
+ */
123
+ export function resolveConcepts(
124
+ content: Record<string, ConceptConfig | undefined>,
125
+ siteConfig: SiteConfig,
126
+ ): ConceptDescriptor[] {
127
+ return normalizeConcepts(content, urlPolicyFrom(siteConfig));
128
+ }
129
+
70
130
  /** Look up a normalized concept by id, or undefined when the site does not enable it. */
71
131
  export function findConcept(
72
132
  concepts: ConceptDescriptor[],