@glw907/cairn-cms 0.5.1 → 0.6.0-rc.1

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 (234) hide show
  1. package/dist/auth/crypto.d.ts +13 -0
  2. package/dist/auth/crypto.d.ts.map +1 -0
  3. package/dist/auth/crypto.js +31 -0
  4. package/dist/auth/store.d.ts +41 -0
  5. package/dist/auth/store.d.ts.map +1 -0
  6. package/dist/auth/store.js +115 -0
  7. package/dist/auth/types.d.ts +25 -0
  8. package/dist/auth/types.d.ts.map +1 -0
  9. package/dist/auth/types.js +1 -0
  10. package/dist/components/AdminLayout.svelte +58 -164
  11. package/dist/components/AdminLayout.svelte.d.ts +14 -18
  12. package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
  13. package/dist/components/ComponentPalette.svelte +36 -20
  14. package/dist/components/ComponentPalette.svelte.d.ts +11 -4
  15. package/dist/components/ComponentPalette.svelte.d.ts.map +1 -1
  16. package/dist/components/ConceptList.svelte +81 -0
  17. package/dist/components/ConceptList.svelte.d.ts +13 -0
  18. package/dist/components/ConceptList.svelte.d.ts.map +1 -0
  19. package/dist/components/ConfirmPage.svelte +23 -20
  20. package/dist/components/ConfirmPage.svelte.d.ts +6 -0
  21. package/dist/components/ConfirmPage.svelte.d.ts.map +1 -1
  22. package/dist/components/EditPage.svelte +155 -136
  23. package/dist/components/EditPage.svelte.d.ts +16 -8
  24. package/dist/components/EditPage.svelte.d.ts.map +1 -1
  25. package/dist/components/LoginPage.svelte +42 -52
  26. package/dist/components/LoginPage.svelte.d.ts +12 -0
  27. package/dist/components/LoginPage.svelte.d.ts.map +1 -1
  28. package/dist/components/ManageEditors.svelte +81 -0
  29. package/dist/components/ManageEditors.svelte.d.ts +23 -0
  30. package/dist/components/ManageEditors.svelte.d.ts.map +1 -0
  31. package/dist/components/MarkdownEditor.svelte +81 -0
  32. package/dist/components/MarkdownEditor.svelte.d.ts +20 -0
  33. package/dist/components/MarkdownEditor.svelte.d.ts.map +1 -0
  34. package/dist/components/NavTree.svelte +73 -63
  35. package/dist/components/NavTree.svelte.d.ts +13 -4
  36. package/dist/components/NavTree.svelte.d.ts.map +1 -1
  37. package/dist/components/cairn-admin.css +42 -0
  38. package/dist/components/index.d.ts +3 -2
  39. package/dist/components/index.d.ts.map +1 -1
  40. package/dist/components/index.js +5 -4
  41. package/dist/content/compose.d.ts +7 -0
  42. package/dist/content/compose.d.ts.map +1 -0
  43. package/dist/content/compose.js +32 -0
  44. package/dist/content/concepts.d.ts +17 -0
  45. package/dist/content/concepts.d.ts.map +1 -0
  46. package/dist/content/concepts.js +41 -0
  47. package/dist/content/frontmatter.d.ts +18 -0
  48. package/dist/content/frontmatter.d.ts.map +1 -0
  49. package/dist/content/frontmatter.js +58 -0
  50. package/dist/content/ids.d.ts +17 -0
  51. package/dist/content/ids.d.ts.map +1 -0
  52. package/dist/content/ids.js +33 -0
  53. package/dist/content/types.d.ts +210 -0
  54. package/dist/content/types.d.ts.map +1 -0
  55. package/dist/content/types.js +1 -0
  56. package/dist/content/validate.d.ts +13 -0
  57. package/dist/content/validate.d.ts.map +1 -0
  58. package/dist/content/validate.js +45 -0
  59. package/dist/email.d.ts +25 -12
  60. package/dist/email.d.ts.map +1 -1
  61. package/dist/email.js +24 -24
  62. package/dist/env.d.ts +24 -0
  63. package/dist/env.d.ts.map +1 -0
  64. package/dist/env.js +29 -0
  65. package/dist/github/credentials.d.ts +12 -0
  66. package/dist/github/credentials.d.ts.map +1 -0
  67. package/dist/github/credentials.js +11 -0
  68. package/dist/github/repo.d.ts +49 -0
  69. package/dist/github/repo.d.ts.map +1 -0
  70. package/dist/github/repo.js +123 -0
  71. package/dist/github/signing.d.ts +17 -0
  72. package/dist/github/signing.d.ts.map +1 -0
  73. package/dist/github/signing.js +79 -0
  74. package/dist/github/types.d.ts +35 -0
  75. package/dist/github/types.d.ts.map +1 -0
  76. package/dist/github/types.js +19 -0
  77. package/dist/index.d.ts +27 -8
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +21 -10
  80. package/dist/{nav.d.ts → nav/site-config.d.ts} +16 -24
  81. package/dist/nav/site-config.d.ts.map +1 -0
  82. package/dist/{nav.js → nav/site-config.js} +27 -13
  83. package/dist/render/glyph.d.ts +1 -1
  84. package/dist/render/glyph.d.ts.map +1 -1
  85. package/dist/render/index.d.ts +5 -5
  86. package/dist/render/index.d.ts.map +1 -1
  87. package/dist/render/index.js +6 -6
  88. package/dist/render/pipeline.d.ts +7 -6
  89. package/dist/render/pipeline.d.ts.map +1 -1
  90. package/dist/render/pipeline.js +5 -5
  91. package/dist/render/registry.d.ts +10 -6
  92. package/dist/render/registry.d.ts.map +1 -1
  93. package/dist/render/registry.js +8 -6
  94. package/dist/render/rehype-dispatch.d.ts +8 -7
  95. package/dist/render/rehype-dispatch.d.ts.map +1 -1
  96. package/dist/render/rehype-dispatch.js +16 -14
  97. package/dist/render/remark-directives.d.ts +1 -1
  98. package/dist/render/remark-directives.d.ts.map +1 -1
  99. package/dist/render/sanitize.d.ts +8 -0
  100. package/dist/render/sanitize.d.ts.map +1 -0
  101. package/dist/render/sanitize.js +26 -0
  102. package/dist/sveltekit/auth-routes.d.ts +23 -0
  103. package/dist/sveltekit/auth-routes.d.ts.map +1 -0
  104. package/dist/sveltekit/auth-routes.js +85 -0
  105. package/dist/sveltekit/content-routes.d.ts +80 -0
  106. package/dist/sveltekit/content-routes.d.ts.map +1 -0
  107. package/dist/sveltekit/content-routes.js +183 -0
  108. package/dist/sveltekit/editors-routes.d.ts +24 -0
  109. package/dist/sveltekit/editors-routes.d.ts.map +1 -0
  110. package/dist/sveltekit/editors-routes.js +73 -0
  111. package/dist/sveltekit/guard.d.ts +9 -0
  112. package/dist/sveltekit/guard.d.ts.map +1 -0
  113. package/dist/sveltekit/guard.js +43 -0
  114. package/dist/sveltekit/health.d.ts +19 -0
  115. package/dist/sveltekit/health.d.ts.map +1 -0
  116. package/dist/sveltekit/health.js +12 -0
  117. package/dist/sveltekit/index.d.ts +9 -173
  118. package/dist/sveltekit/index.d.ts.map +1 -1
  119. package/dist/sveltekit/index.js +8 -348
  120. package/dist/sveltekit/nav-routes.d.ts +30 -0
  121. package/dist/sveltekit/nav-routes.d.ts.map +1 -0
  122. package/dist/sveltekit/nav-routes.js +103 -0
  123. package/dist/sveltekit/types.d.ts +32 -0
  124. package/dist/sveltekit/types.d.ts.map +1 -0
  125. package/dist/sveltekit/types.js +1 -0
  126. package/package.json +33 -57
  127. package/src/lib/auth/crypto.ts +37 -0
  128. package/src/lib/auth/store.ts +158 -0
  129. package/src/lib/auth/types.ts +27 -0
  130. package/src/lib/components/AdminLayout.svelte +58 -164
  131. package/src/lib/components/ComponentPalette.svelte +36 -20
  132. package/src/lib/components/ConceptList.svelte +81 -0
  133. package/src/lib/components/ConfirmPage.svelte +23 -20
  134. package/src/lib/components/EditPage.svelte +155 -136
  135. package/src/lib/components/LoginPage.svelte +42 -52
  136. package/src/lib/components/ManageEditors.svelte +81 -0
  137. package/src/lib/components/MarkdownEditor.svelte +81 -0
  138. package/src/lib/components/NavTree.svelte +73 -63
  139. package/src/lib/components/cairn-admin.css +42 -0
  140. package/src/lib/components/index.ts +5 -4
  141. package/src/lib/content/compose.ts +39 -0
  142. package/src/lib/content/concepts.ts +57 -0
  143. package/src/lib/content/frontmatter.ts +71 -0
  144. package/src/lib/content/ids.ts +38 -0
  145. package/src/lib/content/types.ts +235 -0
  146. package/src/lib/content/validate.ts +51 -0
  147. package/src/lib/email.ts +52 -38
  148. package/src/lib/env.ts +32 -0
  149. package/src/lib/github/credentials.ts +27 -0
  150. package/src/lib/github/repo.ts +138 -0
  151. package/src/lib/github/signing.ts +97 -0
  152. package/src/lib/github/types.ts +46 -0
  153. package/src/lib/index.ts +86 -10
  154. package/src/lib/{nav.ts → nav/site-config.ts} +31 -24
  155. package/src/lib/render/glyph.ts +6 -6
  156. package/src/lib/render/index.ts +6 -6
  157. package/src/lib/render/pipeline.ts +23 -22
  158. package/src/lib/render/registry.ts +35 -26
  159. package/src/lib/render/rehype-dispatch.ts +58 -56
  160. package/src/lib/render/remark-directives.ts +46 -46
  161. package/src/lib/render/sanitize.ts +27 -0
  162. package/src/lib/sveltekit/auth-routes.ts +107 -0
  163. package/src/lib/sveltekit/content-routes.ts +261 -0
  164. package/src/lib/sveltekit/editors-routes.ts +82 -0
  165. package/src/lib/sveltekit/guard.ts +47 -0
  166. package/src/lib/sveltekit/health.ts +24 -0
  167. package/src/lib/sveltekit/index.ts +19 -512
  168. package/src/lib/sveltekit/nav-routes.ts +139 -0
  169. package/src/lib/sveltekit/types.ts +33 -0
  170. package/dist/adapter.d.ts +0 -93
  171. package/dist/adapter.d.ts.map +0 -1
  172. package/dist/adapter.js +0 -30
  173. package/dist/auth/admins.d.ts +0 -33
  174. package/dist/auth/admins.d.ts.map +0 -1
  175. package/dist/auth/admins.js +0 -90
  176. package/dist/auth/capabilities.d.ts +0 -7
  177. package/dist/auth/capabilities.d.ts.map +0 -1
  178. package/dist/auth/capabilities.js +0 -26
  179. package/dist/auth/config.d.ts +0 -2097
  180. package/dist/auth/config.d.ts.map +0 -1
  181. package/dist/auth/config.js +0 -78
  182. package/dist/auth/guard.d.ts +0 -34
  183. package/dist/auth/guard.d.ts.map +0 -1
  184. package/dist/auth/guard.js +0 -47
  185. package/dist/auth/index.d.ts +0 -5
  186. package/dist/auth/index.d.ts.map +0 -1
  187. package/dist/auth/index.js +0 -7
  188. package/dist/auth/schema.d.ts +0 -750
  189. package/dist/auth/schema.d.ts.map +0 -1
  190. package/dist/auth/schema.js +0 -93
  191. package/dist/carta.d.ts +0 -39
  192. package/dist/carta.d.ts.map +0 -1
  193. package/dist/carta.js +0 -30
  194. package/dist/components/CollectionList.svelte +0 -96
  195. package/dist/components/CollectionList.svelte.d.ts +0 -8
  196. package/dist/components/CollectionList.svelte.d.ts.map +0 -1
  197. package/dist/components/ManageAdmins.svelte +0 -84
  198. package/dist/components/ManageAdmins.svelte.d.ts +0 -10
  199. package/dist/components/ManageAdmins.svelte.d.ts.map +0 -1
  200. package/dist/content.d.ts +0 -3
  201. package/dist/content.d.ts.map +0 -1
  202. package/dist/content.js +0 -10
  203. package/dist/editor.d.ts +0 -25
  204. package/dist/editor.d.ts.map +0 -1
  205. package/dist/editor.js +0 -20
  206. package/dist/frontmatter.d.ts +0 -3
  207. package/dist/frontmatter.d.ts.map +0 -1
  208. package/dist/frontmatter.js +0 -16
  209. package/dist/github.d.ts +0 -72
  210. package/dist/github.d.ts.map +0 -1
  211. package/dist/github.js +0 -171
  212. package/dist/nav.d.ts.map +0 -1
  213. package/dist/slug.d.ts +0 -7
  214. package/dist/slug.d.ts.map +0 -1
  215. package/dist/slug.js +0 -15
  216. package/dist/utils.d.ts +0 -3
  217. package/dist/utils.d.ts.map +0 -1
  218. package/dist/utils.js +0 -11
  219. package/src/lib/adapter.ts +0 -144
  220. package/src/lib/auth/admins.ts +0 -106
  221. package/src/lib/auth/capabilities.ts +0 -35
  222. package/src/lib/auth/config.ts +0 -108
  223. package/src/lib/auth/guard.ts +0 -60
  224. package/src/lib/auth/index.ts +0 -7
  225. package/src/lib/auth/schema.ts +0 -112
  226. package/src/lib/carta.ts +0 -59
  227. package/src/lib/components/CollectionList.svelte +0 -96
  228. package/src/lib/components/ManageAdmins.svelte +0 -84
  229. package/src/lib/content.ts +0 -11
  230. package/src/lib/editor.ts +0 -38
  231. package/src/lib/frontmatter.ts +0 -17
  232. package/src/lib/github.ts +0 -220
  233. package/src/lib/slug.ts +0 -16
  234. package/src/lib/utils.ts +0 -12
@@ -1,10 +1,16 @@
1
- // cairn-core: the navigation tree. A menu lives in the site's git-committed `site.config.yaml`
2
- // under `menus.<name>`, read at build time by the public layout and edited from `/admin/nav`,
3
- // which commits the file back through the GitHub-App pipeline. The engine returns data only; each
4
- // site renders the tree with its own header markup.
1
+ // The navigation tree and its YAML site-config. A menu lives in the site's git-committed config
2
+ // under `menus.<name>`, read at build time by the public layout and edited from /admin/nav, which
3
+ // commits the file back through the GitHub-App pipeline. This module is pure: parse, validate, and
4
+ // rewrite only. The engine returns data; each site renders the tree with its own markup.
5
5
  import { parse as parseYaml, parseDocument } from 'yaml';
6
6
  /** Total node cap across the whole tree, a guard against a runaway payload. */
7
7
  export const MAX_NAV_NODES = 200;
8
+ /** Maximum character length for a node label. */
9
+ export const MAX_LABEL_LENGTH = 500;
10
+ /** Maximum character length for a node URL. */
11
+ export const MAX_URL_LENGTH = 2048;
12
+ /** Allowlist for safe URL schemes: site-relative, in-page anchors, http(s), mailto, and tel. */
13
+ const SAFE_URL = /^(\/|#|https?:\/\/|mailto:|tel:)/i;
8
14
  export class NavValidationError extends Error {
9
15
  constructor(message) {
10
16
  super(message);
@@ -12,9 +18,9 @@ export class NavValidationError extends Error {
12
18
  }
13
19
  }
14
20
  /**
15
- * Validate and normalize an untrusted value into a NavNode[]: arrays only, non-empty labels,
16
- * depth within `maxDepth` (1 = flat), bounded node count, and only the three known keys kept.
17
- * Throws NavValidationError on any violation. Used by `navSave` before writing.
21
+ * Validate and normalize an untrusted value into a NavNode[]: arrays only, non-empty labels, depth
22
+ * within `maxDepth` (1 is flat), a bounded node count, and only the three known keys kept. Throws
23
+ * NavValidationError on any violation. Used by navSave before writing.
18
24
  */
19
25
  export function validateNavTree(value, maxDepth) {
20
26
  let count = 0;
@@ -30,11 +36,19 @@ export function validateNavTree(value, maxDepth) {
30
36
  const label = typeof item.label === 'string' ? item.label.trim() : '';
31
37
  if (!label)
32
38
  throw new NavValidationError('Each item needs a label');
39
+ if (label.length > MAX_LABEL_LENGTH)
40
+ throw new NavValidationError('Label is too long (max 500 characters)');
33
41
  if (++count > MAX_NAV_NODES)
34
42
  throw new NavValidationError('Too many navigation items');
35
43
  const node = { label };
36
- if (typeof item.url === 'string' && item.url.trim())
37
- node.url = item.url.trim();
44
+ if (typeof item.url === 'string' && item.url.trim()) {
45
+ const url = item.url.trim();
46
+ if (url.length > MAX_URL_LENGTH)
47
+ throw new NavValidationError('URL is too long (max 2048 characters)');
48
+ if (!SAFE_URL.test(url))
49
+ throw new NavValidationError('URL must start with /, #, http(s)://, mailto:, or tel:');
50
+ node.url = url;
51
+ }
38
52
  if (item.children !== undefined) {
39
53
  const children = walk(item.children, depth + 1);
40
54
  if (children.length)
@@ -71,10 +85,10 @@ export function extractMenu(config, name, maxDepth) {
71
85
  return validateNavTree(menu, maxDepth);
72
86
  }
73
87
  /**
74
- * Replace one named menu in the YAML site-config text and re-serialize, preserving every other
75
- * top-level key (siteName, other menus, settings, ...). The `/admin/nav` editor commits the result.
76
- * Parses into a Document so the rest of the file round-trips; YAML comments are not preserved
77
- * (an accepted trade), but data keys are. A leaf node serializes without `url`/`children` keys.
88
+ * Replace one named menu in the YAML site-config text and reserialize, preserving every other
89
+ * top-level key (siteName, other menus, settings). Parses into a Document so the rest of the file
90
+ * round-trips. YAML comments are not preserved (an accepted trade); data keys are. A leaf node
91
+ * serializes without `url`/`children` keys.
78
92
  */
79
93
  export function setMenu(raw, name, tree) {
80
94
  const doc = parseDocument(raw);
@@ -1,5 +1,5 @@
1
1
  import type { Element } from 'hast';
2
- /** A glyph name SVG path-data map (the site owns the icon set). */
2
+ /** A glyph name to SVG path-data map (the site owns the icon set). */
3
3
  export type IconSet = Record<string, string>;
4
4
  /** Inline SVG glyph as a real hast node: class ec-glyph, 256 viewBox, currentColor fill. */
5
5
  export declare function glyph(name: string, icons: IconSet): Element;
@@ -1 +1 @@
1
- {"version":3,"file":"glyph.d.ts","sourceRoot":"","sources":["../../src/lib/render/glyph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,qEAAqE;AACrE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE7C,4FAA4F;AAC5F,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAM3D"}
1
+ {"version":3,"file":"glyph.d.ts","sourceRoot":"","sources":["../../src/lib/render/glyph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,sEAAsE;AACtE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE7C,4FAA4F;AAC5F,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAM3D"}
@@ -1,6 +1,6 @@
1
- export * from './registry';
2
- export * from './glyph';
3
- export * from './remark-directives';
4
- export * from './rehype-dispatch';
5
- export * from './pipeline';
1
+ export * from './registry.js';
2
+ export * from './glyph.js';
3
+ export * from './remark-directives.js';
4
+ export * from './rehype-dispatch.js';
5
+ export * from './pipeline.js';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/render/index.ts"],"names":[],"mappings":"AAGA,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/render/index.ts"],"names":[],"mappings":"AAGA,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC"}
@@ -1,8 +1,8 @@
1
- // cairn-cms render engine: a directive-driven markdown HTML pipeline whose
1
+ // cairn-cms render engine: a directive-driven markdown to HTML pipeline whose
2
2
  // component vocabulary is supplied by a site's component registry. The site owns the
3
3
  // component builders, class names, icon set, and CSS; the engine owns the machinery.
4
- export * from './registry';
5
- export * from './glyph';
6
- export * from './remark-directives';
7
- export * from './rehype-dispatch';
8
- export * from './pipeline';
4
+ export * from './registry.js';
5
+ export * from './glyph.js';
6
+ export * from './remark-directives.js';
7
+ export * from './rehype-dispatch.js';
8
+ export * from './pipeline.js';
@@ -1,12 +1,13 @@
1
1
  import { type PluggableList } from 'unified';
2
- import type { ComponentRegistry } from './registry';
2
+ import type { ComponentRegistry } from './registry.js';
3
3
  export interface RendererOptions {
4
- /** A site's per-index motion formula for the top-level rise stagger
5
- * (e.g. ecnordic's `(i) => '--rise:' + …`). Omit for no stagger. */
6
- rise?: (idx: number) => string;
4
+ /** Stamp a `data-rise` ordinal (0, 1, 2, …) on each top-level component so a site's
5
+ * CSS can drive an entrance-cascade delay off it. Omit for no stagger. The ordinal
6
+ * is inert, so a consumer's sanitize floor can keep `data-rise` and drop `style`. */
7
+ stagger?: boolean;
7
8
  }
8
- /** Compose a site's render pipeline from its component registry: directive syntax
9
- * stamped markers registry-built hast. Returns `renderMarkdown` plus the remark/
9
+ /** Compose a site's render pipeline from its component registry: directive syntax to
10
+ * stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
10
11
  * rehype plugin arrays (so the Carta editor preview can reuse the exact same set). */
11
12
  export declare function createRenderer(registry: ComponentRegistry, options?: RendererOptions): {
12
13
  remarkPlugins: PluggableList;
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAUtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC/B;yEACqE;IACrE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/B;AAED;;uFAEuF;AACvF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,GAAE,eAAoB;;;8BAavD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;EAEzD"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAUtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;;0FAEsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;uFAEuF;AACvF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,GAAE,eAAoB;;;8BAarD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;EAE3D"}
@@ -6,14 +6,14 @@ import remarkRehype from 'remark-rehype';
6
6
  import rehypeRaw from 'rehype-raw';
7
7
  import rehypeSlug from 'rehype-slug';
8
8
  import rehypeStringify from 'rehype-stringify';
9
- import { remarkDirectiveStamp } from './remark-directives';
10
- import { rehypeDispatch } from './rehype-dispatch';
11
- /** Compose a site's render pipeline from its component registry: directive syntax
12
- * stamped markers registry-built hast. Returns `renderMarkdown` plus the remark/
9
+ import { remarkDirectiveStamp } from './remark-directives.js';
10
+ import { rehypeDispatch } from './rehype-dispatch.js';
11
+ /** Compose a site's render pipeline from its component registry: directive syntax to
12
+ * stamped markers to registry-built hast. Returns `renderMarkdown` plus the remark/
13
13
  * rehype plugin arrays (so the Carta editor preview can reuse the exact same set). */
14
14
  export function createRenderer(registry, options = {}) {
15
15
  const remarkPlugins = [remarkDirective, [remarkDirectiveStamp, registry]];
16
- const rehypePlugins = [rehypeRaw, [rehypeDispatch, registry, options.rise], rehypeSlug];
16
+ const rehypePlugins = [rehypeRaw, [rehypeDispatch, registry, options.stagger], rehypeSlug];
17
17
  const processor = unified()
18
18
  .use(remarkParse)
19
19
  .use(remarkGfm)
@@ -9,9 +9,11 @@ export interface ComponentDef {
9
9
  description: string;
10
10
  /** Markdown scaffold inserted at the cursor by the editor palette. */
11
11
  insertTemplate: string;
12
- /** Build the final hast element from the stamped directive element. */
13
- build: (node: Element, rise?: string) => Element;
14
- /** Optional role→default-icon (e.g. `{ caution: 'warning' }`). */
12
+ /** Build the final hast element from the stamped directive element. The engine
13
+ * stamps the entrance-stagger ordinal (`data-rise`) on the top-level result, so a
14
+ * build fn stays free of any motion concern. */
15
+ build: (node: Element) => Element;
16
+ /** Optional role-to-default-icon, e.g. `{ caution: 'warning' }`. */
15
17
  defaultIconByRole?: Record<string, string>;
16
18
  }
17
19
  export interface ComponentRegistry {
@@ -20,9 +22,11 @@ export interface ComponentRegistry {
20
22
  get(name: string): ComponentDef | undefined;
21
23
  defaultIcon(name: string, role?: string): string | undefined;
22
24
  }
23
- /** Build a registry from a site's component definitions. The single source the
24
- * render pipeline (directive stamp + rehype dispatch) and the editor palette read. */
25
- export declare function defineRegistry(input: {
25
+ /**
26
+ * Build a registry from a site's component definitions. The single source the render
27
+ * pipeline (directive stamp plus rehype dispatch) and the editor palette both read.
28
+ */
29
+ export declare function defineRegistry({ components }: {
26
30
  components: ComponentDef[];
27
31
  }): ComponentRegistry;
28
32
  //# sourceMappingURL=registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/lib/render/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC5B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC7D;AAED;uFACuF;AACvF,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,UAAU,EAAE,YAAY,EAAE,CAAA;CAAE,GAAG,iBAAiB,CAQvF"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/lib/render/registry.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB;;qDAEiD;IACjD,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;IAClC,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC9D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,YAAY,EAAE,CAAA;CAAE,GAAG,iBAAiB,CAQhG"}
@@ -1,10 +1,12 @@
1
- /** Build a registry from a site's component definitions. The single source the
2
- * render pipeline (directive stamp + rehype dispatch) and the editor palette read. */
3
- export function defineRegistry(input) {
4
- const byName = new Map(input.components.map((c) => [c.name, c]));
1
+ /**
2
+ * Build a registry from a site's component definitions. The single source the render
3
+ * pipeline (directive stamp plus rehype dispatch) and the editor palette both read.
4
+ */
5
+ export function defineRegistry({ components }) {
6
+ const byName = new Map(components.map((c) => [c.name, c]));
5
7
  return {
6
- defs: input.components,
7
- names: input.components.map((c) => c.name),
8
+ defs: components,
9
+ names: components.map((c) => c.name),
8
10
  get: (name) => byName.get(name),
9
11
  defaultIcon: (name, role) => (role ? byName.get(name)?.defaultIconByRole?.[role] : undefined),
10
12
  };
@@ -1,5 +1,5 @@
1
1
  import type { Root, Element, ElementContent } from 'hast';
2
- import type { ComponentRegistry } from './registry';
2
+ import type { ComponentRegistry } from './registry.js';
3
3
  export declare function isElement(node: ElementContent | undefined): node is Element;
4
4
  export declare function strProp(node: Element, name: string): string | undefined;
5
5
  /** Wrap a pre-built glyph in an ec-icon span; secondary role adds the modifier. */
@@ -10,15 +10,16 @@ export declare function splitHead(node: Element, makeIcon?: MakeIcon): {
10
10
  head: Element;
11
11
  rest: ElementContent[];
12
12
  };
13
- /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`,
14
- * with an optional inline rise style. */
15
- export declare function cardShell(classes: string[], rise: string | undefined, body: ElementContent[]): Element;
13
+ /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`. */
14
+ export declare function cardShell(classes: string[], body: ElementContent[]): Element;
16
15
  /** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
17
16
  * text nodes so the bare list serializes without newlines. Returns that <ul>. */
18
17
  export declare function markFirstList(children: ElementContent[]): Element | undefined;
19
18
  /** Rehype transformer: dispatch each stamped element through its registry `build`
20
- * fn. Top-level primitives get a document-order rise stagger when `rise` is
21
- * supplied (a site's per-index motion formula); nested ones don't. Non-primitive
19
+ * fn. When `stagger` is on, each top-level primitive gets a `data-rise` attribute
20
+ * carrying its document-order index (0, 1, 2, …); the site's CSS maps that ordinal
21
+ * to an entrance delay. The index is inert, so a consumer's sanitize floor can keep
22
+ * `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
22
23
  * content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
23
- export declare function rehypeDispatch(registry: ComponentRegistry, rise?: (idx: number) => string): (tree: Root) => void;
24
+ export declare function rehypeDispatch(registry: ComponentRegistry, stagger?: boolean): (tree: Root) => void;
24
25
  //# sourceMappingURL=rehype-dispatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rehype-dispatch.d.ts","sourceRoot":"","sources":["../../src/lib/render/rehype-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAc,MAAM,MAAM,CAAC;AAEtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,IAAI,OAAO,CAE3E;AAKD,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGvE;AAED,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;AAMhE,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,cAAc,EAAE,CAAA;CAAE,CAYvG;AAED;0CAC0C;AAC1C,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,OAAO,CAItG;AAED;kFACkF;AAClF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,OAAO,GAAG,SAAS,CAS7E;AAmBD;;;mFAGmF;AACnF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,IACjF,MAAM,IAAI,UAUlB"}
1
+ {"version":3,"file":"rehype-dispatch.d.ts","sourceRoot":"","sources":["../../src/lib/render/rehype-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAE1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,IAAI,OAAO,CAE3E;AAKD,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGvE;AAED,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;AAMhE,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,cAAc,EAAE,CAAA;CAAE,CAYvG;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,OAAO,CAE5E;AAED;kFACkF;AAClF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,OAAO,GAAG,SAAS,CAS7E;AAoBD;;;;;mFAKmF;AACnF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,OAAO,IACnE,MAAM,IAAI,UAYnB"}
@@ -32,13 +32,9 @@ export function splitHead(node, makeIcon) {
32
32
  headKids.push(h2);
33
33
  return { head: h('div', { className: ['ec-head'] }, headKids), rest };
34
34
  }
35
- /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`,
36
- * with an optional inline rise style. */
37
- export function cardShell(classes, rise, body) {
38
- const properties = { className: classes };
39
- if (rise)
40
- properties.style = rise;
41
- return h('section', properties, [h('div', { className: ['card-body'] }, body)]);
35
+ /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`. */
36
+ export function cardShell(classes, body) {
37
+ return h('section', { className: classes }, [h('div', { className: ['card-body'] }, body)]);
42
38
  }
43
39
  /** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
44
40
  * text nodes so the bare list serializes without newlines. Returns that <ul>. */
@@ -51,7 +47,8 @@ export function markFirstList(children) {
51
47
  return ul;
52
48
  }
53
49
  // Recurse into a node's children, transforming any nested primitive sections
54
- // (a grid inside a card, panels inside a split) WITHOUT a rise stagger.
50
+ // (a grid inside a card, panels inside a split). Nested primitives never carry the
51
+ // entrance stagger; only top-level ones do (stamped in the transformer below).
55
52
  function transformChildren(children, registry) {
56
53
  return children.map((c) => {
57
54
  if (isElement(c) && c.properties?.dataPrimitive)
@@ -61,22 +58,27 @@ function transformChildren(children, registry) {
61
58
  return c;
62
59
  });
63
60
  }
64
- function transformNode(node, registry, rise) {
61
+ function transformNode(node, registry) {
65
62
  node.children = transformChildren(node.children, registry);
66
63
  const name = strProp(node, 'dataPrimitive');
67
64
  const def = name ? registry.get(name) : undefined;
68
- return def ? def.build(node, rise) : node;
65
+ return def ? def.build(node) : node;
69
66
  }
70
67
  /** Rehype transformer: dispatch each stamped element through its registry `build`
71
- * fn. Top-level primitives get a document-order rise stagger when `rise` is
72
- * supplied (a site's per-index motion formula); nested ones don't. Non-primitive
68
+ * fn. When `stagger` is on, each top-level primitive gets a `data-rise` attribute
69
+ * carrying its document-order index (0, 1, 2, …); the site's CSS maps that ordinal
70
+ * to an entrance delay. The index is inert, so a consumer's sanitize floor can keep
71
+ * `data-rise` while dropping `style`. Nested primitives never get it. Non-primitive
73
72
  * content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
74
- export function rehypeDispatch(registry, rise) {
73
+ export function rehypeDispatch(registry, stagger) {
75
74
  return (tree) => {
76
75
  let idx = 0;
77
76
  tree.children = tree.children.map((child) => {
78
77
  if (isElement(child) && child.properties?.dataPrimitive) {
79
- return transformNode(child, registry, rise ? rise(idx++) : undefined);
78
+ const el = transformNode(child, registry);
79
+ if (stagger)
80
+ el.properties = { ...el.properties, dataRise: String(idx++) };
81
+ return el;
80
82
  }
81
83
  if (isElement(child))
82
84
  child.children = transformChildren(child.children, registry);
@@ -1,4 +1,4 @@
1
1
  import type { Root } from 'mdast';
2
- import type { ComponentRegistry } from './registry';
2
+ import type { ComponentRegistry } from './registry.js';
3
3
  export declare function remarkDirectiveStamp(registry: ComponentRegistry): (tree: Root) => void;
4
4
  //# sourceMappingURL=remark-directives.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"remark-directives.d.ts","sourceRoot":"","sources":["../../src/lib/render/remark-directives.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA8B,IAAI,EAAQ,MAAM,OAAO,CAAC;AAGpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAmCpD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,iBAAiB,IAEvD,MAAM,IAAI,UA8BlB"}
1
+ {"version":3,"file":"remark-directives.d.ts","sourceRoot":"","sources":["../../src/lib/render/remark-directives.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA8B,IAAI,EAAQ,MAAM,OAAO,CAAC;AAGpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAmCvD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,iBAAiB,IAEtD,MAAM,IAAI,UA8BnB"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Sanitize rendered preview HTML before it reaches `{@html}`. Strips scripts, inline event
3
+ * handlers, and dangerous URL schemes (`javascript:`, `data:`) while keeping ordinary formatting.
4
+ * Also forces `rel="noopener noreferrer"` on any anchor with `target="_blank"` to prevent
5
+ * reverse-tabnabbing. Browser-only; resolves the same string DOMPurify would return.
6
+ */
7
+ export declare function sanitizePreviewHtml(html: string): Promise<string>;
8
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/lib/render/sanitize.ts"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAavE"}
@@ -0,0 +1,26 @@
1
+ // The live preview's sanitize floor. Carta runs with `sanitizer: false` behind the MarkdownEditor
2
+ // seam, so the admin preview pane is the one barrier between editor-authored markdown and the DOM.
3
+ // DOMPurify needs a DOM, and the preview renders only in the browser after mount, so DOMPurify
4
+ // loads through a dynamic import: the module never evaluates a DOM library on the Worker, and a
5
+ // server import of this file pulls in nothing.
6
+ let purify = null;
7
+ /**
8
+ * Sanitize rendered preview HTML before it reaches `{@html}`. Strips scripts, inline event
9
+ * handlers, and dangerous URL schemes (`javascript:`, `data:`) while keeping ordinary formatting.
10
+ * Also forces `rel="noopener noreferrer"` on any anchor with `target="_blank"` to prevent
11
+ * reverse-tabnabbing. Browser-only; resolves the same string DOMPurify would return.
12
+ */
13
+ export async function sanitizePreviewHtml(html) {
14
+ if (!purify) {
15
+ const mod = await import('dompurify');
16
+ purify = mod.default;
17
+ purify.addHook('afterSanitizeAttributes', (node) => {
18
+ if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {
19
+ node.setAttribute('rel', 'noopener noreferrer');
20
+ }
21
+ });
22
+ }
23
+ // ADD_ATTR: ['target'] allows target="_blank" through so the afterSanitizeAttributes hook
24
+ // can enforce rel="noopener noreferrer" on those anchors before they reach the DOM.
25
+ return purify.sanitize(html, { ADD_ATTR: ['target'] });
26
+ }
@@ -0,0 +1,23 @@
1
+ import { type AuthBranding, type SendMagicLink } from '../email.js';
2
+ import type { RequestContext } from './types.js';
3
+ export interface AuthRoutesConfig {
4
+ branding: AuthBranding;
5
+ send?: SendMagicLink;
6
+ }
7
+ export declare function createAuthRoutes(config: AuthRoutesConfig): {
8
+ loginLoad: (event: RequestContext) => {
9
+ siteName: string;
10
+ error: string | null;
11
+ };
12
+ requestAction: (event: RequestContext) => Promise<{
13
+ sent: true;
14
+ }>;
15
+ confirmLoad: (event: RequestContext) => {
16
+ token: string;
17
+ siteName: string;
18
+ error: string | null;
19
+ };
20
+ confirmAction: (event: RequestContext) => Promise<never>;
21
+ logoutAction: (event: RequestContext) => Promise<never>;
22
+ };
23
+ //# sourceMappingURL=auth-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/auth-routes.ts"],"names":[],"mappings":"AAcA,OAAO,EAAyC,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB;uBA2B7B,cAAc,KAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;2BAnBjD,cAAc,KAAG,OAAO,CAAC;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;yBA4BnE,cAAc,KACpB;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;2BAcxB,cAAc,KAAG,OAAO,CAAC,KAAK,CAAC;0BAwBhC,cAAc,KAAG,OAAO,CAAC,KAAK,CAAC;EASnE"}
@@ -0,0 +1,85 @@
1
+ // The SvelteKit handlers for the magic-link flow, consumed by a site's thin route shims.
2
+ // The factory takes per-site branding and an injected send, so tests run the real handlers
3
+ // against a sink. The confirm-load, confirm, and logout handlers arrive in Task 6.
4
+ import { redirect } from '@sveltejs/kit';
5
+ import { requireOrigin, requireDb } from '../env.js';
6
+ import { generateToken, generateSessionId, hashToken, TOKEN_TTL_MS, SESSION_TTL_MS, COOKIE_NAME, } from '../auth/crypto.js';
7
+ import { findEditor, issueToken, consumeToken, createSession, deleteSession } from '../auth/store.js';
8
+ import { buildMagicLinkMessage, cloudflareSend } from '../email.js';
9
+ export function createAuthRoutes(config) {
10
+ const send = config.send ?? cloudflareSend;
11
+ /**
12
+ * POST /admin/auth/request. Looks the email up in the allowlist; on a match, issues a token
13
+ * and emails the confirmation link. The response is identical whether or not the email is
14
+ * allow-listed, so the endpoint never leaks membership.
15
+ */
16
+ async function requestAction(event) {
17
+ const env = event.platform?.env ?? {};
18
+ const origin = requireOrigin(env);
19
+ const db = requireDb(env);
20
+ const form = await event.request.formData();
21
+ const email = String(form.get('email') ?? '').trim().toLowerCase();
22
+ const editor = email ? await findEditor(db, email) : null;
23
+ if (editor) {
24
+ const token = generateToken();
25
+ const now = Date.now();
26
+ await issueToken(db, email, await hashToken(token), now + TOKEN_TTL_MS, now);
27
+ const link = `${origin}/admin/auth/confirm?token=${encodeURIComponent(token)}`;
28
+ await send(env, buildMagicLinkMessage({ to: email, branding: config.branding, link }));
29
+ }
30
+ return { sent: true };
31
+ }
32
+ /** GET /admin/login. Public. Carries the site name and an optional `?error` for the form. */
33
+ function loginLoad(event) {
34
+ return { siteName: config.branding.siteName, error: event.url.searchParams.get('error') };
35
+ }
36
+ /**
37
+ * GET /admin/auth/confirm. Renders the confirm page and consumes nothing; only the POST
38
+ * verifies. Sets Referrer-Policy: no-referrer so the token does not leak to a referrer.
39
+ */
40
+ function confirmLoad(event) {
41
+ event.setHeaders({ 'Referrer-Policy': 'no-referrer' });
42
+ return {
43
+ token: event.url.searchParams.get('token') ?? '',
44
+ siteName: config.branding.siteName,
45
+ error: event.url.searchParams.get('error'),
46
+ };
47
+ }
48
+ /**
49
+ * POST /admin/auth/confirm. Hashes the submitted token and consumes it atomically. A valid
50
+ * token yields the email; the handler creates a session, sets the cookie, and redirects to
51
+ * /admin. An invalid, replayed, or expired token redirects to the login page.
52
+ */
53
+ async function confirmAction(event) {
54
+ const db = requireDb(event.platform?.env ?? {});
55
+ const form = await event.request.formData();
56
+ const token = String(form.get('token') ?? '');
57
+ if (!token)
58
+ throw redirect(303, '/admin/login?error=expired');
59
+ const now = Date.now();
60
+ const email = await consumeToken(db, await hashToken(token), now);
61
+ if (!email)
62
+ throw redirect(303, '/admin/login?error=expired');
63
+ const id = generateSessionId();
64
+ await createSession(db, id, email, now + SESSION_TTL_MS, now);
65
+ event.cookies.set(COOKIE_NAME, id, {
66
+ path: '/',
67
+ httpOnly: true,
68
+ // Secure on HTTPS (every real deploy); off on local http dev so the cookie sticks.
69
+ secure: event.url.protocol === 'https:',
70
+ sameSite: 'lax',
71
+ maxAge: Math.floor(SESSION_TTL_MS / 1000),
72
+ });
73
+ throw redirect(303, '/admin');
74
+ }
75
+ /** POST /admin/auth/logout. Deletes the session row and clears the cookie. */
76
+ async function logoutAction(event) {
77
+ const db = requireDb(event.platform?.env ?? {});
78
+ const id = event.cookies.get(COOKIE_NAME);
79
+ if (id)
80
+ await deleteSession(db, id);
81
+ event.cookies.delete(COOKIE_NAME, { path: '/' });
82
+ throw redirect(303, '/admin/login');
83
+ }
84
+ return { loginLoad, requestAction, confirmLoad, confirmAction, logoutAction };
85
+ }
@@ -0,0 +1,80 @@
1
+ import { type GithubKeyEnv } from '../github/credentials.js';
2
+ import type { CairnRuntime, FrontmatterField } from '../content/types.js';
3
+ import type { Editor, Role } from '../auth/types.js';
4
+ /** A sidebar concept entry: just enough to render the nav without shipping validators to the client. */
5
+ export interface NavConcept {
6
+ id: string;
7
+ label: string;
8
+ }
9
+ /** The admin layout's data: site identity, the signed-in user, the nav, and the active path. */
10
+ export interface LayoutData {
11
+ siteName: string;
12
+ user: {
13
+ displayName: string;
14
+ role: Role;
15
+ };
16
+ concepts: NavConcept[];
17
+ pathname: string;
18
+ canManageEditors: boolean;
19
+ /** The nav menu's label when the site configures one; gates the Navigation nav entry. Null otherwise. */
20
+ navLabel: string | null;
21
+ }
22
+ /** One row in a concept's list view. */
23
+ export interface EntrySummary {
24
+ id: string;
25
+ title: string;
26
+ date: string | null;
27
+ draft: boolean;
28
+ }
29
+ /** The concept list view's data. */
30
+ export interface ListData {
31
+ conceptId: string;
32
+ label: string;
33
+ /** Posts carry a date in the new-entry form; pages do not (concept routing, spec §7.2). */
34
+ dated: boolean;
35
+ entries: EntrySummary[];
36
+ /** A listing failure degrades to an inline message rather than a thrown 500. */
37
+ error: string | null;
38
+ /** A create-form bounce error read from `?error`. */
39
+ formError: string | null;
40
+ }
41
+ /** The editor's data. `frontmatter` holds form-ready values (dates already `YYYY-MM-DD`). */
42
+ export interface EditData {
43
+ conceptId: string;
44
+ id: string;
45
+ label: string;
46
+ fields: FrontmatterField[];
47
+ frontmatter: Record<string, unknown>;
48
+ body: string;
49
+ title: string;
50
+ isNew: boolean;
51
+ saved: boolean;
52
+ error: string | null;
53
+ }
54
+ /** The structural event the content routes read; a real SvelteKit RequestEvent satisfies it. */
55
+ export interface ContentEvent {
56
+ url: URL;
57
+ params: Record<string, string>;
58
+ request: Request;
59
+ locals: {
60
+ editor?: Editor | null;
61
+ };
62
+ platform?: {
63
+ env?: GithubKeyEnv;
64
+ };
65
+ }
66
+ /** Injectable dependencies; tests stub the token mint to avoid signing a real key. */
67
+ export interface ContentRoutesDeps {
68
+ /** Mint a GitHub App installation token from the Worker env. Defaults to the real signer. */
69
+ mintToken?: (env: GithubKeyEnv) => Promise<string>;
70
+ }
71
+ export declare function createContentRoutes(runtime: CairnRuntime, deps?: ContentRoutesDeps): {
72
+ layoutLoad: (event: ContentEvent) => LayoutData;
73
+ indexRedirect: () => never;
74
+ listLoad: (event: ContentEvent) => Promise<ListData>;
75
+ createAction: (event: ContentEvent) => Promise<never>;
76
+ editLoad: (event: ContentEvent) => Promise<EditData>;
77
+ saveAction: (event: ContentEvent) => Promise<never>;
78
+ mintToken: (env: GithubKeyEnv) => Promise<string>;
79
+ };
80
+ //# sourceMappingURL=content-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/content-routes.ts"],"names":[],"mappings":"AAQA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAI7E,OAAO,KAAK,EAAE,YAAY,EAAqB,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAErD,wGAAwG;AACxG,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gGAAgG;AAChG,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;IAC1C,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,yGAAyG;IACzG,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,oCAAoC;AACpC,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,2FAA2F;IAC3F,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,gFAAgF;IAChF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qDAAqD;IACrD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,6FAA6F;AAC7F,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,gGAAgG;AAChG,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACnC,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC;CACnC;AAED,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,6FAA6F;IAC7F,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACpD;AAgBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,GAAE,iBAAsB;wBAK1D,YAAY,KAAG,UAAU;yBAa1B,KAAK;sBAqBA,YAAY,KAAG,OAAO,CAAC,QAAQ,CAAC;0BAqB5B,YAAY,KAAG,OAAO,CAAC,KAAK,CAAC;sBA+BjC,YAAY,KAAG,OAAO,CAAC,QAAQ,CAAC;wBAgC9B,YAAY,KAAG,OAAO,CAAC,KAAK,CAAC;qBA5I5C,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC;EAqLnD"}