@glw907/cairn-cms 0.4.0 → 0.5.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 (68) hide show
  1. package/README.md +4 -4
  2. package/dist/adapter.d.ts +10 -1
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/auth/config.d.ts +9 -9
  5. package/dist/auth/config.d.ts.map +1 -1
  6. package/dist/auth/config.js +5 -5
  7. package/dist/auth/guard.d.ts +1 -1
  8. package/dist/auth/guard.d.ts.map +1 -1
  9. package/dist/auth/guard.js +2 -2
  10. package/dist/carta.d.ts +1 -1
  11. package/dist/carta.d.ts.map +1 -1
  12. package/dist/components/AdminLayout.svelte +3 -3
  13. package/dist/components/AdminList.svelte +1 -1
  14. package/dist/components/ConfirmPage.svelte +2 -2
  15. package/dist/components/EditPage.svelte +5 -5
  16. package/dist/components/LoginPage.svelte +5 -5
  17. package/dist/email.js +4 -4
  18. package/dist/github.d.ts +22 -2
  19. package/dist/github.d.ts.map +1 -1
  20. package/dist/github.js +40 -5
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/dist/render/glyph.d.ts +6 -0
  25. package/dist/render/glyph.d.ts.map +1 -0
  26. package/dist/render/glyph.js +5 -0
  27. package/dist/render/index.d.ts +6 -0
  28. package/dist/render/index.d.ts.map +1 -0
  29. package/dist/render/index.js +8 -0
  30. package/dist/render/pipeline.d.ts +16 -0
  31. package/dist/render/pipeline.d.ts.map +1 -0
  32. package/dist/render/pipeline.js +29 -0
  33. package/dist/render/registry.d.ts +28 -0
  34. package/dist/render/registry.d.ts.map +1 -0
  35. package/dist/render/registry.js +11 -0
  36. package/dist/render/rehype-dispatch.d.ts +24 -0
  37. package/dist/render/rehype-dispatch.d.ts.map +1 -0
  38. package/dist/render/rehype-dispatch.js +86 -0
  39. package/dist/render/remark-directives.d.ts +4 -0
  40. package/dist/render/remark-directives.d.ts.map +1 -0
  41. package/dist/render/remark-directives.js +74 -0
  42. package/dist/sveltekit/index.d.ts +17 -2
  43. package/dist/sveltekit/index.d.ts.map +1 -1
  44. package/dist/sveltekit/index.js +33 -6
  45. package/dist/utils.d.ts +1 -1
  46. package/dist/utils.d.ts.map +1 -1
  47. package/dist/utils.js +2 -2
  48. package/package.json +15 -3
  49. package/src/lib/adapter.ts +12 -3
  50. package/src/lib/auth/config.ts +6 -6
  51. package/src/lib/auth/guard.ts +3 -3
  52. package/src/lib/carta.ts +2 -2
  53. package/src/lib/components/AdminLayout.svelte +3 -3
  54. package/src/lib/components/AdminList.svelte +1 -1
  55. package/src/lib/components/ConfirmPage.svelte +2 -2
  56. package/src/lib/components/EditPage.svelte +5 -5
  57. package/src/lib/components/LoginPage.svelte +5 -5
  58. package/src/lib/email.ts +4 -4
  59. package/src/lib/github.ts +38 -6
  60. package/src/lib/index.ts +1 -0
  61. package/src/lib/render/glyph.ts +14 -0
  62. package/src/lib/render/index.ts +8 -0
  63. package/src/lib/render/pipeline.ts +37 -0
  64. package/src/lib/render/registry.ts +36 -0
  65. package/src/lib/render/rehype-dispatch.ts +97 -0
  66. package/src/lib/render/remark-directives.ts +71 -0
  67. package/src/lib/sveltekit/index.ts +54 -13
  68. package/src/lib/utils.ts +2 -2
@@ -0,0 +1,28 @@
1
+ import type { Element } from 'hast';
2
+ /** A site component: how it inserts (editor) and how it renders (rehype). */
3
+ export interface ComponentDef {
4
+ /** Directive name, e.g. 'card' (matches `:::card`). */
5
+ name: string;
6
+ /** Palette label. */
7
+ label: string;
8
+ /** Palette description. */
9
+ description: string;
10
+ /** Markdown scaffold inserted at the cursor by the editor palette. */
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' }`). */
15
+ defaultIconByRole?: Record<string, string>;
16
+ }
17
+ export interface ComponentRegistry {
18
+ defs: ComponentDef[];
19
+ names: string[];
20
+ get(name: string): ComponentDef | undefined;
21
+ defaultIcon(name: string, role?: string): string | undefined;
22
+ }
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: {
26
+ components: ComponentDef[];
27
+ }): ComponentRegistry;
28
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,11 @@
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]));
5
+ return {
6
+ defs: input.components,
7
+ names: input.components.map((c) => c.name),
8
+ get: (name) => byName.get(name),
9
+ defaultIcon: (name, role) => (role ? byName.get(name)?.defaultIconByRole?.[role] : undefined),
10
+ };
11
+ }
@@ -0,0 +1,24 @@
1
+ import type { Root, Element, ElementContent } from 'hast';
2
+ import type { ComponentRegistry } from './registry';
3
+ export declare function isElement(node: ElementContent | undefined): node is Element;
4
+ export declare function strProp(node: Element, name: string): string | undefined;
5
+ /** Wrap a pre-built glyph in an ec-icon span; secondary role adds the modifier. */
6
+ export declare function iconSpan(glyphEl: Element, role?: string): Element;
7
+ /** A site's icon factory: turn a stamped icon name + role into a hast element. */
8
+ export type MakeIcon = (name: string, role?: string) => Element;
9
+ export declare function splitHead(node: Element, makeIcon?: MakeIcon): {
10
+ head: Element;
11
+ rest: ElementContent[];
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;
16
+ /** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
17
+ * text nodes so the bare list serializes without newlines. Returns that <ul>. */
18
+ export declare function markFirstList(children: ElementContent[]): Element | undefined;
19
+ /** 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
22
+ * 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
+ //# sourceMappingURL=rehype-dispatch.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,86 @@
1
+ import { h } from 'hastscript';
2
+ export function isElement(node) {
3
+ return !!node && node.type === 'element';
4
+ }
5
+ // hast Properties values are PropertyValue (string | number | boolean | array | null).
6
+ // Directive markers (dataIcon/dataRole/dataPrimitive) are always stamped as strings;
7
+ // this reads them back with that guarantee instead of casting at each call site.
8
+ export function strProp(node, name) {
9
+ const value = node.properties?.[name];
10
+ return typeof value === 'string' ? value : undefined;
11
+ }
12
+ /** Wrap a pre-built glyph in an ec-icon span; secondary role adds the modifier. */
13
+ export function iconSpan(glyphEl, role) {
14
+ const className = role === 'secondary' ? ['ec-icon', 'ec-icon-secondary'] : ['ec-icon'];
15
+ return h('span', { className }, [glyphEl]);
16
+ }
17
+ // Pull the section's <h2> out, retag it .card-title, and build the .ec-head row
18
+ // (optional icon + heading). Returns the head plus the remaining body children.
19
+ // `makeIcon` (site-supplied) turns the stamped data-icon into an element; omit it
20
+ // for a head with no icon.
21
+ export function splitHead(node, makeIcon) {
22
+ const children = node.children;
23
+ const i = children.findIndex((c) => isElement(c) && c.tagName === 'h2');
24
+ const h2 = children[i];
25
+ h2.properties = { ...h2.properties, className: ['card-title'] };
26
+ const rest = children.filter((_, j) => j !== i);
27
+ const icon = strProp(node, 'dataIcon');
28
+ const role = strProp(node, 'dataRole');
29
+ const headKids = [];
30
+ if (makeIcon && icon)
31
+ headKids.push(makeIcon(icon, role));
32
+ headKids.push(h2);
33
+ return { head: h('div', { className: ['ec-head'] }, headKids), rest };
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)]);
42
+ }
43
+ /** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
44
+ * text nodes so the bare list serializes without newlines. Returns that <ul>. */
45
+ export function markFirstList(children) {
46
+ const ul = children.find((c) => isElement(c) && c.tagName === 'ul');
47
+ if (ul) {
48
+ ul.properties = { ...ul.properties, className: ['ec-grid'] };
49
+ ul.children = ul.children.filter((c) => !(c.type === 'text' && /^\s*$/.test(c.value)));
50
+ }
51
+ return ul;
52
+ }
53
+ // 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.
55
+ function transformChildren(children, registry) {
56
+ return children.map((c) => {
57
+ if (isElement(c) && c.properties?.dataPrimitive)
58
+ return transformNode(c, registry);
59
+ if (isElement(c))
60
+ c.children = transformChildren(c.children, registry);
61
+ return c;
62
+ });
63
+ }
64
+ function transformNode(node, registry, rise) {
65
+ node.children = transformChildren(node.children, registry);
66
+ const name = strProp(node, 'dataPrimitive');
67
+ const def = name ? registry.get(name) : undefined;
68
+ return def ? def.build(node, rise) : node;
69
+ }
70
+ /** 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
73
+ * content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
74
+ export function rehypeDispatch(registry, rise) {
75
+ return (tree) => {
76
+ let idx = 0;
77
+ tree.children = tree.children.map((child) => {
78
+ if (isElement(child) && child.properties?.dataPrimitive) {
79
+ return transformNode(child, registry, rise ? rise(idx++) : undefined);
80
+ }
81
+ if (isElement(child))
82
+ child.children = transformChildren(child.children, registry);
83
+ return child;
84
+ });
85
+ };
86
+ }
@@ -0,0 +1,4 @@
1
+ import type { Root } from 'mdast';
2
+ import type { ComponentRegistry } from './registry';
3
+ export declare function remarkDirectiveStamp(registry: ComponentRegistry): (tree: Root) => void;
4
+ //# sourceMappingURL=remark-directives.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,74 @@
1
+ import { visit } from 'unist-util-visit';
2
+ // Reconstruct a directive's authored attribute block (`{#id .class key="value"}`).
3
+ // Accidental prose directives carry none, so this is almost always empty.
4
+ function serializeAttributes(attributes) {
5
+ if (!attributes)
6
+ return '';
7
+ const tokens = [];
8
+ for (const [key, value] of Object.entries(attributes)) {
9
+ if (value == null)
10
+ tokens.push(key);
11
+ else if (key === 'id')
12
+ tokens.push(`#${value}`);
13
+ else if (key === 'class')
14
+ for (const c of value.split(/\s+/).filter(Boolean))
15
+ tokens.push(`.${c}`);
16
+ else
17
+ tokens.push(`${key}="${value}"`);
18
+ }
19
+ return tokens.length ? `{${tokens.join(' ')}}` : '';
20
+ }
21
+ // The vocabulary is container-only (`:::name`). A text directive (`:name`) or
22
+ // leaf directive (`::name`) is therefore always an accidental colon in prose
23
+ // ("4:00", "9:30", "ratio 16:9") that micromark tokenized as a directive.
24
+ // Restore it to its literal source text so prose renders verbatim.
25
+ function restoreLiteral(node) {
26
+ const marker = node.type === 'leafDirective' ? '::' : ':';
27
+ const attrs = serializeAttributes(node.attributes);
28
+ if (node.children.length === 0) {
29
+ return [{ type: 'text', value: marker + node.name + attrs }];
30
+ }
31
+ const open = { type: 'text', value: `${marker}${node.name}[` };
32
+ const close = { type: 'text', value: `]${attrs}` };
33
+ return [open, ...node.children, close];
34
+ }
35
+ // Stamp each registered container directive with data-* markers carrying its
36
+ // component name, icon, and role. No structure is built here; the rehype
37
+ // dispatcher rewrites the marked elements once their children are hast.
38
+ // Text and leaf directives are restored to literal text (accidental prose colons).
39
+ export function remarkDirectiveStamp(registry) {
40
+ const known = new Set(registry.names);
41
+ return (tree) => {
42
+ visit(tree, 'containerDirective', (node) => {
43
+ if (!known.has(node.name))
44
+ return;
45
+ const attrs = node.attributes ?? {};
46
+ const role = attrs.role || undefined;
47
+ let icon = attrs.icon || undefined;
48
+ if (!icon && role)
49
+ icon = registry.defaultIcon(node.name, role);
50
+ const properties = { dataPrimitive: node.name };
51
+ if (icon)
52
+ properties.dataIcon = icon;
53
+ if (role)
54
+ properties.dataRole = role;
55
+ const data = node.data ?? (node.data = {});
56
+ data.hName = 'div';
57
+ data.hProperties = properties;
58
+ });
59
+ visit(tree, ['textDirective', 'leafDirective'], (node, index, parent) => {
60
+ if (!parent || index == null)
61
+ return;
62
+ const literal = restoreLiteral(node);
63
+ if (node.type === 'leafDirective') {
64
+ // Leaf directives sit at block level; wrap the restored text in a paragraph.
65
+ const paragraph = { type: 'paragraph', children: literal };
66
+ parent.children.splice(index, 1, paragraph);
67
+ }
68
+ else {
69
+ parent.children.splice(index, 1, ...literal);
70
+ }
71
+ return index;
72
+ });
73
+ };
74
+ }
@@ -1,7 +1,7 @@
1
1
  import type { CairnUser } from '../auth/guard';
2
2
  import { type RepoFile } from '../github';
3
3
  import { type CairnAdapter, type CairnField } from '../adapter';
4
- /** The `platform.env` bindings the content routes read. All optional the handlers guard. */
4
+ /** The `platform.env` bindings the content routes read. All optional; the handlers guard. */
5
5
  export interface AdminEnv {
6
6
  GITHUB_APP_ID?: string;
7
7
  GITHUB_APP_INSTALLATION_ID?: string;
@@ -19,7 +19,7 @@ export interface AdminLayoutData {
19
19
  }
20
20
  /**
21
21
  * Branding + session for every admin page. `siteName` flows from the adapter without pulling
22
- * its plugin graph into client bundles the import stays server-side in the layout load.
22
+ * its plugin graph into client bundles; the import stays server-side in the layout load.
23
23
  * `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
24
24
  * (those kit virtual modules have no types outside a kit app, so they can't live in the
25
25
  * package); reading `event.url` here also opts the layout load into rerunning on navigation.
@@ -65,5 +65,20 @@ export declare function saveCommit(event: PlatformEvent & {
65
65
  user: CairnUser | null;
66
66
  };
67
67
  }, adapter: CairnAdapter): Promise<never>;
68
+ export interface HealthData {
69
+ ok: boolean;
70
+ checks: {
71
+ githubAppSigning: {
72
+ ok: boolean;
73
+ detail?: string;
74
+ };
75
+ };
76
+ }
77
+ /**
78
+ * Deploy-time health check (M2): signs a dummy App JWT to prove the GitHub App key loads and
79
+ * the PKCS#1→PKCS#8 conversion still works, before an editor hits it on save. Behind the
80
+ * `/admin` guard (signed-in editors only); returns ok/fail with no secret in the body.
81
+ */
82
+ export declare function healthLoad(event: PlatformEvent): Promise<HealthData>;
68
83
  export {};
69
84
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAwD,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEhG,OAAO,EAAuC,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAErG,8FAA8F;AAC9F,MAAM,WAAW,QAAQ;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,QAAQ,CAAA;KAAE,CAAC;CAC/B;AA2BD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE;IAAE,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,EACvD,OAAO,EAAE,YAAY,GACpB,eAAe,CAEjB;AAID,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,4FAA4F;AAC5F,wBAAsB,aAAa,CACjC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,WAAW,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC,CAajD;AAID,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,EACzE,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,QAAQ,CAAC,CAyBnB;AAID,wBAAsB,UAAU,CAC9B,KAAK,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,EAC/E,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,KAAK,CAAC,CA0ChB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAOL,KAAK,QAAQ,EACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAuC,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAErG,6FAA6F;AAC7F,MAAM,WAAW,QAAQ;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,QAAQ,CAAA;KAAE,CAAC;CAC/B;AA2BD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE;IAAE,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,EACvD,OAAO,EAAE,YAAY,GACpB,eAAe,CAEjB;AAID,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,4FAA4F;AAC5F,wBAAsB,aAAa,CACjC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,WAAW,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC,CAajD;AAID,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,EACzE,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,QAAQ,CAAC,CAyBnB;AAID,wBAAsB,UAAU,CAC9B,KAAK,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,EAC/E,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,KAAK,CAAC,CAoDhB;AAID,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE;QAAE,gBAAgB,EAAE;YAAE,EAAE,EAAE,OAAO,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CAChE;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAS1E"}
@@ -2,22 +2,22 @@
2
2
  // route files are thin shims (`export const load = (event) => editLoad(event, cairn)`).
3
3
  //
4
4
  // SvelteKit's filesystem routing requires the route *files* to live in each site's
5
- // `src/routes/`, but their bodies are identical across sites only the adapter differs.
5
+ // `src/routes/`, but their bodies are identical across sites. Only the adapter differs.
6
6
  // These functions take the SvelteKit event (typed structurally, to avoid depending on the
7
7
  // site-generated `App.*` ambient types) plus the site `CairnAdapter`, and throw
8
8
  // `redirect`/`error` from `@sveltejs/kit` (a peer dependency, so the thrown objects share
9
- // class identity with the host's runtime else the redirect 500s). Auth/session/manage-editors
9
+ // class identity with the host's runtime; otherwise the redirect 500s). Auth/session/manage-editors
10
10
  // logic lives under `@glw907/cairn-cms/auth`; this module is content-only (list/edit/save).
11
11
  import { redirect, error } from '@sveltejs/kit';
12
12
  import matter from 'gray-matter';
13
- import { listMarkdown, readRaw, commitFile, installationToken } from '../github';
13
+ import { listMarkdown, readRaw, commitFile, installationToken, signingSelfTest, CommitConflictError, } from '../github';
14
14
  import { serializeMarkdown } from '../content';
15
15
  import { findCollection, frontmatterFromForm } from '../adapter';
16
16
  /**
17
17
  * Mint a GitHub App installation token for *reads* when the App is configured, else undefined
18
18
  * (reads then fall back to anonymous). Authenticated reads get the 5000/hr limit; anonymous
19
19
  * reads share GitHub's 60/hr-per-IP budget across Cloudflare's egress IPs, so they 403 in prod.
20
- * A mint failure degrades gracefully to anonymous rather than 500ing unlike the commit path,
20
+ * A mint failure degrades gracefully to anonymous rather than 500ing. Unlike the commit path,
21
21
  * where a missing App is fatal, a read can still succeed unauthenticated.
22
22
  */
23
23
  async function readToken(env) {
@@ -38,7 +38,7 @@ async function readToken(env) {
38
38
  }
39
39
  /**
40
40
  * Branding + session for every admin page. `siteName` flows from the adapter without pulling
41
- * its plugin graph into client bundles the import stays server-side in the layout load.
41
+ * its plugin graph into client bundles; the import stays server-side in the layout load.
42
42
  * `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
43
43
  * (those kit virtual modules have no types outside a kit app, so they can't live in the
44
44
  * package); reading `event.url` here also opts the layout load into rerunning on navigation.
@@ -117,6 +117,33 @@ export async function saveCommit(event, adapter) {
117
117
  installationId: env.GITHUB_APP_INSTALLATION_ID,
118
118
  privateKeyB64: env.GITHUB_APP_PRIVATE_KEY_B64,
119
119
  });
120
- await commitFile(adapter.backend, `${collection.dir}/${id}.md`, markdown, { message: `Update ${collection.label.toLowerCase()}: ${id}`, author: { name: user.name, email: user.email } }, token);
120
+ try {
121
+ await commitFile(adapter.backend, `${collection.dir}/${id}.md`, markdown, { message: `Update ${collection.label.toLowerCase()}: ${id}`, author: { name: user.name, email: user.email } }, token);
122
+ }
123
+ catch (err) {
124
+ // Concurrent-edit 409 (C3): fail safe. Bounce back with a reload prompt; the editor reloads
125
+ // the current version and reapplies. Any other error is unexpected, so rethrow.
126
+ if (err instanceof CommitConflictError) {
127
+ const message = 'This file changed since you opened it. Reload and reapply your edits.';
128
+ throw redirect(303, `/admin/edit/${type}/${id}?error=${encodeURIComponent(message)}`);
129
+ }
130
+ throw err;
131
+ }
121
132
  throw redirect(303, `/admin/edit/${type}/${id}?saved=1`);
122
133
  }
134
+ /**
135
+ * Deploy-time health check (M2): signs a dummy App JWT to prove the GitHub App key loads and
136
+ * the PKCS#1→PKCS#8 conversion still works, before an editor hits it on save. Behind the
137
+ * `/admin` guard (signed-in editors only); returns ok/fail with no secret in the body.
138
+ */
139
+ export async function healthLoad(event) {
140
+ const env = event.platform?.env;
141
+ let githubAppSigning;
142
+ if (env?.GITHUB_APP_ID && env.GITHUB_APP_PRIVATE_KEY_B64) {
143
+ githubAppSigning = await signingSelfTest(env.GITHUB_APP_ID, env.GITHUB_APP_PRIVATE_KEY_B64);
144
+ }
145
+ else {
146
+ githubAppSigning = { ok: false, detail: 'GitHub App not configured' };
147
+ }
148
+ return { ok: githubAppSigning.ok, checks: { githubAppSigning } };
149
+ }
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- /** Encode bytes as unpadded base64url (RFC 4648 §5) the JWT/token wire format. */
1
+ /** Encode bytes as unpadded base64url (RFC 4648 §5), the JWT/token wire format. */
2
2
  export declare function bytesToB64url(bytes: Uint8Array): string;
3
3
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.ts"],"names":[],"mappings":"AAOA,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAGvD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.ts"],"names":[],"mappings":"AAOA,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAGvD"}
package/dist/utils.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // cairn-core: internal encoding helpers shared across modules.
2
2
  //
3
- // Deliberately NOT re-exported from index.ts these are implementation details of the
3
+ // Deliberately NOT re-exported from index.ts. These are implementation details of the
4
4
  // auth/github crypto, not part of the public API (auth.ts signs tokens, github.ts builds
5
5
  // the App JWT; both need base64url). Keeping them here stops bytesToB64url leaking through
6
6
  // the `export *` barrel.
7
- /** Encode bytes as unpadded base64url (RFC 4648 §5) the JWT/token wire format. */
7
+ /** Encode bytes as unpadded base64url (RFC 4648 §5), the JWT/token wire format. */
8
8
  export function bytesToB64url(bytes) {
9
9
  const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join('');
10
10
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glw907/cairn-cms",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -86,7 +86,20 @@
86
86
  "svelte": "^5.0.0"
87
87
  },
88
88
  "dependencies": {
89
- "gray-matter": "^4"
89
+ "@types/hast": "^3.0.4",
90
+ "@types/mdast": "^4.0.4",
91
+ "gray-matter": "^4",
92
+ "hastscript": "^9.0.1",
93
+ "mdast-util-directive": "^3.1.0",
94
+ "rehype-raw": "^7.0.0",
95
+ "rehype-slug": "^6.0.0",
96
+ "rehype-stringify": "^10.0.1",
97
+ "remark-directive": "^4.0.0",
98
+ "remark-gfm": "^4",
99
+ "remark-parse": "^11.0.0",
100
+ "remark-rehype": "^11.1.2",
101
+ "unified": "^11.0.5",
102
+ "unist-util-visit": "^5.1.0"
90
103
  },
91
104
  "devDependencies": {
92
105
  "@better-auth/cli": "^1.4.21",
@@ -103,7 +116,6 @@
103
116
  "svelte": "^5",
104
117
  "svelte-check": "^4",
105
118
  "typescript": "^6.0.3",
106
- "unified": "^11.0.5",
107
119
  "vitest": "^4.1.6"
108
120
  }
109
121
  }
@@ -3,11 +3,12 @@
3
3
  // This is the single seam that lets one admin surface serve different designs. A site
4
4
  // supplies a `CairnAdapter` (see `src/lib/cairn.config.ts`) describing its backend repo,
5
5
  // its editable collections (folder + form fields + frontmatter validator), and its preview
6
- // plugin set. cairn-core never hard-codes a collection, tag, or directive it reads them
6
+ // plugin set. cairn-core never hard-codes a collection, tag, or directive; it reads them
7
7
  // from the adapter. Field descriptors are plain data so a load function can hand them to
8
- // the editor form across the serverclient boundary.
8
+ // the editor form across the server-to-client boundary.
9
9
  import type { PreviewPlugins } from './carta';
10
10
  import type { RepoRef } from './github';
11
+ import type { ComponentRegistry } from './render';
11
12
 
12
13
  interface FieldBase {
13
14
  /** Frontmatter key and form input name. */
@@ -63,13 +64,21 @@ export interface CairnCollection {
63
64
  export interface CairnAdapter {
64
65
  /** Branding + magic-link email copy. */
65
66
  siteName: string;
66
- /** From: address for magic-link email a domain-authenticated sender. */
67
+ /** From: address for magic-link email (must be a domain-authenticated sender). */
67
68
  sender: string;
68
69
  /** The repository the admin reads content from and commits to. */
69
70
  backend: RepoRef;
70
71
  /** Site plugin set for the Carta preview (parity with the live render). */
71
72
  preview: PreviewPlugins;
72
73
  collections: CairnCollection[];
74
+ /**
75
+ * The site's component registry: the single declaration of its directive
76
+ * components (R10a). Rendering parity already flows through `preview`; this
77
+ * exposes the same registry so the editor's insert-component palette can read
78
+ * `registry.defs`. Optional: a site with no rich components (e.g. 907.life) may
79
+ * omit it or supply an empty registry.
80
+ */
81
+ registry?: ComponentRegistry;
73
82
  }
74
83
 
75
84
  /** Look up a collection by its route segment, or undefined if the segment is unknown. */
@@ -1,5 +1,5 @@
1
1
  // cairn-core: the better-auth instance. Auth is engine code (engine-fat rule), so the whole
2
- // config Drizzle/D1 adapter, magic-link (POST-confirm-shaped send), admin roles — lives here.
2
+ // config lives here: Drizzle/D1 adapter, magic-link (POST-confirm-shaped send), admin roles.
3
3
  // Instantiated PER REQUEST in hooks.server.ts (the D1 binding is request-scoped); the factory
4
4
  // is cheap (no I/O at construction).
5
5
  import { betterAuth } from 'better-auth';
@@ -14,7 +14,7 @@ import * as schema from './schema';
14
14
 
15
15
  // Two-tier roles on the admin plugin's access-control system: `owner` holds every admin
16
16
  // statement (manage editors, revoke sessions); `editor` holds none (content-only). `adminRoles`
17
- // must name a role defined here, so owner not the plugin's built-in `admin` is the gate.
17
+ // must name a role defined here, so owner (not the plugin's built-in `admin`) is the gate.
18
18
  const ac = createAccessControl(defaultStatements);
19
19
  const owner = ac.newRole(defaultStatements);
20
20
  const editor = ac.newRole({});
@@ -37,17 +37,17 @@ export interface AuthBranding {
37
37
  sender: string;
38
38
  }
39
39
 
40
- /** The drizzle adapter result `betterAuth` consumes — the same provider/schema everywhere. */
40
+ /** The drizzle adapter result `betterAuth` consumes (same provider/schema everywhere). */
41
41
  type DrizzleDb = Parameters<typeof drizzleAdapter>[0];
42
42
 
43
43
  /**
44
44
  * The shared better-auth config. Kept separate from `createAuth` so the test harness can run
45
45
  * the EXACT plugin set (allowlist semantics, expiry, POST-confirm send) over an in-memory
46
- * SQLite instead of D1. `disableSignUp:true` makes the `user` table the editor allowlist
46
+ * SQLite instead of D1. `disableSignUp:true` makes the `user` table the editor allowlist:
47
47
  * magic-link never auto-creates, so the only way in is the owner-gated admin `createUser`
48
48
  * (see auth/admins.ts). `adminRoles:['owner']` lets owners (not the default `admin` role)
49
49
  * drive the admin API. Tokens are stored hashed and consumed atomically on first verify
50
- * (better-auth GHSA-hc7v-rggr-4hvx) single-use by construction (C1).
50
+ * (better-auth GHSA-hc7v-rggr-4hvx), single-use by construction (C1).
51
51
  */
52
52
  export function buildAuth(opts: {
53
53
  database: DrizzleDb;
@@ -70,7 +70,7 @@ export function buildAuth(opts: {
70
70
  sendMagicLink: async ({ email, token }, ctx) => {
71
71
  // Allowlist gate: better-auth always fires this callback (even for unknown emails, to
72
72
  // avoid enumeration) and only blocks user creation at verify. So gate the actual send
73
- // here never email a non-editor. The login UI shows neutral copy either way, so this
73
+ // here. Never email a non-editor. The login UI shows neutral copy either way, so this
74
74
  // leaks nothing; it just stops strangers receiving a dead link.
75
75
  const existing = await ctx?.context.internalAdapter.findUserByEmail(email);
76
76
  if (!existing?.user) return;
@@ -1,10 +1,10 @@
1
1
  // cairn-core: server-side auth helpers the site route shims delegate to. Each takes the
2
- // SvelteKit event (typed structurally, so the package never depends on a site's generated
3
- // `App.*` ambient types) plus the per-request `Auth` from `locals`.
2
+ // SvelteKit event, typed structurally so the package never depends on a site's generated
3
+ // `App.*` ambient types, plus the per-request `Auth` from `locals`.
4
4
  import { redirect } from '@sveltejs/kit';
5
5
  import type { Auth } from './config';
6
6
 
7
- /** The session shape the whole admin reads layout, guards, content fns, manage-editors. */
7
+ /** The session shape the whole admin reads: layout, guards, content fns, manage-editors. */
8
8
  export interface CairnUser {
9
9
  id: string;
10
10
  email: string;
package/src/lib/carta.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // cairn-core: pure Carta options/transformer wiring for render-only preview.
2
2
  //
3
- // Plugins are passed in, not imported that seam is what the Pass D adapter formalises.
3
+ // Plugins are passed in rather than imported; that seam is what the Pass D adapter formalises.
4
4
  // No `carta-md` import: its index re-exports Svelte components that the node test env
5
5
  // can't load. The Svelte component calls `new Carta(previewCartaOptions(...))` directly.
6
6
  import type { Pluggable, Processor } from 'unified';
@@ -37,7 +37,7 @@ export function previewTransformers({ remarkPlugins, rehypePlugins }: PreviewPlu
37
37
  return [...phase(remarkPlugins, 'remark'), ...phase(rehypePlugins, 'rehype')];
38
38
  }
39
39
 
40
- /** Minimal Options subset we populate avoids importing carta-md (Svelte re-exports). */
40
+ /** Minimal Options subset we populate (avoids importing carta-md, which re-exports Svelte components). */
41
41
  interface PreviewCartaOptions {
42
42
  sanitizer: false;
43
43
  rehypeOptions: { allowDangerousHtml: boolean };
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  // Neutral admin chrome, shared across sites so the tool looks identical everywhere (only the
3
3
  // adapter's siteName varies). When signed in it's a responsive DaisyUI drawer+navbar shell
4
- // (`drawer lg:drawer-open` sidebar pinned on desktop, slide-over + hamburger on mobile),
4
+ // (`drawer lg:drawer-open`, sidebar pinned on desktop, slide-over + hamburger on mobile),
5
5
  // patterned on scosman/CMSaasStarter's `(admin)/(menu)` layout. The nav is data-driven and
6
6
  // role-gated, so a new surface is one entry in `nav` (plus its route + component). Signed out
7
7
  // (the login page lives under this layout) it falls back to a minimal centered shell.
@@ -22,7 +22,7 @@
22
22
  label: string;
23
23
  icon: Snippet;
24
24
  active: boolean;
25
- /** Owner-only surface hidden from regular editors. */
25
+ /** Owner-only surface; hidden from regular editors. */
26
26
  owner?: boolean;
27
27
  }
28
28
 
@@ -73,7 +73,7 @@
73
73
  <input id="admin-drawer" type="checkbox" class="drawer-toggle" />
74
74
 
75
75
  <div class="drawer-content">
76
- <!-- Mobile top bar the desktop sidebar replaces this at lg. -->
76
+ <!-- Mobile top bar; the desktop sidebar replaces this at lg. -->
77
77
  <div class="navbar bg-base-100 lg:hidden">
78
78
  <div class="flex-1">
79
79
  <span class="px-2 text-xl font-bold">{data.siteName} CMS</span>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  // The /admin content list: every collection's files, linking into the editor. Data comes
3
3
  // from `adminListLoad` (collections) merged with `adminLayoutLoad` (siteName). The shell
4
- // (AdminLayout) owns the chrome site title, signed-in identity, nav, sign out so this
4
+ // (AdminLayout) owns the chrome (site title, signed-in identity, nav, sign out), so this
5
5
  // page renders only the content body.
6
6
  import type { AdminCollectionList } from '../sveltekit';
7
7
 
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
- // The scanner-safe confirm surface (C2). A GET renders this static page nothing is consumed.
3
- // The token rides in a hidden field; only the explicit form POST (the route's default action
2
+ // The scanner-safe confirm surface (C2). A GET renders this static page and consumes nothing.
3
+ // The token rides in a hidden field; only the explicit form POST (the route's default action,
4
4
  // confirmSignIn) verifies it. Mail scanners GET URLs but don't submit forms, so prefetch can't
5
5
  // burn the link. JS-free by design.
6
6
  interface Props {
@@ -13,21 +13,21 @@
13
13
 
14
14
  // Body is editable state; the Carta editor's preview runs the exact site plugin set, so it
15
15
  // matches the live page. A hidden input carries the current value into the form.
16
- // svelte-ignore state_referenced_locally seeding from the initial load is intended.
16
+ // svelte-ignore state_referenced_locally (seeding from the initial load is intended)
17
17
  let body = $state(data.body);
18
18
 
19
- // svelte-ignore state_referenced_locally the preview plugin set is fixed for the load.
19
+ // svelte-ignore state_referenced_locally (the preview plugin set is fixed for the load)
20
20
  const carta = new Carta(previewCartaOptions(preview));
21
21
 
22
22
  // Carta's MarkdownEditor must not render on the worker (it pulls Shiki). onMount fires only
23
- // in the browser, so SSR renders the plain textarea and the client swaps in the editor
24
- // the kit-free equivalent of the per-site route's `$app/environment` `browser` guard.
23
+ // in the browser, so SSR renders the plain textarea and the client swaps in the editor.
24
+ // This is the kit-free equivalent of the per-site route's `$app/environment` `browser` guard.
25
25
  let mounted = $state(false);
26
26
  onMount(() => {
27
27
  mounted = true;
28
28
  });
29
29
 
30
- // svelte-ignore state_referenced_locally form defaults from the initial load.
30
+ // svelte-ignore state_referenced_locally (form defaults from the initial load)
31
31
  const fm = data.frontmatter as Record<string, unknown>;
32
32
 
33
33
  function fmString(key: string): string {