@glw907/cairn-cms 0.5.0 → 0.6.0-rc.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 (216) 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 -108
  11. package/dist/components/AdminLayout.svelte.d.ts +14 -9
  12. package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
  13. package/dist/components/ComponentPalette.svelte +50 -0
  14. package/dist/components/ComponentPalette.svelte.d.ts +16 -0
  15. package/dist/components/ComponentPalette.svelte.d.ts.map +1 -0
  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 +160 -103
  23. package/dist/components/EditPage.svelte.d.ts +17 -7
  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 +24 -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 +138 -0
  35. package/dist/components/NavTree.svelte.d.ts +17 -0
  36. package/dist/components/NavTree.svelte.d.ts.map +1 -0
  37. package/dist/components/cairn-admin.css +42 -0
  38. package/dist/components/index.d.ts +5 -2
  39. package/dist/components/index.d.ts.map +1 -1
  40. package/dist/components/index.js +7 -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 -6
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +21 -8
  80. package/dist/nav/site-config.d.ts +50 -0
  81. package/dist/nav/site-config.d.ts.map +1 -0
  82. package/dist/nav/site-config.js +100 -0
  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 +3 -3
  89. package/dist/render/pipeline.d.ts.map +1 -1
  90. package/dist/render/pipeline.js +4 -4
  91. package/dist/render/registry.d.ts +6 -4
  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 +1 -1
  95. package/dist/render/rehype-dispatch.d.ts.map +1 -1
  96. package/dist/render/remark-directives.d.ts +1 -1
  97. package/dist/render/remark-directives.d.ts.map +1 -1
  98. package/dist/render/sanitize.d.ts +8 -0
  99. package/dist/render/sanitize.d.ts.map +1 -0
  100. package/dist/render/sanitize.js +26 -0
  101. package/dist/sveltekit/auth-routes.d.ts +23 -0
  102. package/dist/sveltekit/auth-routes.d.ts.map +1 -0
  103. package/dist/sveltekit/auth-routes.js +85 -0
  104. package/dist/sveltekit/content-routes.d.ts +80 -0
  105. package/dist/sveltekit/content-routes.d.ts.map +1 -0
  106. package/dist/sveltekit/content-routes.js +183 -0
  107. package/dist/sveltekit/editors-routes.d.ts +24 -0
  108. package/dist/sveltekit/editors-routes.d.ts.map +1 -0
  109. package/dist/sveltekit/editors-routes.js +73 -0
  110. package/dist/sveltekit/guard.d.ts +9 -0
  111. package/dist/sveltekit/guard.d.ts.map +1 -0
  112. package/dist/sveltekit/guard.js +43 -0
  113. package/dist/sveltekit/health.d.ts +19 -0
  114. package/dist/sveltekit/health.d.ts.map +1 -0
  115. package/dist/sveltekit/health.js +12 -0
  116. package/dist/sveltekit/index.d.ts +9 -83
  117. package/dist/sveltekit/index.d.ts.map +1 -1
  118. package/dist/sveltekit/index.js +8 -149
  119. package/dist/sveltekit/nav-routes.d.ts +30 -0
  120. package/dist/sveltekit/nav-routes.d.ts.map +1 -0
  121. package/dist/sveltekit/nav-routes.js +103 -0
  122. package/dist/sveltekit/types.d.ts +32 -0
  123. package/dist/sveltekit/types.d.ts.map +1 -0
  124. package/dist/sveltekit/types.js +1 -0
  125. package/package.json +38 -58
  126. package/src/lib/auth/crypto.ts +37 -0
  127. package/src/lib/auth/store.ts +158 -0
  128. package/src/lib/auth/types.ts +27 -0
  129. package/src/lib/components/AdminLayout.svelte +58 -108
  130. package/src/lib/components/ComponentPalette.svelte +50 -0
  131. package/src/lib/components/ConceptList.svelte +81 -0
  132. package/src/lib/components/ConfirmPage.svelte +23 -20
  133. package/src/lib/components/EditPage.svelte +160 -103
  134. package/src/lib/components/LoginPage.svelte +42 -52
  135. package/src/lib/components/ManageEditors.svelte +81 -0
  136. package/src/lib/components/MarkdownEditor.svelte +81 -0
  137. package/src/lib/components/NavTree.svelte +138 -0
  138. package/src/lib/components/cairn-admin.css +42 -0
  139. package/src/lib/components/index.ts +7 -4
  140. package/src/lib/content/compose.ts +39 -0
  141. package/src/lib/content/concepts.ts +57 -0
  142. package/src/lib/content/frontmatter.ts +71 -0
  143. package/src/lib/content/ids.ts +38 -0
  144. package/src/lib/content/types.ts +235 -0
  145. package/src/lib/content/validate.ts +51 -0
  146. package/src/lib/email.ts +52 -38
  147. package/src/lib/env.ts +32 -0
  148. package/src/lib/github/credentials.ts +27 -0
  149. package/src/lib/github/repo.ts +138 -0
  150. package/src/lib/github/signing.ts +97 -0
  151. package/src/lib/github/types.ts +46 -0
  152. package/src/lib/index.ts +86 -8
  153. package/src/lib/nav/site-config.ts +124 -0
  154. package/src/lib/render/glyph.ts +6 -6
  155. package/src/lib/render/index.ts +6 -6
  156. package/src/lib/render/pipeline.ts +22 -22
  157. package/src/lib/render/registry.ts +33 -26
  158. package/src/lib/render/rehype-dispatch.ts +47 -47
  159. package/src/lib/render/remark-directives.ts +46 -46
  160. package/src/lib/render/sanitize.ts +27 -0
  161. package/src/lib/sveltekit/auth-routes.ts +107 -0
  162. package/src/lib/sveltekit/content-routes.ts +261 -0
  163. package/src/lib/sveltekit/editors-routes.ts +82 -0
  164. package/src/lib/sveltekit/guard.ts +47 -0
  165. package/src/lib/sveltekit/health.ts +24 -0
  166. package/src/lib/sveltekit/index.ts +19 -235
  167. package/src/lib/sveltekit/nav-routes.ts +139 -0
  168. package/src/lib/sveltekit/types.ts +33 -0
  169. package/dist/adapter.d.ts +0 -69
  170. package/dist/adapter.d.ts.map +0 -1
  171. package/dist/adapter.js +0 -30
  172. package/dist/auth/admins.d.ts +0 -33
  173. package/dist/auth/admins.d.ts.map +0 -1
  174. package/dist/auth/admins.js +0 -90
  175. package/dist/auth/config.d.ts +0 -2097
  176. package/dist/auth/config.d.ts.map +0 -1
  177. package/dist/auth/config.js +0 -78
  178. package/dist/auth/guard.d.ts +0 -34
  179. package/dist/auth/guard.d.ts.map +0 -1
  180. package/dist/auth/guard.js +0 -47
  181. package/dist/auth/index.d.ts +0 -4
  182. package/dist/auth/index.d.ts.map +0 -1
  183. package/dist/auth/index.js +0 -6
  184. package/dist/auth/schema.d.ts +0 -750
  185. package/dist/auth/schema.d.ts.map +0 -1
  186. package/dist/auth/schema.js +0 -93
  187. package/dist/carta.d.ts +0 -39
  188. package/dist/carta.d.ts.map +0 -1
  189. package/dist/carta.js +0 -30
  190. package/dist/components/AdminList.svelte +0 -33
  191. package/dist/components/AdminList.svelte.d.ts +0 -10
  192. package/dist/components/AdminList.svelte.d.ts.map +0 -1
  193. package/dist/components/ManageAdmins.svelte +0 -84
  194. package/dist/components/ManageAdmins.svelte.d.ts +0 -10
  195. package/dist/components/ManageAdmins.svelte.d.ts.map +0 -1
  196. package/dist/content.d.ts +0 -3
  197. package/dist/content.d.ts.map +0 -1
  198. package/dist/content.js +0 -10
  199. package/dist/github.d.ts +0 -72
  200. package/dist/github.d.ts.map +0 -1
  201. package/dist/github.js +0 -171
  202. package/dist/utils.d.ts +0 -3
  203. package/dist/utils.d.ts.map +0 -1
  204. package/dist/utils.js +0 -11
  205. package/src/lib/adapter.ts +0 -119
  206. package/src/lib/auth/admins.ts +0 -106
  207. package/src/lib/auth/config.ts +0 -108
  208. package/src/lib/auth/guard.ts +0 -60
  209. package/src/lib/auth/index.ts +0 -6
  210. package/src/lib/auth/schema.ts +0 -112
  211. package/src/lib/carta.ts +0 -59
  212. package/src/lib/components/AdminList.svelte +0 -33
  213. package/src/lib/components/ManageAdmins.svelte +0 -84
  214. package/src/lib/content.ts +0 -11
  215. package/src/lib/github.ts +0 -220
  216. package/src/lib/utils.ts +0 -12
@@ -0,0 +1,210 @@
1
+ import type { ComponentRegistry } from '../render/registry.js';
2
+ /** Common to every frontmatter field: the frontmatter key, the form label, and whether it is required. */
3
+ interface FieldBase {
4
+ /** Frontmatter key and form input name. */
5
+ name: string;
6
+ /** Form label. */
7
+ label: string;
8
+ /** A required field fails validation when empty (spec §7.4). */
9
+ required?: boolean;
10
+ }
11
+ /** A single-line text input. */
12
+ export interface TextField extends FieldBase {
13
+ type: 'text';
14
+ }
15
+ /** A multi-line text input. */
16
+ export interface TextareaField extends FieldBase {
17
+ type: 'textarea';
18
+ /** Visible rows; the editor picks a default when omitted. */
19
+ rows?: number;
20
+ }
21
+ /** A `YYYY-MM-DD` date input. */
22
+ export interface DateField extends FieldBase {
23
+ type: 'date';
24
+ }
25
+ /** A checkbox; absent means false. */
26
+ export interface BooleanField extends FieldBase {
27
+ type: 'boolean';
28
+ }
29
+ /** A closed-vocabulary tag set, rendered as checkboxes (ecnordic). */
30
+ export interface TagsField extends FieldBase {
31
+ type: 'tags';
32
+ /** The controlled vocabulary. */
33
+ options: readonly string[];
34
+ }
35
+ /** Free-form tags, edited as one comma-separated input (907). */
36
+ export interface FreeTagsField extends FieldBase {
37
+ type: 'freetags';
38
+ placeholder?: string;
39
+ }
40
+ /**
41
+ * The discriminated union the per-concept frontmatter form is generated from. Adding a
42
+ * field type is one variant here plus one decode arm in `frontmatterFromForm` and one in
43
+ * `validateFields`.
44
+ */
45
+ export type FrontmatterField = TextField | TextareaField | DateField | BooleanField | TagsField | FreeTagsField;
46
+ /**
47
+ * A validator's verdict. On success it carries the normalized frontmatter to commit; on
48
+ * failure it carries field-keyed error messages (the empty key is a form-level error).
49
+ * Invalid input bounces to the form and never reaches git (spec §7.4).
50
+ */
51
+ export type ValidationResult = {
52
+ ok: true;
53
+ data: Record<string, unknown>;
54
+ } | {
55
+ ok: false;
56
+ errors: Record<string, string>;
57
+ };
58
+ /**
59
+ * Per-site configuration for one content concept (spec §8). Concept-fixed behavior such as
60
+ * routability is not here; it lives in the engine's routing table (`CONCEPT_ROUTING`).
61
+ */
62
+ export interface ConceptConfig {
63
+ /** Repo-relative content directory, e.g. "src/content/posts". */
64
+ dir: string;
65
+ /** Sidebar label; defaults from the concept id when omitted. */
66
+ label?: string;
67
+ /** Drives the per-concept frontmatter form, in order. */
68
+ fields: FrontmatterField[];
69
+ /** Validate submitted frontmatter before any commit. */
70
+ validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
71
+ }
72
+ /** The GitHub App backend a site reads from and commits to (spec §8). Plain data the GitHub engine (Plan 03) consumes. */
73
+ export interface BackendConfig {
74
+ owner: string;
75
+ repo: string;
76
+ /** Commit target, e.g. "main". */
77
+ branch: string;
78
+ appId: string;
79
+ installationId: string;
80
+ }
81
+ /** Magic-link sender identity for Cloudflare Email Sending. */
82
+ export interface SenderConfig {
83
+ from: string;
84
+ replyTo?: string;
85
+ }
86
+ /** A git-committed YAML menu this site's nav editor manages (Plan 06). */
87
+ export interface NavMenuConfig {
88
+ /** Repo-relative path to the site-config YAML, e.g. "src/lib/site.config.yaml". */
89
+ configPath: string;
90
+ /** Key within the file's menus map, e.g. "primary". */
91
+ menuName: string;
92
+ /** Sidebar label for the menu. */
93
+ label: string;
94
+ /** Max nesting depth allowed in the editor; defaults to 2. */
95
+ maxDepth?: number;
96
+ }
97
+ /** Reserved asset slot (seam 4). Typed and unused in the rebuild; R7/R9 read it later with no contract change. */
98
+ export interface AssetConfig {
99
+ /** Repo-relative asset roots, e.g. ["static/images"]. */
100
+ roots: string[];
101
+ /** Public URL base, e.g. "/images". */
102
+ publicBase: string;
103
+ }
104
+ /** The single seam the engine consumes. A site implements this at `src/lib/cairn.config.ts`. */
105
+ export interface CairnAdapter {
106
+ siteName: string;
107
+ /**
108
+ * Which content concepts this site enables. A future `fragments?` key attaches here with
109
+ * no reshape of the contract (seam 1). A site never has two of the same concept.
110
+ */
111
+ content: {
112
+ posts?: ConceptConfig;
113
+ pages?: ConceptConfig;
114
+ };
115
+ backend: BackendConfig;
116
+ sender: SenderConfig;
117
+ /** Design-accurate preview: the same render pipeline the site ships. */
118
+ renderPreview(md: string): string | Promise<string>;
119
+ /** Directive component registry; the renderer and the future palette derive from it (seam 3). */
120
+ registry?: ComponentRegistry;
121
+ navMenu?: NavMenuConfig;
122
+ assets?: AssetConfig;
123
+ }
124
+ /**
125
+ * Concept-fixed routing for a normalized concept (spec §7.2). Posts are dated feed entries;
126
+ * pages are plain navigable structure. Not in adapter config.
127
+ */
128
+ export interface RoutingRule {
129
+ /** Routable as a standalone URL. A future Fragments concept is embedded, not routable. */
130
+ routable: boolean;
131
+ /** Carries a date (posts). */
132
+ dated: boolean;
133
+ /** Appears in feeds and the sitemap (posts). */
134
+ inFeeds: boolean;
135
+ }
136
+ /**
137
+ * The engine-internal, uniform view of one concept after normalization (seam 1). The admin
138
+ * nav, the list views, and the editor all read this, never the raw config.
139
+ */
140
+ export interface ConceptDescriptor {
141
+ /** Concept id, the key under `content`, e.g. "posts". */
142
+ id: string;
143
+ label: string;
144
+ dir: string;
145
+ routing: RoutingRule;
146
+ fields: FrontmatterField[];
147
+ validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
148
+ }
149
+ /**
150
+ * A site-defined admin screen contributed by an extension (Mode 2). It gains a sidebar entry, the
151
+ * `/admin` guard, and the session, and may commit through the same GitHub pipeline. The dispatch
152
+ * route is built in Plan 09; the `load`/`actions`/`component` members are typed loosely here and
153
+ * tightened when the machinery lands.
154
+ */
155
+ export interface AdminPanel {
156
+ /** Routes under `/admin/<id>`; also the sidebar key. */
157
+ id: string;
158
+ /** Sidebar label. */
159
+ label: string;
160
+ /** Owner-gated, like editor management. */
161
+ owner?: boolean;
162
+ /** Server load, behind the guard. Typed in Plan 09. */
163
+ load?: (event: unknown) => unknown;
164
+ /** Named form actions, which may use the commit pipeline. Typed in Plan 09. */
165
+ actions?: Record<string, (event: unknown) => Promise<unknown>>;
166
+ /** The panel UI, rendered inside the admin shell. Typed as a component in Plan 09. */
167
+ component: unknown;
168
+ }
169
+ /**
170
+ * A custom frontmatter field type contributed by an extension (Mode 2): a renderer plus a validator
171
+ * dispatched alongside the built-in field union. The renderer and validator are typed in Plan 09
172
+ * when the form dispatch becomes a registry; the `type` key reserves the discriminator now.
173
+ */
174
+ export interface FieldTypeDef {
175
+ /** The field-type discriminator, e.g. "color". */
176
+ type: string;
177
+ }
178
+ /**
179
+ * A future build-time extension (seam 2). It folds in the same way the adapter does and
180
+ * contributes the same kinds of things. Reserved and unused in the rebuild; the shape is
181
+ * fixed now so the extension contract is additive later.
182
+ */
183
+ export interface CairnExtension {
184
+ /** Additional concepts, merged after the adapter's. */
185
+ content?: Record<string, ConceptConfig>;
186
+ /** Site-defined admin panels (Mode 2). Carried onto the runtime now; dispatched in Plan 09. */
187
+ adminPanels?: AdminPanel[];
188
+ /** Custom field types (Mode 2). Carried onto the runtime now; dispatched in Plan 09. */
189
+ fieldTypes?: FieldTypeDef[];
190
+ }
191
+ /**
192
+ * The composed runtime the engine serves from (seam 2 output). The single aggregation point
193
+ * (`composeRuntime`) folds the adapter and any extensions into this shape.
194
+ */
195
+ export interface CairnRuntime {
196
+ siteName: string;
197
+ concepts: ConceptDescriptor[];
198
+ backend: BackendConfig;
199
+ sender: SenderConfig;
200
+ renderPreview(md: string): string | Promise<string>;
201
+ registry?: ComponentRegistry;
202
+ navMenu?: NavMenuConfig;
203
+ assets?: AssetConfig;
204
+ /** Admin panels contributed by extensions (Mode 2). Empty until Plan 09 wires the dispatch route. */
205
+ adminPanels?: AdminPanel[];
206
+ /** Field types contributed by extensions (Mode 2). Empty until Plan 09 wires the form dispatch. */
207
+ fieldTypes?: FieldTypeDef[];
208
+ }
209
+ export {};
210
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/content/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { FrontmatterField, ValidationResult } from './types.js';
2
+ /**
3
+ * Validate raw frontmatter against a field list. Required text and date fields must be
4
+ * non-empty; required tag fields must be non-empty lists. Booleans coerce to `true`/`false`
5
+ * and tag fields to string arrays. Returns the normalized data, or field-keyed errors when
6
+ * any required field is empty.
7
+ *
8
+ * Frontmatter may arrive from the edit form (all string values) or from `parseMarkdown`,
9
+ * where gray-matter turns an unquoted YAML date into a JS `Date`. The `date` case coerces a
10
+ * `Date` to `YYYY-MM-DD` so a valid parsed date is not mistaken for an empty one.
11
+ */
12
+ export declare function validateFields(fields: FrontmatterField[], frontmatter: Record<string, unknown>): ValidationResult;
13
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/lib/content/validate.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGrE;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EAAE,EAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,gBAAgB,CA8BlB"}
@@ -0,0 +1,45 @@
1
+ import { dateInputValue } from './frontmatter.js';
2
+ /**
3
+ * Validate raw frontmatter against a field list. Required text and date fields must be
4
+ * non-empty; required tag fields must be non-empty lists. Booleans coerce to `true`/`false`
5
+ * and tag fields to string arrays. Returns the normalized data, or field-keyed errors when
6
+ * any required field is empty.
7
+ *
8
+ * Frontmatter may arrive from the edit form (all string values) or from `parseMarkdown`,
9
+ * where gray-matter turns an unquoted YAML date into a JS `Date`. The `date` case coerces a
10
+ * `Date` to `YYYY-MM-DD` so a valid parsed date is not mistaken for an empty one.
11
+ */
12
+ export function validateFields(fields, frontmatter) {
13
+ const data = {};
14
+ const errors = {};
15
+ for (const field of fields) {
16
+ const value = frontmatter[field.name];
17
+ switch (field.type) {
18
+ case 'boolean':
19
+ data[field.name] = value === true;
20
+ break;
21
+ case 'tags':
22
+ case 'freetags': {
23
+ const list = Array.isArray(value) ? value.map(String) : [];
24
+ if (field.required && list.length === 0)
25
+ errors[field.name] = `${field.label} is required`;
26
+ data[field.name] = list;
27
+ break;
28
+ }
29
+ case 'date': {
30
+ const text = value instanceof Date ? dateInputValue(value) : typeof value === 'string' ? value.trim() : '';
31
+ if (field.required && text === '')
32
+ errors[field.name] = `${field.label} is required`;
33
+ data[field.name] = text;
34
+ break;
35
+ }
36
+ default: {
37
+ const text = typeof value === 'string' ? value.trim() : '';
38
+ if (field.required && text === '')
39
+ errors[field.name] = `${field.label} is required`;
40
+ data[field.name] = text;
41
+ }
42
+ }
43
+ }
44
+ return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, data };
45
+ }
package/dist/email.d.ts CHANGED
@@ -1,14 +1,27 @@
1
- /** Cloudflare Email Sending binding surface (the object-form `send`, not the MIME form). */
2
- export interface EmailSender {
3
- send(message: {
4
- to: string;
5
- from: string;
6
- subject: string;
7
- text?: string;
8
- html?: string;
9
- }): Promise<{
10
- messageId: string;
11
- }>;
1
+ import type { AuthEnv } from './auth/types.js';
2
+ export type { AuthEnv };
3
+ /** The message a built magic-link email carries. */
4
+ export interface MagicLinkMessage {
5
+ to: string;
6
+ from: string;
7
+ subject: string;
8
+ html: string;
9
+ text: string;
12
10
  }
13
- export declare function sendMagicLink(sender: EmailSender, to: string, link: string, siteName: string, from: string): Promise<void>;
11
+ /** Per-site identity for the magic-link email, sourced from the adapter. */
12
+ export interface AuthBranding {
13
+ siteName: string;
14
+ from: string;
15
+ replyTo?: string;
16
+ }
17
+ /** The injected send. Production uses `cloudflareSend`; tests pass a sink. */
18
+ export type SendMagicLink = (env: AuthEnv, message: MagicLinkMessage) => Promise<void>;
19
+ /** Build the confirmation email. The link is the only action; the copy stays plain. */
20
+ export declare function buildMagicLinkMessage(input: {
21
+ to: string;
22
+ branding: AuthBranding;
23
+ link: string;
24
+ }): MagicLinkMessage;
25
+ /** The production send: Cloudflare Email Sending through the EMAIL binding. */
26
+ export declare const cloudflareSend: SendMagicLink;
14
27
  //# sourceMappingURL=email.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/lib/email.ts"],"names":[],"mappings":"AAQA,4FAA4F;AAC5F,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,OAAO,EAAE;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpC;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAgBf"}
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/lib/email.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,YAAY,EAAE,OAAO,EAAE,CAAC;AAExB,oDAAoD;AACpD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAYvF,uFAAuF;AACvF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,gBAAgB,CAQnB;AAED,+EAA+E;AAC/E,eAAO,MAAM,cAAc,EAAE,aAG5B,CAAC"}
package/dist/email.js CHANGED
@@ -1,25 +1,25 @@
1
- // cairn-core: pluggable magic-link email sender.
2
- //
3
- // The default adapter targets Cloudflare Email Service (Email Sending, transactional,
4
- // arbitrary recipients), distinct from Email Routing's recipient-restricted `EmailMessage`
5
- // flow. Both share the same `send_email` binding (configured without a destination_address)
6
- // but use a different call shape: `binding.send({ to, from, ... })`.
7
- // Resend can slot in behind the same `sendMagicLink` signature if needed.
8
- export async function sendMagicLink(sender, to, link, siteName, from) {
9
- const expiry = "This link expires in 10 minutes and works only once. If you didn't request it, ignore this email.";
10
- try {
11
- await sender.send({
12
- to,
13
- from,
14
- subject: `Your ${siteName} sign-in link`,
15
- text: `Sign in to ${siteName}:\n\n${link}\n\n${expiry}`,
16
- html: `<p>Sign in to ${siteName}:</p><p><a href="${link}">Confirm sign-in</a></p><p style="color:#666;font-size:0.9em">${expiry}</p>`,
17
- });
18
- }
19
- catch (err) {
20
- // H6: Email Sending is beta + the sole auth channel. Surface + audit; a Resend fallback
21
- // can slot in behind this same signature if Sending proves unreliable.
22
- console.error(`magic-link email send failed for ${to}:`, err);
23
- throw err;
24
- }
1
+ /** Escape the five HTML-significant characters. */
2
+ function escapeHtml(value) {
3
+ return value
4
+ .replaceAll('&', '&amp;')
5
+ .replaceAll('<', '&lt;')
6
+ .replaceAll('>', '&gt;')
7
+ .replaceAll('"', '&quot;')
8
+ .replaceAll("'", '&#39;');
25
9
  }
10
+ /** Build the confirmation email. The link is the only action; the copy stays plain. */
11
+ export function buildMagicLinkMessage(input) {
12
+ const { to, branding, link } = input;
13
+ const subject = `Sign in to ${branding.siteName}`;
14
+ const text = `Open this link to sign in to ${branding.siteName}:\n\n${link}\n\nThe link expires in 10 minutes. If you did not request it, ignore this email.`;
15
+ // `link` is engine-built and url-safe; `siteName` is site config, so escape it for HTML.
16
+ const name = escapeHtml(branding.siteName);
17
+ const html = `<p>Open this link to sign in to ${name}:</p><p><a href="${link}">Sign in</a></p><p>The link expires in 10 minutes. If you did not request it, ignore this email.</p>`;
18
+ return { to, from: branding.from, subject, html, text };
19
+ }
20
+ /** The production send: Cloudflare Email Sending through the EMAIL binding. */
21
+ export const cloudflareSend = async (env, message) => {
22
+ if (!env.EMAIL)
23
+ throw new Error('EMAIL binding is not configured');
24
+ await env.EMAIL.send(message);
25
+ };
package/dist/env.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { D1Database } from '@cloudflare/workers-types';
2
+ /**
3
+ * Returns the site's public origin from configuration.
4
+ *
5
+ * The origin is always config-derived, never read from a request header, so a
6
+ * forged Host header cannot redirect a magic link (spec 7.1, risk H3).
7
+ *
8
+ * @throws Error when `PUBLIC_ORIGIN` is unset or empty.
9
+ */
10
+ export declare function requireOrigin(env: {
11
+ PUBLIC_ORIGIN?: string;
12
+ }): string;
13
+ /**
14
+ * Returns the `AUTH_DB` binding, or throws a clear error when a site has not wired it.
15
+ *
16
+ * The handlers read D1 off `event.platform.env`; without this a misconfigured binding
17
+ * surfaces as a raw `TypeError` deep in a store call. This gives the failure a name.
18
+ *
19
+ * @throws Error when `AUTH_DB` is missing.
20
+ */
21
+ export declare function requireDb(env: {
22
+ AUTH_DB?: D1Database;
23
+ }): D1Database;
24
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/lib/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMrE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,UAAU,CAKnE"}
package/dist/env.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Returns the site's public origin from configuration.
3
+ *
4
+ * The origin is always config-derived, never read from a request header, so a
5
+ * forged Host header cannot redirect a magic link (spec 7.1, risk H3).
6
+ *
7
+ * @throws Error when `PUBLIC_ORIGIN` is unset or empty.
8
+ */
9
+ export function requireOrigin(env) {
10
+ const origin = env.PUBLIC_ORIGIN;
11
+ if (!origin) {
12
+ throw new Error('PUBLIC_ORIGIN is not configured');
13
+ }
14
+ return origin;
15
+ }
16
+ /**
17
+ * Returns the `AUTH_DB` binding, or throws a clear error when a site has not wired it.
18
+ *
19
+ * The handlers read D1 off `event.platform.env`; without this a misconfigured binding
20
+ * surfaces as a raw `TypeError` deep in a store call. This gives the failure a name.
21
+ *
22
+ * @throws Error when `AUTH_DB` is missing.
23
+ */
24
+ export function requireDb(env) {
25
+ if (!env.AUTH_DB) {
26
+ throw new Error('AUTH_DB binding is not configured');
27
+ }
28
+ return env.AUTH_DB;
29
+ }
@@ -0,0 +1,12 @@
1
+ import type { BackendConfig } from '../content/types.js';
2
+ import type { AppCredentials } from './types.js';
3
+ /** The Worker secret holding the GitHub App private key: base64 of the PEM, single line. */
4
+ export interface GithubKeyEnv {
5
+ GITHUB_APP_PRIVATE_KEY_B64?: string;
6
+ }
7
+ /**
8
+ * Assemble the `AppCredentials` the signer needs from the adapter's `backend` (app id,
9
+ * installation) and the Worker's private-key secret. Throws when the secret is unset.
10
+ */
11
+ export declare function appCredentials(backend: Pick<BackendConfig, 'appId' | 'installationId'>, env: GithubKeyEnv): AppCredentials;
12
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/lib/github/credentials.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,gBAAgB,CAAC,EACxD,GAAG,EAAE,YAAY,GAChB,cAAc,CAMhB"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Assemble the `AppCredentials` the signer needs from the adapter's `backend` (app id,
3
+ * installation) and the Worker's private-key secret. Throws when the secret is unset.
4
+ */
5
+ export function appCredentials(backend, env) {
6
+ const privateKeyB64 = env.GITHUB_APP_PRIVATE_KEY_B64;
7
+ if (!privateKeyB64) {
8
+ throw new Error('GITHUB_APP_PRIVATE_KEY_B64 is not configured');
9
+ }
10
+ return { appId: backend.appId, installationId: backend.installationId, privateKeyB64 };
11
+ }
@@ -0,0 +1,49 @@
1
+ import type { CommitAuthor, RepoFile, RepoRef } from './types.js';
2
+ /** The recursive Git Trees API URL for the configured branch. */
3
+ export declare function treeUrl(repo: RepoRef): string;
4
+ /** A Git Trees API entry: a full repo path and whether it is a blob or a subtree. */
5
+ interface TreeEntry {
6
+ path: string;
7
+ type: string;
8
+ }
9
+ /**
10
+ * Markdown files directly in `dir`, newest id first. Tree entries carry full repo paths, so
11
+ * the directory prefix is stripped to a basename before deriving the id. Nested files, non
12
+ * markdown, and other directories are dropped.
13
+ */
14
+ export declare function markdownFilesIn(dir: string, tree: TreeEntry[]): RepoFile[];
15
+ /**
16
+ * List the markdown files in a concept directory through the Git Trees API. A truncated tree
17
+ * (GitHub caps the recursive listing near 100,000 entries) throws rather than returning a
18
+ * silent partial list; a concept directory sits far below that, and sharding is deferred
19
+ * until one approaches it (spec §7.3).
20
+ */
21
+ export declare function listMarkdown(repo: RepoRef, dir: string, token?: string): Promise<RepoFile[]>;
22
+ /** The contents-API URL for a repo path, pinned to the configured branch. */
23
+ export declare function contentsUrl(repo: RepoRef, path: string): string;
24
+ /**
25
+ * Fetch a file's raw markdown, or null if it does not exist. The contents API caps a raw
26
+ * read at 1 MB; a concept's files sit far below that, and sharding is deferred until one
27
+ * approaches it (spec §7.3).
28
+ */
29
+ export declare function readRaw(repo: RepoRef, path: string, token?: string): Promise<string | null>;
30
+ /** The current blob sha for a path, or null if the file does not yet exist. */
31
+ export declare function fileSha(repo: RepoRef, path: string, token: string): Promise<string | null>;
32
+ /**
33
+ * Commit `content` to `path` on the configured branch through the contents API. The author is
34
+ * the editor; the committer is omitted, so GitHub attributes it to the App (`cairn-cms[bot]`).
35
+ * Updates the file in place when it exists (passing its sha), creates it otherwise. Returns the
36
+ * commit sha. A stale-sha 409 (someone committed in between) becomes a `CommitConflictError`,
37
+ * so the save fails safe: re-fetch and ask the editor to reapply, never a merge.
38
+ *
39
+ * Caller preconditions this layer cannot enforce, and the save action (Plan 05) must:
40
+ * `path` is confined to the concept's configured directory (the App token can write anywhere
41
+ * in the repo, so an unvalidated path could overwrite CI config or source), and `author` is
42
+ * derived from the verified server-side session, never from request input.
43
+ */
44
+ export declare function commitFile(repo: RepoRef, path: string, content: string, opts: {
45
+ message: string;
46
+ author: CommitAuthor;
47
+ }, token: string): Promise<string>;
48
+ export {};
49
+ //# sourceMappingURL=repo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/lib/github/repo.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAelE,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAE7C;AAED,qFAAqF;AACrF,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAOD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE,CAW1E;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAMlG;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKjG;AAOD,+EAA+E;AAC/E,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKhG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,EAC/C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAiBjB"}
@@ -0,0 +1,123 @@
1
+ // src/lib/github/repo.ts
2
+ // cairn-cms: repo reads and the commit, over the GitHub REST API. Listing a concept
3
+ // directory uses the Git Trees API (the contents API silently truncates at 1,000 entries,
4
+ // spec §7.3); a single-file read uses the contents API. An optional token lifts reads to
5
+ // the authenticated rate limit and unlocks private repos; ecnordic's repo is public, 907's
6
+ // is not.
7
+ import { idFromFilename } from '../content/ids.js';
8
+ import { CommitConflictError } from './types.js';
9
+ const API = 'https://api.github.com';
10
+ /** Standard GitHub API headers, with a bearer token when one is supplied. */
11
+ function ghHeaders(accept, token) {
12
+ const headers = {
13
+ Accept: accept,
14
+ 'User-Agent': 'cairn-cms',
15
+ 'X-GitHub-Api-Version': '2022-11-28',
16
+ };
17
+ if (token)
18
+ headers.Authorization = `Bearer ${token}`;
19
+ return headers;
20
+ }
21
+ /** The recursive Git Trees API URL for the configured branch. */
22
+ export function treeUrl(repo) {
23
+ return `${API}/repos/${repo.owner}/${repo.repo}/git/trees/${encodeURIComponent(repo.branch)}?recursive=1`;
24
+ }
25
+ /** The basename of a repo path: the segment after the last slash. */
26
+ function basename(path) {
27
+ return path.slice(path.lastIndexOf('/') + 1);
28
+ }
29
+ /**
30
+ * Markdown files directly in `dir`, newest id first. Tree entries carry full repo paths, so
31
+ * the directory prefix is stripped to a basename before deriving the id. Nested files, non
32
+ * markdown, and other directories are dropped.
33
+ */
34
+ export function markdownFilesIn(dir, tree) {
35
+ const clean = dir.replace(/^\/+|\/+$/g, '');
36
+ const prefix = `${clean}/`;
37
+ return tree
38
+ .filter((entry) => entry.type === 'blob' && entry.path.startsWith(prefix) && entry.path.endsWith('.md'))
39
+ .filter((entry) => !entry.path.slice(prefix.length).includes('/'))
40
+ .map((entry) => {
41
+ const name = basename(entry.path);
42
+ return { id: idFromFilename(name), name, path: entry.path };
43
+ })
44
+ .sort((a, b) => b.id.localeCompare(a.id));
45
+ }
46
+ /**
47
+ * List the markdown files in a concept directory through the Git Trees API. A truncated tree
48
+ * (GitHub caps the recursive listing near 100,000 entries) throws rather than returning a
49
+ * silent partial list; a concept directory sits far below that, and sharding is deferred
50
+ * until one approaches it (spec §7.3).
51
+ */
52
+ export async function listMarkdown(repo, dir, token) {
53
+ const res = await fetch(treeUrl(repo), { headers: ghHeaders('application/vnd.github+json', token) });
54
+ if (!res.ok)
55
+ throw new Error(`GitHub tree ${repo.branch} failed: ${res.status}`);
56
+ const body = (await res.json());
57
+ if (body.truncated)
58
+ throw new Error(`GitHub tree ${repo.branch} is truncated; ${dir} exceeds the listing cap`);
59
+ return markdownFilesIn(dir, body.tree);
60
+ }
61
+ /** The contents-API URL for a repo path, pinned to the configured branch. */
62
+ export function contentsUrl(repo, path) {
63
+ const clean = path.replace(/^\/+|\/+$/g, '');
64
+ return `${API}/repos/${repo.owner}/${repo.repo}/contents/${clean}?ref=${encodeURIComponent(repo.branch)}`;
65
+ }
66
+ /**
67
+ * Fetch a file's raw markdown, or null if it does not exist. The contents API caps a raw
68
+ * read at 1 MB; a concept's files sit far below that, and sharding is deferred until one
69
+ * approaches it (spec §7.3).
70
+ */
71
+ export async function readRaw(repo, path, token) {
72
+ const res = await fetch(contentsUrl(repo, path), { headers: ghHeaders('application/vnd.github.raw', token) });
73
+ if (res.status === 404)
74
+ return null;
75
+ if (!res.ok)
76
+ throw new Error(`GitHub read ${path} failed: ${res.status}`);
77
+ return res.text();
78
+ }
79
+ /** Standard (padded) base64 of UTF-8 text, the form the contents API expects. */
80
+ function toBase64(text) {
81
+ return btoa(Array.from(new TextEncoder().encode(text), (b) => String.fromCharCode(b)).join(''));
82
+ }
83
+ /** The current blob sha for a path, or null if the file does not yet exist. */
84
+ export async function fileSha(repo, path, token) {
85
+ const res = await fetch(contentsUrl(repo, path), { headers: ghHeaders('application/vnd.github+json', token) });
86
+ if (res.status === 404)
87
+ return null;
88
+ if (!res.ok)
89
+ throw new Error(`GitHub stat ${path} failed: ${res.status}`);
90
+ return (await res.json()).sha;
91
+ }
92
+ /**
93
+ * Commit `content` to `path` on the configured branch through the contents API. The author is
94
+ * the editor; the committer is omitted, so GitHub attributes it to the App (`cairn-cms[bot]`).
95
+ * Updates the file in place when it exists (passing its sha), creates it otherwise. Returns the
96
+ * commit sha. A stale-sha 409 (someone committed in between) becomes a `CommitConflictError`,
97
+ * so the save fails safe: re-fetch and ask the editor to reapply, never a merge.
98
+ *
99
+ * Caller preconditions this layer cannot enforce, and the save action (Plan 05) must:
100
+ * `path` is confined to the concept's configured directory (the App token can write anywhere
101
+ * in the repo, so an unvalidated path could overwrite CI config or source), and `author` is
102
+ * derived from the verified server-side session, never from request input.
103
+ */
104
+ export async function commitFile(repo, path, content, opts, token) {
105
+ const sha = await fileSha(repo, path, token);
106
+ const url = `${API}/repos/${repo.owner}/${repo.repo}/contents/${path.replace(/^\/+|\/+$/g, '')}`;
107
+ const res = await fetch(url, {
108
+ method: 'PUT',
109
+ headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({
111
+ message: opts.message,
112
+ content: toBase64(content),
113
+ branch: repo.branch,
114
+ author: opts.author,
115
+ ...(sha ? { sha } : {}),
116
+ }),
117
+ });
118
+ if (res.status === 409)
119
+ throw new CommitConflictError(path);
120
+ if (!res.ok)
121
+ throw new Error(`GitHub commit ${path} failed: ${res.status} ${await res.text()}`);
122
+ return (await res.json()).commit.sha;
123
+ }