@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,55 @@
2
2
 
3
3
  All notable changes to this project are recorded here, most recent first.
4
4
 
5
+ ## 0.29.0
6
+
7
+ Consolidated the URL-identity model. A content entry's id, slug, date, and permalink are now derived in
8
+ one place (`entryIdentity`), so the content index and the manifest cannot drift on an entry's URL, and a
9
+ site's concept descriptors are resolved through one path shared by the admin runtime and the delivery
10
+ layer. No public surface changed.
11
+
12
+ The YAML URL policy is now validated at build. A permalink pattern must be root-relative and use only the
13
+ tokens `:slug`, `:year`, `:month`, and `:day`, a date token is valid only on a dated concept, a
14
+ `datePrefix` must be `year`, `month`, or `day`, and a policy keyed to an undeclared concept fails the
15
+ build.
16
+
17
+ Behavior note: a site whose `content:` URL policy was malformed and silently defaulted will now fail the
18
+ build with a named error. A valid policy is unaffected.
19
+
20
+ ## 0.28.0
21
+
22
+ ### Security
23
+ Closed the render attribute-sink residual by construction. A new post-dispatch guard runs last in
24
+ `createRenderer` and neutralizes the sinks a component `build()` could route a raw author attribute
25
+ value into, including the unsafe URL schemes `javascript:`, `data:`, and `vbscript:` in `href`,
26
+ `src`, `srcset`, `xlink:href`, `poster`, `formaction`, `action`, `object`'s `data`, and
27
+ `background`, the inline `on*` event handlers, and inline `style`, which is stripped wholesale. Safe
28
+ schemes, relative URLs, anchors, and the `cairn:` token are preserved. The guard is gated by the
29
+ existing `unsafeDisableSanitize` switch.
30
+
31
+ Behavior note: a site whose component `build()` emits a non-standard URL scheme, an `on*` handler,
32
+ or inline `style` will see that output neutralized. Route dynamic styling through a class or an
33
+ inert `data-*` attribute instead.
34
+
35
+ ## 0.27.0
36
+
37
+ ### Changed (breaking)
38
+ Narrowed the public export surface so each symbol has one canonical home. The `.` root and
39
+ `/sveltekit` no longer re-export another subpath's symbols, and the internal GitHub, signing, and
40
+ hast helpers left the public API. No symbol changed behavior; only where it exports from.
41
+
42
+ - Consumers must: import the delivery read helpers (`createContentIndex`, `createSiteIndexes`, the
43
+ feed, sitemap, robots, SEO, and pagination builders, `permalink`) from `@glw907/cairn-cms/delivery/data`
44
+ instead of the `.` root.
45
+ - Consumers must: import the public route loaders and the `*Response` helpers (`createPublicRoutes`,
46
+ `rssResponse`, `jsonFeedResponse`, `sitemapResponse`, `robotsResponse`) and the public route types
47
+ (`PublicRoutesDeps`, the public `ListData`, `TagData`, `TagIndexData`, `EntryData`) from
48
+ `@glw907/cairn-cms/delivery` instead of the `.` root or `/sveltekit`.
49
+ - Consumers must: stop importing the internal helpers that left the public API (`appJwt`,
50
+ `installationToken`, `signingSelfTest`, `appCredentials`, `treeUrl`, `contentsUrl`, `readRaw`,
51
+ `fileSha`, `listMarkdown`, `markdownFilesIn`, `commitFile`, `isElement`, `strProp`, `markFirstList`);
52
+ the engine wires GitHub token minting and the render pipeline internally, so no consumer needs them.
53
+
5
54
  ## 0.26.0
6
55
 
7
56
  ### Added
@@ -16,4 +16,3 @@ export declare function generateToken(): string;
16
16
  export declare function generateSessionId(): string;
17
17
  /** The lowercase hex SHA-256 of a token, for storage and lookup. */
18
18
  export declare function hashToken(token: string): Promise<string>;
19
- //# sourceMappingURL=crypto.d.ts.map
@@ -40,4 +40,3 @@ export declare function setEditorRole(db: D1Database, email: string, role: Role)
40
40
  * `removeOwnerIfNotLast`). Returns false (and writes nothing) when this is the last owner.
41
41
  */
42
42
  export declare function demoteOwnerIfNotLast(db: D1Database, email: string): Promise<boolean>;
43
- //# sourceMappingURL=store.d.ts.map
@@ -22,4 +22,3 @@ export interface AuthEnv {
22
22
  }): Promise<void>;
23
23
  };
24
24
  }
25
- //# sourceMappingURL=types.d.ts.map
@@ -16,4 +16,3 @@ interface Props {
16
16
  declare const AdminLayout: import("svelte").Component<Props, {}, "">;
17
17
  type AdminLayout = ReturnType<typeof AdminLayout>;
18
18
  export default AdminLayout;
19
- //# sourceMappingURL=AdminLayout.svelte.d.ts.map
@@ -17,4 +17,3 @@ interface Props {
17
17
  declare const ComponentForm: import("svelte").Component<Props, {}, "">;
18
18
  type ComponentForm = ReturnType<typeof ComponentForm>;
19
19
  export default ComponentForm;
20
- //# sourceMappingURL=ComponentForm.svelte.d.ts.map
@@ -17,4 +17,3 @@ interface Props {
17
17
  declare const ComponentInsertDialog: import("svelte").Component<Props, {}, "">;
18
18
  type ComponentInsertDialog = ReturnType<typeof ComponentInsertDialog>;
19
19
  export default ComponentInsertDialog;
20
- //# sourceMappingURL=ComponentInsertDialog.svelte.d.ts.map
@@ -10,4 +10,3 @@ interface Props {
10
10
  declare const ConceptList: import("svelte").Component<Props, {}, "">;
11
11
  type ConceptList = ReturnType<typeof ConceptList>;
12
12
  export default ConceptList;
13
- //# sourceMappingURL=ConceptList.svelte.d.ts.map
@@ -14,4 +14,3 @@ interface Props {
14
14
  declare const ConfirmPage: import("svelte").Component<Props, {}, "">;
15
15
  type ConfirmPage = ReturnType<typeof ConfirmPage>;
16
16
  export default ConfirmPage;
17
- //# sourceMappingURL=ConfirmPage.svelte.d.ts.map
@@ -18,4 +18,3 @@ interface Props {
18
18
  declare const DeleteDialog: import("svelte").Component<Props, {}, "">;
19
19
  type DeleteDialog = ReturnType<typeof DeleteDialog>;
20
20
  export default DeleteDialog;
21
- //# sourceMappingURL=DeleteDialog.svelte.d.ts.map
@@ -33,4 +33,3 @@ interface Props {
33
33
  declare const EditPage: import("svelte").Component<Props, {}, "">;
34
34
  type EditPage = ReturnType<typeof EditPage>;
35
35
  export default EditPage;
36
- //# sourceMappingURL=EditPage.svelte.d.ts.map
@@ -12,4 +12,3 @@ interface Props {
12
12
  declare const EditorToolbar: import("svelte").Component<Props, {}, "">;
13
13
  type EditorToolbar = ReturnType<typeof EditorToolbar>;
14
14
  export default EditorToolbar;
15
- //# sourceMappingURL=EditorToolbar.svelte.d.ts.map
@@ -21,4 +21,3 @@ interface Props {
21
21
  declare const IconPicker: import("svelte").Component<Props, {}, "">;
22
22
  type IconPicker = ReturnType<typeof IconPicker>;
23
23
  export default IconPicker;
24
- //# sourceMappingURL=IconPicker.svelte.d.ts.map
@@ -15,4 +15,3 @@ interface Props {
15
15
  declare const LinkPicker: import("svelte").Component<Props, {}, "">;
16
16
  type LinkPicker = ReturnType<typeof LinkPicker>;
17
17
  export default LinkPicker;
18
- //# sourceMappingURL=LinkPicker.svelte.d.ts.map
@@ -18,4 +18,3 @@ interface Props {
18
18
  declare const LoginPage: import("svelte").Component<Props, {}, "">;
19
19
  type LoginPage = ReturnType<typeof LoginPage>;
20
20
  export default LoginPage;
21
- //# sourceMappingURL=LoginPage.svelte.d.ts.map
@@ -20,4 +20,3 @@ interface Props {
20
20
  declare const ManageEditors: import("svelte").Component<Props, {}, "">;
21
21
  type ManageEditors = ReturnType<typeof ManageEditors>;
22
22
  export default ManageEditors;
23
- //# sourceMappingURL=ManageEditors.svelte.d.ts.map
@@ -21,4 +21,3 @@ interface Props {
21
21
  declare const MarkdownEditor: import("svelte").Component<Props, {}, "value">;
22
22
  type MarkdownEditor = ReturnType<typeof MarkdownEditor>;
23
23
  export default MarkdownEditor;
24
- //# sourceMappingURL=MarkdownEditor.svelte.d.ts.map
@@ -14,4 +14,3 @@ interface Props {
14
14
  declare const NavTree: import("svelte").Component<Props, {}, "">;
15
15
  type NavTree = ReturnType<typeof NavTree>;
16
16
  export default NavTree;
17
- //# sourceMappingURL=NavTree.svelte.d.ts.map
@@ -17,4 +17,3 @@ interface Props {
17
17
  declare const RenameDialog: import("svelte").Component<Props, {}, "">;
18
18
  type RenameDialog = ReturnType<typeof RenameDialog>;
19
19
  export default RenameDialog;
20
- //# sourceMappingURL=RenameDialog.svelte.d.ts.map
@@ -12,4 +12,3 @@ export { default as NavTree } from './NavTree.svelte';
12
12
  export { default as LinkPicker } from './LinkPicker.svelte';
13
13
  export { default as DeleteDialog } from './DeleteDialog.svelte';
14
14
  export { default as RenameDialog } from './RenameDialog.svelte';
15
- //# sourceMappingURL=index.d.ts.map
@@ -13,4 +13,3 @@ export declare function linkCompletions(targets: LinkTarget[], query: string): C
13
13
  * whole `[[query` with the chosen link, and sets filter:false because linkCompletions already
14
14
  * filtered by the query (CodeMirror would otherwise re-filter against the literal `[[query`). */
15
15
  export declare function cairnLinkCompletionSource(targets: LinkTarget[]): CompletionSource;
16
- //# sourceMappingURL=link-completion.d.ts.map
@@ -30,4 +30,3 @@ export declare function unwrapCairnLink(doc: string, href: string): string;
30
30
  * last to first, replacing only the `](oldHref` run so the label and title stay exact.
31
31
  */
32
32
  export declare function rewriteCairnLink(doc: string, oldHref: string, newHref: string): string;
33
- //# sourceMappingURL=markdown-format.d.ts.map
@@ -1,4 +1,3 @@
1
1
  import type { CairnAdapter } from './types.js';
2
2
  /** Declare a site's adapter while preserving each concept's concrete schema type for typed reads. */
3
3
  export declare function defineAdapter<const A extends CairnAdapter>(adapter: A): A;
4
- //# sourceMappingURL=adapter.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
2
- import { type SiteConfig } from '../nav/site-config.js';
2
+ import type { SiteConfig } from '../nav/site-config.js';
3
3
  /** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
4
4
  * always derived from one source and can never be silently dropped. `extensions` fold in after the
5
5
  * adapter's concepts. */
@@ -15,4 +15,3 @@ export interface ComposeInput {
15
15
  * (seam 4) passes through untouched.
16
16
  */
17
17
  export declare function composeRuntime({ adapter, siteConfig, extensions }: ComposeInput): CairnRuntime;
18
- //# sourceMappingURL=compose.d.ts.map
@@ -1,5 +1,4 @@
1
- import { normalizeConcepts } from './concepts.js';
2
- import { urlPolicyFrom } from '../nav/site-config.js';
1
+ import { resolveConcepts } from './concepts.js';
3
2
  /**
4
3
  * Fold an adapter and any extensions into the composed runtime (seam 2). The per-concept URL policy
5
4
  * is derived from the site config, the same source the delivery path uses, so the runtime and
@@ -24,7 +23,7 @@ export function composeRuntime({ adapter, siteConfig, extensions = [] }) {
24
23
  }
25
24
  return {
26
25
  siteName: adapter.siteName,
27
- concepts: normalizeConcepts(content, urlPolicyFrom(siteConfig)),
26
+ concepts: resolveConcepts(content, siteConfig),
28
27
  backend: adapter.backend,
29
28
  sender: adapter.sender,
30
29
  render: adapter.render,
@@ -1,4 +1,5 @@
1
1
  import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
2
+ import { type SiteConfig } from '../nav/site-config.js';
2
3
  /**
3
4
  * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
4
5
  * pages are plain navigable structure. Not in adapter config. A future Fragments adds one
@@ -13,6 +14,11 @@ export declare const CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>>;
13
14
  * a new concept attaches additively; production passes the default `CONCEPT_ROUTING`.
14
15
  */
15
16
  export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, urlPolicy?: Record<string, ConceptUrlPolicy | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
17
+ /**
18
+ * Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
19
+ * (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
20
+ * policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
21
+ */
22
+ export declare function resolveConcepts(content: Record<string, ConceptConfig | undefined>, siteConfig: SiteConfig): ConceptDescriptor[];
16
23
  /** Look up a normalized concept by id, or undefined when the site does not enable it. */
17
24
  export declare function findConcept(concepts: ConceptDescriptor[], id: string): ConceptDescriptor | undefined;
18
- //# sourceMappingURL=concepts.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { urlPolicyFrom } from '../nav/site-config.js';
1
2
  /**
2
3
  * Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
3
4
  * pages are plain navigable structure. Not in adapter config. A future Fragments adds one
@@ -17,6 +18,37 @@ function defaultLabel(id) {
17
18
  function defaultPermalink(id) {
18
19
  return id === 'pages' ? '/:slug' : `/${id}/:slug`;
19
20
  }
21
+ /** Permalink tokens the resolver understands. */
22
+ const KNOWN_TOKENS = new Set(['slug', 'year', 'month', 'day']);
23
+ /** The date-bearing tokens; valid only for a dated concept. */
24
+ const DATE_TOKENS = new Set(['year', 'month', 'day']);
25
+ /** The valid date-prefix granularities. A runtime check, since the YAML is untyped. */
26
+ const DATE_PREFIXES = new Set(['year', 'month', 'day']);
27
+ /**
28
+ * Validate one concept's URL policy at build, so a misconfigured permalink or datePrefix fails loudly
29
+ * here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
30
+ * use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
31
+ */
32
+ function validateUrlPolicy(id, policy, dated) {
33
+ if (policy.permalink !== undefined) {
34
+ const pattern = policy.permalink;
35
+ if (!pattern.startsWith('/')) {
36
+ throw new Error(`cairn: concept "${id}" permalink "${pattern}" must start with "/"`);
37
+ }
38
+ for (const match of pattern.matchAll(/:(\w+)/g)) {
39
+ const token = match[1];
40
+ if (!KNOWN_TOKENS.has(token)) {
41
+ throw new Error(`cairn: concept "${id}" permalink "${pattern}" uses unknown token ":${token}"`);
42
+ }
43
+ if (DATE_TOKENS.has(token) && !dated) {
44
+ throw new Error(`cairn: concept "${id}" is not dated, so permalink "${pattern}" cannot use the date token ":${token}"`);
45
+ }
46
+ }
47
+ }
48
+ if (policy.datePrefix !== undefined && !DATE_PREFIXES.has(policy.datePrefix)) {
49
+ throw new Error(`cairn: concept "${id}" datePrefix "${policy.datePrefix}" must be one of year, month, day`);
50
+ }
51
+ }
20
52
  /**
21
53
  * Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
22
54
  * (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
@@ -26,6 +58,12 @@ function defaultPermalink(id) {
26
58
  */
27
59
  export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROUTING) {
28
60
  const descriptors = [];
61
+ const declaredConcepts = new Set(Object.keys(content).filter((key) => content[key] !== undefined));
62
+ for (const key of Object.keys(urlPolicy)) {
63
+ if (!declaredConcepts.has(key)) {
64
+ throw new Error(`cairn: URL policy names concept "${key}", which is not declared under content`);
65
+ }
66
+ }
29
67
  for (const [id, config] of Object.entries(content)) {
30
68
  if (!config)
31
69
  continue;
@@ -35,12 +73,14 @@ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROU
35
73
  if (undeclared !== undefined) {
36
74
  throw new Error(`cairn: concept "${id}" summaryFields key "${undeclared}" is not a declared field`);
37
75
  }
76
+ const conceptRouting = routing[id] ?? DEFAULT_ROUTING;
38
77
  const policy = urlPolicy[id] ?? {};
78
+ validateUrlPolicy(id, policy, conceptRouting.dated);
39
79
  descriptors.push({
40
80
  id,
41
81
  label: config.label ?? defaultLabel(id),
42
82
  dir: config.dir,
43
- routing: routing[id] ?? DEFAULT_ROUTING,
83
+ routing: conceptRouting,
44
84
  permalink: policy.permalink ?? defaultPermalink(id),
45
85
  datePrefix: policy.datePrefix ?? 'day',
46
86
  fields: config.schema.fields,
@@ -50,6 +90,14 @@ export function normalizeConcepts(content, urlPolicy = {}, routing = CONCEPT_ROU
50
90
  }
51
91
  return descriptors;
52
92
  }
93
+ /**
94
+ * Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
95
+ * (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
96
+ * policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
97
+ */
98
+ export function resolveConcepts(content, siteConfig) {
99
+ return normalizeConcepts(content, urlPolicyFrom(siteConfig));
100
+ }
53
101
  /** Look up a normalized concept by id, or undefined when the site does not enable it. */
54
102
  export function findConcept(concepts, id) {
55
103
  return concepts.find((concept) => concept.id === id);
@@ -23,4 +23,3 @@ export declare function parseMarkdown(source: string): {
23
23
  frontmatter: Record<string, unknown>;
24
24
  body: string;
25
25
  };
26
- //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1,23 @@
1
+ import type { ConceptDescriptor } from './types.js';
2
+ /** A content entry's resolved URL identity. */
3
+ export interface EntryIdentity {
4
+ id: string;
5
+ slug: string;
6
+ date?: string;
7
+ permalink: string;
8
+ }
9
+ /** A present, non-empty string, else undefined. The read-model string coercion. */
10
+ export declare function asString(value: unknown): string | undefined;
11
+ /** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
12
+ export declare function asDate(value: unknown): string | undefined;
13
+ /** Tags as an array, empty when the file declares none. */
14
+ export declare function asTags(value: unknown): string[];
15
+ /** A content entry's id: its filename stem (the date prefix is part of a dated id). */
16
+ export declare function entryId(path: string): string;
17
+ /**
18
+ * Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
19
+ * frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
20
+ * an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
21
+ * once and passes the frontmatter, so there is no second parse here.
22
+ */
23
+ export declare function entryIdentity(descriptor: ConceptDescriptor, path: string, frontmatter: Record<string, unknown>): EntryIdentity;
@@ -0,0 +1,43 @@
1
+ // cairn-cms: a content entry's URL identity in one place (engine-hardening pass 3). The id, the
2
+ // slug, the date, and the permalink are computed here, so the content index and the manifest cannot
3
+ // drift on what an entry's URL is. A cairn: link resolves through the manifest in the admin preview
4
+ // and through the content index in the public build, so the two must agree by construction.
5
+ import { idFromFilename, slugFromId } from './ids.js';
6
+ import { permalink } from './permalink.js';
7
+ /** The basename of a glob path: the segment after the last slash, or the whole path. */
8
+ function basename(path) {
9
+ const slash = path.lastIndexOf('/');
10
+ return slash >= 0 ? path.slice(slash + 1) : path;
11
+ }
12
+ /** A present, non-empty string, else undefined. The read-model string coercion. */
13
+ export function asString(value) {
14
+ return typeof value === 'string' && value.trim() ? value : undefined;
15
+ }
16
+ /** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
17
+ export function asDate(value) {
18
+ if (value instanceof Date)
19
+ return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
20
+ if (typeof value === 'string')
21
+ return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
22
+ return undefined;
23
+ }
24
+ /** Tags as an array, empty when the file declares none. */
25
+ export function asTags(value) {
26
+ return Array.isArray(value) ? value.map(String) : [];
27
+ }
28
+ /** A content entry's id: its filename stem (the date prefix is part of a dated id). */
29
+ export function entryId(path) {
30
+ return idFromFilename(basename(path));
31
+ }
32
+ /**
33
+ * Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
34
+ * frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
35
+ * an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
36
+ * once and passes the frontmatter, so there is no second parse here.
37
+ */
38
+ export function entryIdentity(descriptor, path, frontmatter) {
39
+ const id = entryId(path);
40
+ const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
41
+ const date = asDate(frontmatter.date);
42
+ return { id, slug, date, permalink: permalink(descriptor, { id, slug, date }) };
43
+ }
@@ -35,4 +35,3 @@ export declare function composeDatedId(date: string, slug: string, datePrefix: D
35
35
  * newSlug. The caller validates newSlug with isValidId first.
36
36
  */
37
37
  export declare function renameId(oldId: string, newSlug: string, datePrefix: DatePrefix | null): string;
38
- //# sourceMappingURL=ids.d.ts.map
@@ -18,4 +18,3 @@ export declare function escapeLinkText(text: string): string;
18
18
  /** The cairn links a markdown body points at, in first-occurrence order, deduped by concept/id.
19
19
  * Parses the body as mdast, so a token inside a code span or fence is never matched. */
20
20
  export declare function extractCairnLinks(body: string): CairnRef[];
21
- //# sourceMappingURL=links.d.ts.map
@@ -24,7 +24,9 @@ export interface LinkTarget {
24
24
  date?: string;
25
25
  draft: boolean;
26
26
  }
27
- /** Build one manifest entry from a content file. Drafts are included and flagged. */
27
+ /** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
28
+ * permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
29
+ * one URL whether the admin preview reads the manifest or the public build reads the content index. */
28
30
  export declare function manifestEntryFromFile(descriptor: ConceptDescriptor, file: {
29
31
  path: string;
30
32
  raw: string;
@@ -83,4 +85,3 @@ export declare function manifestLinkResolver(targets: {
83
85
  id: string;
84
86
  permalink: string;
85
87
  }[]): LinkResolve;
86
- //# sourceMappingURL=manifest.d.ts.map
@@ -3,41 +3,21 @@
3
3
  // code reads the content graph without an N+1 GitHub crawl. The build regenerates and verifies
4
4
  // it; the save path patches one entry and commits it with the content in one commit. Each entry
5
5
  // carries its identity and its outbound cairn: edges, so the manifest is the link graph.
6
- import { idFromFilename, slugFromId } from './ids.js';
7
6
  import { parseMarkdown } from './frontmatter.js';
8
- import { permalink } from './permalink.js';
7
+ import { entryIdentity, asString } from './identity.js';
9
8
  import { extractCairnLinks } from './links.js';
10
- function basename(path) {
11
- const slash = path.lastIndexOf('/');
12
- return slash >= 0 ? path.slice(slash + 1) : path;
13
- }
14
- /** Mirror content-index's frontmatter coercion: a present non-empty string, else undefined. */
15
- function asString(value) {
16
- return typeof value === 'string' && value.trim() ? value : undefined;
17
- }
18
- /** Mirror content-index's date coercion: an unquoted YAML date is a JS Date, a string is sliced. */
19
- function asDate(value) {
20
- if (value instanceof Date)
21
- return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
22
- if (typeof value === 'string')
23
- return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
24
- return undefined;
25
- }
26
- /** Build one manifest entry from a content file. Drafts are included and flagged. */
9
+ /** Build one manifest entry from a content file. Drafts are included and flagged. The id, date, and
10
+ * permalink come from entryIdentity, the same source content-index uses, so a cairn: link resolves to
11
+ * one URL whether the admin preview reads the manifest or the public build reads the content index. */
27
12
  export function manifestEntryFromFile(descriptor, file) {
28
- const id = idFromFilename(basename(file.path));
29
- // Use the same slug rule content-index uses, so the manifest's permalink for an entry always
30
- // equals content-index's permalink for it. A cairn link must resolve to one URL whether the
31
- // admin preview reads the manifest or the public build reads the content index.
32
- const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
33
13
  const { frontmatter, body } = parseMarkdown(file.raw);
34
- const date = asDate(frontmatter.date);
14
+ const { id, date, permalink } = entryIdentity(descriptor, file.path, frontmatter);
35
15
  return {
36
16
  id,
37
17
  concept: descriptor.id,
38
18
  title: asString(frontmatter.title) ?? id,
39
19
  date,
40
- permalink: permalink(descriptor, { id, slug, date }),
20
+ permalink,
41
21
  draft: frontmatter.draft === true,
42
22
  links: extractCairnLinks(body),
43
23
  };
@@ -9,4 +9,3 @@ export declare function permalink(descriptor: ConceptDescriptor, entry: {
9
9
  slug: string;
10
10
  date?: string;
11
11
  }): string;
12
- //# sourceMappingURL=permalink.d.ts.map
@@ -72,4 +72,3 @@ export interface DefineFieldsOptions<F extends readonly FrontmatterField[]> {
72
72
  /** Declare a concept's fields once. Returns the schema's faces derived from that one declaration. */
73
73
  export declare function defineFields<const F extends readonly FrontmatterField[]>(fields: F, options?: DefineFieldsOptions<F>): ConceptSchema<F>;
74
74
  export {};
75
- //# sourceMappingURL=schema.d.ts.map
@@ -270,4 +270,3 @@ export interface CairnRuntime {
270
270
  fieldTypes?: FieldTypeDef[];
271
271
  }
272
272
  export {};
273
- //# sourceMappingURL=types.d.ts.map
@@ -15,4 +15,3 @@ import type { FrontmatterField, ValidationResult } from './types.js';
15
15
  * `Date` to `YYYY-MM-DD` so a valid parsed date is not mistaken for an empty one.
16
16
  */
17
17
  export declare function validateFields(fields: FrontmatterField[], frontmatter: Record<string, unknown>): ValidationResult;
18
- //# sourceMappingURL=validate.d.ts.map
@@ -12,4 +12,3 @@ type $$ComponentProps = {
12
12
  declare const CairnHead: import("svelte").Component<$$ComponentProps, {}, "">;
13
13
  type CairnHead = ReturnType<typeof CairnHead>;
14
14
  export default CairnHead;
15
- //# sourceMappingURL=CairnHead.svelte.d.ts.map
@@ -65,4 +65,3 @@ export interface ContentIndex<F = Record<string, unknown>> {
65
65
  export declare function fromGlob(record: Record<string, string>): RawFile[];
66
66
  /** Build a concept's index from its raw files and normalized descriptor. */
67
67
  export declare function createContentIndex<F = Record<string, unknown>>(files: RawFile[], descriptor: ConceptDescriptor): ContentIndex<F>;
68
- //# sourceMappingURL=content-index.d.ts.map
@@ -3,47 +3,30 @@
3
3
  // returns cheap plain-data summaries plus an on-demand detail lookup. It is concept-generic:
4
4
  // every operation reads the descriptor and its routing rule, never a hardcoded concept id.
5
5
  import { parseMarkdown } from '../content/frontmatter.js';
6
- import { idFromFilename, slugFromId } from '../content/ids.js';
7
- import { permalink } from '../content/permalink.js';
6
+ import { entryId, entryIdentity, asDate, asString, asTags } from '../content/identity.js';
8
7
  import { deriveExcerpt, wordCount } from './excerpt.js';
9
8
  /** Map a Vite eager `?raw` glob record (`{ path: raw }`) to `RawFile[]`. */
10
9
  export function fromGlob(record) {
11
10
  return Object.entries(record).map(([path, raw]) => ({ path, raw }));
12
11
  }
13
- function basename(path) {
14
- const slash = path.lastIndexOf('/');
15
- return slash >= 0 ? path.slice(slash + 1) : path;
16
- }
17
- function asString(value) {
18
- return typeof value === 'string' && value.trim() ? value : undefined;
19
- }
20
- function asDate(value) {
21
- if (value instanceof Date)
22
- return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
23
- if (typeof value === 'string')
24
- return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
25
- return undefined;
26
- }
27
- function asTags(value) {
28
- return Array.isArray(value) ? value.map(String) : [];
29
- }
30
12
  /** Build a concept's index from its raw files and normalized descriptor. */
31
13
  export function createContentIndex(files, descriptor) {
32
14
  const problems = [];
33
15
  const entries = [];
34
16
  for (const file of files) {
35
- const id = idFromFilename(basename(file.path));
36
- const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
37
17
  const { frontmatter: raw, body } = parseMarkdown(file.raw);
38
- const date = asDate(raw.date);
18
+ const id = entryId(file.path);
39
19
  const draft = raw.draft === true;
40
- // Validate once at build. A failure is recorded for the site gate and excluded from the typed
41
- // read, so every readable entry's frontmatter is the validator's normalized output, never raw.
20
+ // Validate before resolving the permalink. A date-token permalink throws on an entry with no
21
+ // valid date; the validate gate records that as a content problem rather than aborting the whole
22
+ // index build, so one bad entry degrades to a skip, not a crash. A failure is also excluded from
23
+ // the typed read, so every readable entry's frontmatter is the validator's normalized output.
42
24
  const result = descriptor.validate(raw, body);
43
25
  if (!result.ok) {
44
26
  problems.push({ id, draft, errors: result.errors });
45
27
  continue;
46
28
  }
29
+ const { slug, date, permalink } = entryIdentity(descriptor, file.path, raw);
47
30
  const summaryFieldValues = {};
48
31
  for (const key of descriptor.summaryFields) {
49
32
  if (key in result.data)
@@ -53,7 +36,7 @@ export function createContentIndex(files, descriptor) {
53
36
  concept: descriptor.id,
54
37
  id,
55
38
  slug,
56
- permalink: permalink(descriptor, { id, slug, date }),
39
+ permalink,
57
40
  title: asString(raw.title) ?? id,
58
41
  date,
59
42
  updated: asDate(raw.updated),
@@ -21,4 +21,3 @@ export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from '
21
21
  export { jsonLdScript } from './json-ld.js';
22
22
  export { permalink } from '../content/permalink.js';
23
23
  export { buildSiteManifest, buildLinkResolver } from './manifest.js';
24
- //# sourceMappingURL=data.d.ts.map
@@ -8,4 +8,3 @@ export declare function deriveExcerpt(body: string, opts?: {
8
8
  }): string;
9
9
  /** Count words in the stripped body. */
10
10
  export declare function wordCount(body: string): number;
11
- //# sourceMappingURL=excerpt.d.ts.map
@@ -24,4 +24,3 @@ export interface FeedItem {
24
24
  export declare function buildRssFeed(channel: FeedChannel, items: FeedItem[]): string;
25
25
  /** Build a JSON Feed 1.1 document. */
26
26
  export declare function buildJsonFeed(channel: FeedChannel, items: FeedItem[]): string;
27
- //# sourceMappingURL=feeds.d.ts.map
@@ -1,2 +1 @@
1
1
  export { default as CairnHead } from './CairnHead.svelte';
2
- //# sourceMappingURL=head.d.ts.map
@@ -1,4 +1,3 @@
1
1
  export * from './data.js';
2
2
  export { createPublicRoutes } from '../sveltekit/public-routes.js';
3
3
  export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData, } from '../sveltekit/public-routes.js';
4
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export declare function jsonLdScript(data: Record<string, unknown>): string;
2
- //# sourceMappingURL=json-ld.d.ts.map
@@ -10,4 +10,3 @@ export declare function buildSiteManifest<A extends CairnAdapter>(adapter: A, co
10
10
  /** A resolver backed by the site index, for the build. A miss throws, so a dangling cairn: token
11
11
  * fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
12
12
  export declare function buildLinkResolver(site: SiteIndex): LinkResolve;
13
- //# sourceMappingURL=manifest.d.ts.map