@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
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
+ }
@@ -0,0 +1,17 @@
1
+ import type { AppCredentials } from './types.js';
2
+ /** Mint a GitHub App JWT (RS256), valid ~9 min, with `iat` backdated for clock skew. */
3
+ export declare function appJwt(appId: string, privateKeyPem: string): Promise<string>;
4
+ /** Exchange the App JWT for a short-lived installation access token. */
5
+ export declare function installationToken(creds: AppCredentials): Promise<string>;
6
+ /**
7
+ * Deploy-time self-test for the App signer: sign a dummy JWT with the configured key. It
8
+ * exercises the brittle PKCS#1-to-PKCS#8 conversion and the Web Crypto import and sign with
9
+ * no network call and no secret in the result, so `/admin/healthz` (Plan 05) catches a bad
10
+ * or rotated key before an editor's save fails. The `detail` is a fixed classifier, never the
11
+ * raw crypto error, so the surfaced health result cannot echo key bytes. Never throws.
12
+ */
13
+ export declare function signingSelfTest(appId: string, privateKeyB64: string): Promise<{
14
+ ok: boolean;
15
+ detail?: string;
16
+ }>;
17
+ //# sourceMappingURL=signing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../src/lib/github/signing.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA0CjD,wFAAwF;AACxF,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclF;AAED,wEAAwE;AACxE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAa9E;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH"}
@@ -0,0 +1,79 @@
1
+ const API = 'https://api.github.com';
2
+ const encoder = new TextEncoder();
3
+ /** Encode bytes as unpadded base64url (RFC 4648 §5), the JWT wire format. */
4
+ function bytesToB64url(bytes) {
5
+ const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join('');
6
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
7
+ }
8
+ // TextEncoder/atob produce Uint8Arrays whose generic buffer type no longer satisfies Web
9
+ // Crypto's BufferSource under strict lib types; hand the underlying ArrayBuffer over.
10
+ function buf(bytes) {
11
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
12
+ }
13
+ /** DER length octets for a value of `n` bytes (short form < 128, else long form). */
14
+ function derLength(n) {
15
+ if (n < 0x80)
16
+ return [n];
17
+ const out = [];
18
+ for (let v = n; v > 0; v >>= 8)
19
+ out.unshift(v & 0xff);
20
+ return [0x80 | out.length, ...out];
21
+ }
22
+ // AlgorithmIdentifier for rsaEncryption (OID 1.2.840.113549.1.1.1) with NULL parameters.
23
+ const RSA_ALG_ID = [0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00];
24
+ /** Wrap a PKCS#1 RSAPrivateKey (DER) as PKCS#8 (the only RSA form Web Crypto importKey takes). */
25
+ function pkcs1ToPkcs8(pkcs1) {
26
+ const octet = [0x04, ...derLength(pkcs1.length), ...pkcs1];
27
+ const body = [0x02, 0x01, 0x00, ...RSA_ALG_ID, ...octet];
28
+ return Uint8Array.from([0x30, ...derLength(body.length), ...body]);
29
+ }
30
+ /** Decode a PEM private key to PKCS#8 DER, converting from PKCS#1 (GitHub's format) if needed. */
31
+ function pemToPkcs8(pem) {
32
+ const b64 = pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
33
+ const der = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
34
+ return pem.includes('RSA PRIVATE KEY') ? pkcs1ToPkcs8(der) : der;
35
+ }
36
+ /** Mint a GitHub App JWT (RS256), valid ~9 min, with `iat` backdated for clock skew. */
37
+ export async function appJwt(appId, privateKeyPem) {
38
+ const now = Math.floor(Date.now() / 1000);
39
+ const header = bytesToB64url(encoder.encode(JSON.stringify({ alg: 'RS256', typ: 'JWT' })));
40
+ const payload = bytesToB64url(encoder.encode(JSON.stringify({ iat: now - 60, exp: now + 540, iss: appId })));
41
+ const signingInput = `${header}.${payload}`;
42
+ const key = await crypto.subtle.importKey('pkcs8', buf(pemToPkcs8(privateKeyPem)), { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign']);
43
+ const sig = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', key, buf(encoder.encode(signingInput)));
44
+ return `${signingInput}.${bytesToB64url(new Uint8Array(sig))}`;
45
+ }
46
+ /** Exchange the App JWT for a short-lived installation access token. */
47
+ export async function installationToken(creds) {
48
+ const jwt = await appJwt(creds.appId, atob(creds.privateKeyB64));
49
+ const res = await fetch(`${API}/app/installations/${creds.installationId}/access_tokens`, {
50
+ method: 'POST',
51
+ headers: {
52
+ Accept: 'application/vnd.github+json',
53
+ Authorization: `Bearer ${jwt}`,
54
+ 'User-Agent': 'cairn-cms',
55
+ 'X-GitHub-Api-Version': '2022-11-28',
56
+ },
57
+ });
58
+ if (!res.ok)
59
+ throw new Error(`GitHub installation token failed: ${res.status}`);
60
+ return (await res.json()).token;
61
+ }
62
+ /**
63
+ * Deploy-time self-test for the App signer: sign a dummy JWT with the configured key. It
64
+ * exercises the brittle PKCS#1-to-PKCS#8 conversion and the Web Crypto import and sign with
65
+ * no network call and no secret in the result, so `/admin/healthz` (Plan 05) catches a bad
66
+ * or rotated key before an editor's save fails. The `detail` is a fixed classifier, never the
67
+ * raw crypto error, so the surfaced health result cannot echo key bytes. Never throws.
68
+ */
69
+ export async function signingSelfTest(appId, privateKeyB64) {
70
+ try {
71
+ const jwt = await appJwt(appId, atob(privateKeyB64));
72
+ if (jwt.split('.').length !== 3)
73
+ return { ok: false, detail: 'malformed JWT' };
74
+ return { ok: true };
75
+ }
76
+ catch {
77
+ return { ok: false, detail: 'key import or sign failed' };
78
+ }
79
+ }
@@ -0,0 +1,35 @@
1
+ /** Repo coordinates pinned to a branch: the structural subset of `BackendConfig` the read and commit paths need. */
2
+ export interface RepoRef {
3
+ owner: string;
4
+ repo: string;
5
+ branch: string;
6
+ }
7
+ /** A markdown file in a concept directory. `id` is the filename without `.md`. */
8
+ export interface RepoFile {
9
+ id: string;
10
+ name: string;
11
+ path: string;
12
+ }
13
+ /** A commit author: the signed-in editor (spec §7.4). The committer is left to the App. */
14
+ export interface CommitAuthor {
15
+ name: string;
16
+ email: string;
17
+ }
18
+ /** What the App signer needs: the app id, the installation, and the base64 PEM secret. */
19
+ export interface AppCredentials {
20
+ appId: string;
21
+ installationId: string;
22
+ /** The stored `GITHUB_APP_PRIVATE_KEY_B64`: base64 of the PEM, single line. */
23
+ privateKeyB64: string;
24
+ }
25
+ /**
26
+ * A concurrent edit lost the SHA race: the file changed between the read and the PUT, from
27
+ * another editor or the site's own CI. Thrown so the save fails safe (re-fetch and ask the
28
+ * editor to reapply) instead of surfacing a raw 409. Defined and caught inside the package
29
+ * so `instanceof` is reliable, unlike kit's `redirect`/`error` across the peer boundary.
30
+ */
31
+ export declare class CommitConflictError extends Error {
32
+ readonly path: string;
33
+ constructor(path: string);
34
+ }
35
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/github/types.ts"],"names":[],"mappings":"AAMA,oHAAoH;AACpH,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAIzC"}
@@ -0,0 +1,19 @@
1
+ // src/lib/github/types.ts
2
+ // cairn-cms: the GitHub backend's plain data types and its one typed error. The backend
3
+ // reads repo coordinates from the adapter's `BackendConfig` (spec §8); `RepoRef` is the
4
+ // `{ owner, repo, branch }` subset, so `backend` is assignable wherever a `RepoRef` is
5
+ // wanted with no conversion.
6
+ /**
7
+ * A concurrent edit lost the SHA race: the file changed between the read and the PUT, from
8
+ * another editor or the site's own CI. Thrown so the save fails safe (re-fetch and ask the
9
+ * editor to reapply) instead of surfacing a raw 409. Defined and caught inside the package
10
+ * so `instanceof` is reliable, unlike kit's `redirect`/`error` across the peer boundary.
11
+ */
12
+ export class CommitConflictError extends Error {
13
+ path;
14
+ constructor(path) {
15
+ super(`Commit conflict on ${path}: it changed since it was opened`);
16
+ this.path = path;
17
+ this.name = 'CommitConflictError';
18
+ }
19
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,28 @@
1
- export * from './email';
2
- export * from './github';
3
- export * from './carta';
4
- export * from './content';
5
- export * from './adapter';
6
- export * from './slug';
7
- export * from './render';
8
- export * from './nav';
1
+ export { requireOrigin } from './env.js';
2
+ export type { Role, Editor, AuthEnv } from './auth/types.js';
3
+ export type { AuthBranding, MagicLinkMessage, SendMagicLink } from './email.js';
4
+ export { buildMagicLinkMessage, cloudflareSend } from './email.js';
5
+ export type { CairnAdapter, ConceptConfig, FrontmatterField, TextField, TextareaField, DateField, BooleanField, TagsField, FreeTagsField, ValidationResult, BackendConfig, SenderConfig, NavMenuConfig, AssetConfig, RoutingRule, ConceptDescriptor, CairnExtension, CairnRuntime, AdminPanel, FieldTypeDef, } from './content/types.js';
6
+ export { CONCEPT_ROUTING, normalizeConcepts, findConcept } from './content/concepts.js';
7
+ export { composeRuntime } from './content/compose.js';
8
+ export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown, } from './content/frontmatter.js';
9
+ export { validateFields } from './content/validate.js';
10
+ export { isValidId, idFromFilename, filenameFromId, slugify } from './content/ids.js';
11
+ export { defineRegistry } from './render/registry.js';
12
+ export type { ComponentDef, ComponentRegistry } from './render/registry.js';
13
+ export { glyph } from './render/glyph.js';
14
+ export type { IconSet } from './render/glyph.js';
15
+ export { remarkDirectiveStamp } from './render/remark-directives.js';
16
+ export { rehypeDispatch, isElement, strProp, iconSpan, splitHead, cardShell, markFirstList, } from './render/rehype-dispatch.js';
17
+ export type { MakeIcon } from './render/rehype-dispatch.js';
18
+ export { createRenderer } from './render/pipeline.js';
19
+ export type { RendererOptions } from './render/pipeline.js';
20
+ export type { RepoRef, RepoFile, CommitAuthor, AppCredentials } from './github/types.js';
21
+ export { CommitConflictError } from './github/types.js';
22
+ export { appJwt, installationToken, signingSelfTest } from './github/signing.js';
23
+ export { treeUrl, markdownFilesIn, listMarkdown, contentsUrl, readRaw, fileSha, commitFile, } from './github/repo.js';
24
+ export { appCredentials } from './github/credentials.js';
25
+ export type { GithubKeyEnv } from './github/credentials.js';
26
+ export { parseSiteConfig, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
27
+ export type { NavNode, SiteConfig } from './nav/site-config.js';
9
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAEA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGnE,YAAY,EACV,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,SAAS,EACT,YAAY,EACZ,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEtF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,YAAY,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EACL,cAAc,EACd,SAAS,EACT,OAAO,EACP,QAAQ,EACR,SAAS,EACT,SAAS,EACT,aAAa,GACd,MAAM,6BAA6B,CAAC;AACrC,YAAY,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EACL,OAAO,EACP,eAAe,EACf,YAAY,EACZ,WAAW,EACX,OAAO,EACP,OAAO,EACP,UAAU,GACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG5D,OAAO,EACL,eAAe,EACf,WAAW,EACX,OAAO,EACP,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -1,10 +1,21 @@
1
- // cairn-cms public API. Consumers import content/email/github/adapter from 'cairn-cms';
2
- // auth (better-auth factory, guards, manage-editors) lives at the 'cairn-cms/auth' subpath.
3
- export * from './email';
4
- export * from './github';
5
- export * from './carta';
6
- export * from './content';
7
- export * from './adapter';
8
- export * from './slug';
9
- export * from './render';
10
- export * from './nav';
1
+ // Engine entry. Auth landed in Plan 01, the content model and adapter in Plan 02, and the
2
+ // GitHub read-and-commit backend in Plan 03; render and nav follow.
3
+ export { requireOrigin } from './env.js';
4
+ export { buildMagicLinkMessage, cloudflareSend } from './email.js';
5
+ export { CONCEPT_ROUTING, normalizeConcepts, findConcept } from './content/concepts.js';
6
+ export { composeRuntime } from './content/compose.js';
7
+ export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown, } from './content/frontmatter.js';
8
+ export { validateFields } from './content/validate.js';
9
+ export { isValidId, idFromFilename, filenameFromId, slugify } from './content/ids.js';
10
+ // Render engine (Plan 04): generic directive pipeline; sites own the component registry.
11
+ export { defineRegistry } from './render/registry.js';
12
+ export { glyph } from './render/glyph.js';
13
+ export { remarkDirectiveStamp } from './render/remark-directives.js';
14
+ export { rehypeDispatch, isElement, strProp, iconSpan, splitHead, cardShell, markFirstList, } from './render/rehype-dispatch.js';
15
+ export { createRenderer } from './render/pipeline.js';
16
+ export { CommitConflictError } from './github/types.js';
17
+ export { appJwt, installationToken, signingSelfTest } from './github/signing.js';
18
+ export { treeUrl, markdownFilesIn, listMarkdown, contentsUrl, readRaw, fileSha, commitFile, } from './github/repo.js';
19
+ export { appCredentials } from './github/credentials.js';
20
+ // Nav tree and site-config helpers (Plan 06).
21
+ export { parseSiteConfig, extractMenu, setMenu, validateNavTree, MAX_NAV_NODES, NavValidationError, SiteConfigError, } from './nav/site-config.js';
@@ -1,4 +1,4 @@
1
- /** One navigation node. `url` omitted/empty is a label-only grouping header; `children` omitted is a leaf. */
1
+ /** One navigation node. An omitted or empty `url` is a label-only grouping header; no `children` is a leaf. */
2
2
  export interface NavNode {
3
3
  label: string;
4
4
  url?: string;
@@ -6,18 +6,22 @@ export interface NavNode {
6
6
  }
7
7
  /** Total node cap across the whole tree, a guard against a runaway payload. */
8
8
  export declare const MAX_NAV_NODES = 200;
9
+ /** Maximum character length for a node label. */
10
+ export declare const MAX_LABEL_LENGTH = 500;
11
+ /** Maximum character length for a node URL. */
12
+ export declare const MAX_URL_LENGTH = 2048;
9
13
  export declare class NavValidationError extends Error {
10
14
  constructor(message: string);
11
15
  }
12
16
  /**
13
- * Validate and normalize an untrusted value into a NavNode[]: arrays only, non-empty labels,
14
- * depth within `maxDepth` (1 = flat), bounded node count, and only the three known keys kept.
15
- * Throws NavValidationError on any violation. Used by `navSave` before writing.
17
+ * Validate and normalize an untrusted value into a NavNode[]: arrays only, non-empty labels, depth
18
+ * within `maxDepth` (1 is flat), a bounded node count, and only the three known keys kept. Throws
19
+ * NavValidationError on any violation. Used by navSave before writing.
16
20
  */
17
21
  export declare function validateNavTree(value: unknown, maxDepth: number): NavNode[];
18
22
  /**
19
- * Shape of the YAML site-config file. Unknown keys are ignored so the file can grow without
20
- * an engine change. Read at build time by the public site.
23
+ * Shape of the YAML site-config file. Unknown keys are ignored so the file can grow without an
24
+ * engine change. Read at build time by the public site.
21
25
  */
22
26
  export interface SiteConfig {
23
27
  siteName: string;
@@ -27,19 +31,7 @@ export interface SiteConfig {
27
31
  locale?: string;
28
32
  /** Named navigation menus, each a NavNode[] (normalized by extractMenu). */
29
33
  menus?: Record<string, unknown>;
30
- email?: {
31
- sender?: string;
32
- senderName?: string;
33
- };
34
- footer?: {
35
- copyrightName?: string;
36
- };
37
- settings?: {
38
- feedMaxItems?: number;
39
- homepageFeaturedCount?: number;
40
- postTags?: string[];
41
- [key: string]: unknown;
42
- };
34
+ [key: string]: unknown;
43
35
  }
44
36
  export declare class SiteConfigError extends Error {
45
37
  constructor(message: string);
@@ -49,10 +41,10 @@ export declare function parseSiteConfig(raw: string): SiteConfig;
49
41
  /** Extract one named menu from a parsed config and validate it. Returns [] when the menu is absent. */
50
42
  export declare function extractMenu(config: SiteConfig, name: string, maxDepth: number): NavNode[];
51
43
  /**
52
- * Replace one named menu in the YAML site-config text and re-serialize, preserving every other
53
- * top-level key (siteName, other menus, settings, ...). The `/admin/nav` editor commits the result.
54
- * Parses into a Document so the rest of the file round-trips; YAML comments are not preserved
55
- * (an accepted trade), but data keys are. A leaf node serializes without `url`/`children` keys.
44
+ * Replace one named menu in the YAML site-config text and reserialize, preserving every other
45
+ * top-level key (siteName, other menus, settings). Parses into a Document so the rest of the file
46
+ * round-trips. YAML comments are not preserved (an accepted trade); data keys are. A leaf node
47
+ * serializes without `url`/`children` keys.
56
48
  */
57
49
  export declare function setMenu(raw: string, name: string, tree: NavNode[]): string;
58
- //# sourceMappingURL=nav.d.ts.map
50
+ //# sourceMappingURL=site-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-config.d.ts","sourceRoot":"","sources":["../../src/lib/nav/site-config.ts"],"names":[],"mappings":"AAMA,+GAA+G;AAC/G,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAED,+EAA+E;AAC/E,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,+CAA+C;AAC/C,eAAO,MAAM,cAAc,OAAO,CAAC;AAKnC,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CA6B3E;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAUvD;AAED,uGAAuG;AACvG,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CAIzF;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAO1E"}