@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,23 @@
1
+ import { type AuthBranding, type SendMagicLink } from '../email.js';
2
+ import type { RequestContext } from './types.js';
3
+ export interface AuthRoutesConfig {
4
+ branding: AuthBranding;
5
+ send?: SendMagicLink;
6
+ }
7
+ export declare function createAuthRoutes(config: AuthRoutesConfig): {
8
+ loginLoad: (event: RequestContext) => {
9
+ siteName: string;
10
+ error: string | null;
11
+ };
12
+ requestAction: (event: RequestContext) => Promise<{
13
+ sent: true;
14
+ }>;
15
+ confirmLoad: (event: RequestContext) => {
16
+ token: string;
17
+ siteName: string;
18
+ error: string | null;
19
+ };
20
+ confirmAction: (event: RequestContext) => Promise<never>;
21
+ logoutAction: (event: RequestContext) => Promise<never>;
22
+ };
23
+ //# sourceMappingURL=auth-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/auth-routes.ts"],"names":[],"mappings":"AAcA,OAAO,EAAyC,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB;uBA2B7B,cAAc,KAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;2BAnBjD,cAAc,KAAG,OAAO,CAAC;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;yBA4BnE,cAAc,KACpB;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;2BAcxB,cAAc,KAAG,OAAO,CAAC,KAAK,CAAC;0BAwBhC,cAAc,KAAG,OAAO,CAAC,KAAK,CAAC;EASnE"}
@@ -0,0 +1,85 @@
1
+ // The SvelteKit handlers for the magic-link flow, consumed by a site's thin route shims.
2
+ // The factory takes per-site branding and an injected send, so tests run the real handlers
3
+ // against a sink. The confirm-load, confirm, and logout handlers arrive in Task 6.
4
+ import { redirect } from '@sveltejs/kit';
5
+ import { requireOrigin, requireDb } from '../env.js';
6
+ import { generateToken, generateSessionId, hashToken, TOKEN_TTL_MS, SESSION_TTL_MS, COOKIE_NAME, } from '../auth/crypto.js';
7
+ import { findEditor, issueToken, consumeToken, createSession, deleteSession } from '../auth/store.js';
8
+ import { buildMagicLinkMessage, cloudflareSend } from '../email.js';
9
+ export function createAuthRoutes(config) {
10
+ const send = config.send ?? cloudflareSend;
11
+ /**
12
+ * POST /admin/auth/request. Looks the email up in the allowlist; on a match, issues a token
13
+ * and emails the confirmation link. The response is identical whether or not the email is
14
+ * allow-listed, so the endpoint never leaks membership.
15
+ */
16
+ async function requestAction(event) {
17
+ const env = event.platform?.env ?? {};
18
+ const origin = requireOrigin(env);
19
+ const db = requireDb(env);
20
+ const form = await event.request.formData();
21
+ const email = String(form.get('email') ?? '').trim().toLowerCase();
22
+ const editor = email ? await findEditor(db, email) : null;
23
+ if (editor) {
24
+ const token = generateToken();
25
+ const now = Date.now();
26
+ await issueToken(db, email, await hashToken(token), now + TOKEN_TTL_MS, now);
27
+ const link = `${origin}/admin/auth/confirm?token=${encodeURIComponent(token)}`;
28
+ await send(env, buildMagicLinkMessage({ to: email, branding: config.branding, link }));
29
+ }
30
+ return { sent: true };
31
+ }
32
+ /** GET /admin/login. Public. Carries the site name and an optional `?error` for the form. */
33
+ function loginLoad(event) {
34
+ return { siteName: config.branding.siteName, error: event.url.searchParams.get('error') };
35
+ }
36
+ /**
37
+ * GET /admin/auth/confirm. Renders the confirm page and consumes nothing; only the POST
38
+ * verifies. Sets Referrer-Policy: no-referrer so the token does not leak to a referrer.
39
+ */
40
+ function confirmLoad(event) {
41
+ event.setHeaders({ 'Referrer-Policy': 'no-referrer' });
42
+ return {
43
+ token: event.url.searchParams.get('token') ?? '',
44
+ siteName: config.branding.siteName,
45
+ error: event.url.searchParams.get('error'),
46
+ };
47
+ }
48
+ /**
49
+ * POST /admin/auth/confirm. Hashes the submitted token and consumes it atomically. A valid
50
+ * token yields the email; the handler creates a session, sets the cookie, and redirects to
51
+ * /admin. An invalid, replayed, or expired token redirects to the login page.
52
+ */
53
+ async function confirmAction(event) {
54
+ const db = requireDb(event.platform?.env ?? {});
55
+ const form = await event.request.formData();
56
+ const token = String(form.get('token') ?? '');
57
+ if (!token)
58
+ throw redirect(303, '/admin/login?error=expired');
59
+ const now = Date.now();
60
+ const email = await consumeToken(db, await hashToken(token), now);
61
+ if (!email)
62
+ throw redirect(303, '/admin/login?error=expired');
63
+ const id = generateSessionId();
64
+ await createSession(db, id, email, now + SESSION_TTL_MS, now);
65
+ event.cookies.set(COOKIE_NAME, id, {
66
+ path: '/',
67
+ httpOnly: true,
68
+ // Secure on HTTPS (every real deploy); off on local http dev so the cookie sticks.
69
+ secure: event.url.protocol === 'https:',
70
+ sameSite: 'lax',
71
+ maxAge: Math.floor(SESSION_TTL_MS / 1000),
72
+ });
73
+ throw redirect(303, '/admin');
74
+ }
75
+ /** POST /admin/auth/logout. Deletes the session row and clears the cookie. */
76
+ async function logoutAction(event) {
77
+ const db = requireDb(event.platform?.env ?? {});
78
+ const id = event.cookies.get(COOKIE_NAME);
79
+ if (id)
80
+ await deleteSession(db, id);
81
+ event.cookies.delete(COOKIE_NAME, { path: '/' });
82
+ throw redirect(303, '/admin/login');
83
+ }
84
+ return { loginLoad, requestAction, confirmLoad, confirmAction, logoutAction };
85
+ }
@@ -0,0 +1,80 @@
1
+ import { type GithubKeyEnv } from '../github/credentials.js';
2
+ import type { CairnRuntime, FrontmatterField } from '../content/types.js';
3
+ import type { Editor, Role } from '../auth/types.js';
4
+ /** A sidebar concept entry: just enough to render the nav without shipping validators to the client. */
5
+ export interface NavConcept {
6
+ id: string;
7
+ label: string;
8
+ }
9
+ /** The admin layout's data: site identity, the signed-in user, the nav, and the active path. */
10
+ export interface LayoutData {
11
+ siteName: string;
12
+ user: {
13
+ displayName: string;
14
+ role: Role;
15
+ };
16
+ concepts: NavConcept[];
17
+ pathname: string;
18
+ canManageEditors: boolean;
19
+ /** The nav menu's label when the site configures one; gates the Navigation nav entry. Null otherwise. */
20
+ navLabel: string | null;
21
+ }
22
+ /** One row in a concept's list view. */
23
+ export interface EntrySummary {
24
+ id: string;
25
+ title: string;
26
+ date: string | null;
27
+ draft: boolean;
28
+ }
29
+ /** The concept list view's data. */
30
+ export interface ListData {
31
+ conceptId: string;
32
+ label: string;
33
+ /** Posts carry a date in the new-entry form; pages do not (concept routing, spec §7.2). */
34
+ dated: boolean;
35
+ entries: EntrySummary[];
36
+ /** A listing failure degrades to an inline message rather than a thrown 500. */
37
+ error: string | null;
38
+ /** A create-form bounce error read from `?error`. */
39
+ formError: string | null;
40
+ }
41
+ /** The editor's data. `frontmatter` holds form-ready values (dates already `YYYY-MM-DD`). */
42
+ export interface EditData {
43
+ conceptId: string;
44
+ id: string;
45
+ label: string;
46
+ fields: FrontmatterField[];
47
+ frontmatter: Record<string, unknown>;
48
+ body: string;
49
+ title: string;
50
+ isNew: boolean;
51
+ saved: boolean;
52
+ error: string | null;
53
+ }
54
+ /** The structural event the content routes read; a real SvelteKit RequestEvent satisfies it. */
55
+ export interface ContentEvent {
56
+ url: URL;
57
+ params: Record<string, string>;
58
+ request: Request;
59
+ locals: {
60
+ editor?: Editor | null;
61
+ };
62
+ platform?: {
63
+ env?: GithubKeyEnv;
64
+ };
65
+ }
66
+ /** Injectable dependencies; tests stub the token mint to avoid signing a real key. */
67
+ export interface ContentRoutesDeps {
68
+ /** Mint a GitHub App installation token from the Worker env. Defaults to the real signer. */
69
+ mintToken?: (env: GithubKeyEnv) => Promise<string>;
70
+ }
71
+ export declare function createContentRoutes(runtime: CairnRuntime, deps?: ContentRoutesDeps): {
72
+ layoutLoad: (event: ContentEvent) => LayoutData;
73
+ indexRedirect: () => never;
74
+ listLoad: (event: ContentEvent) => Promise<ListData>;
75
+ createAction: (event: ContentEvent) => Promise<never>;
76
+ editLoad: (event: ContentEvent) => Promise<EditData>;
77
+ saveAction: (event: ContentEvent) => Promise<never>;
78
+ mintToken: (env: GithubKeyEnv) => Promise<string>;
79
+ };
80
+ //# sourceMappingURL=content-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/content-routes.ts"],"names":[],"mappings":"AAQA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAI7E,OAAO,KAAK,EAAE,YAAY,EAAqB,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAErD,wGAAwG;AACxG,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gGAAgG;AAChG,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;IAC1C,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,yGAAyG;IACzG,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wCAAwC;AACxC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,oCAAoC;AACpC,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,2FAA2F;IAC3F,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,gFAAgF;IAChF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qDAAqD;IACrD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,6FAA6F;AAC7F,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,gGAAgG;AAChG,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACnC,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC;CACnC;AAED,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,6FAA6F;IAC7F,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACpD;AAgBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,GAAE,iBAAsB;wBAK1D,YAAY,KAAG,UAAU;yBAa1B,KAAK;sBAqBA,YAAY,KAAG,OAAO,CAAC,QAAQ,CAAC;0BAqB5B,YAAY,KAAG,OAAO,CAAC,KAAK,CAAC;sBA+BjC,YAAY,KAAG,OAAO,CAAC,QAAQ,CAAC;wBAgC9B,YAAY,KAAG,OAAO,CAAC,KAAK,CAAC;qBA5I5C,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC;EAqLnD"}
@@ -0,0 +1,183 @@
1
+ // The admin content routes: the load and action functions a site's /admin/** shims call.
2
+ // A factory closes over the composed runtime and the GitHub token mint, so the read and
3
+ // commit paths are unit-testable against a fetch double with an injected token, mirroring the
4
+ // email `send` injection in auth-routes. A shim stays one line: `export const load = routes.editLoad`.
5
+ import { redirect, error } from '@sveltejs/kit';
6
+ import { findConcept } from '../content/concepts.js';
7
+ import { frontmatterFromForm, parseMarkdown, dateInputValue, serializeMarkdown } from '../content/frontmatter.js';
8
+ import { isValidId, slugify, filenameFromId } from '../content/ids.js';
9
+ import { appCredentials } from '../github/credentials.js';
10
+ import { listMarkdown, readRaw, commitFile } from '../github/repo.js';
11
+ import { installationToken } from '../github/signing.js';
12
+ import { CommitConflictError } from '../github/types.js';
13
+ /** The signed-in editor the guard resolved, or a login redirect. Kept local to decouple event shapes. */
14
+ function sessionOf(event) {
15
+ const editor = event.locals.editor;
16
+ if (!editor)
17
+ throw redirect(303, '/admin/login');
18
+ return editor;
19
+ }
20
+ /** Look up the concept named by the `[concept]` route param, or a 404. */
21
+ function conceptOf(runtime, params) {
22
+ const concept = findConcept(runtime.concepts, params.concept ?? '');
23
+ if (!concept)
24
+ throw error(404, `Unknown content type: ${params.concept ?? ''}`);
25
+ return concept;
26
+ }
27
+ export function createContentRoutes(runtime, deps = {}) {
28
+ const mintToken = deps.mintToken ?? ((env) => installationToken(appCredentials(runtime.backend, env)));
29
+ /** Layout load for every admin page: the nav, the user, and the active path. */
30
+ function layoutLoad(event) {
31
+ const editor = sessionOf(event);
32
+ return {
33
+ siteName: runtime.siteName,
34
+ user: { displayName: editor.displayName, role: editor.role },
35
+ concepts: runtime.concepts.map((c) => ({ id: c.id, label: c.label })),
36
+ pathname: event.url.pathname,
37
+ canManageEditors: editor.role === 'owner',
38
+ navLabel: runtime.navMenu?.label ?? null,
39
+ };
40
+ }
41
+ /** Redirect /admin to the first concept's list (spec §7.6: land on the first concept). */
42
+ function indexRedirect() {
43
+ const first = runtime.concepts[0];
44
+ if (!first)
45
+ throw error(404, 'No content types configured');
46
+ throw redirect(307, `/admin/${first.id}`);
47
+ }
48
+ /** Read a file's frontmatter for its list row, degrading to the id on any read failure. */
49
+ async function summarize(file, token) {
50
+ try {
51
+ const raw = await readRaw(runtime.backend, file.path, token);
52
+ if (raw === null)
53
+ return { id: file.id, title: file.id, date: null, draft: false };
54
+ const { frontmatter } = parseMarkdown(raw);
55
+ const title = typeof frontmatter.title === 'string' && frontmatter.title.trim() ? frontmatter.title : file.id;
56
+ const date = dateInputValue(frontmatter.date) || null;
57
+ return { id: file.id, title, date, draft: frontmatter.draft === true };
58
+ }
59
+ catch {
60
+ return { id: file.id, title: file.id, date: null, draft: false };
61
+ }
62
+ }
63
+ /** List a concept's entries. A listing failure degrades to an inline error, not a thrown 500. */
64
+ async function listLoad(event) {
65
+ sessionOf(event);
66
+ const concept = conceptOf(runtime, event.params);
67
+ const formError = event.url.searchParams.get('error');
68
+ const base = { conceptId: concept.id, label: concept.label, dated: concept.routing.dated, formError };
69
+ let token;
70
+ try {
71
+ token = await mintToken(event.platform?.env ?? {});
72
+ }
73
+ catch {
74
+ return { ...base, entries: [], error: 'Could not authenticate with GitHub.' };
75
+ }
76
+ try {
77
+ const files = await listMarkdown(runtime.backend, concept.dir, token);
78
+ const entries = await Promise.all(files.map((f) => summarize(f, token)));
79
+ return { ...base, entries, error: null };
80
+ }
81
+ catch {
82
+ return { ...base, entries: [], error: 'Could not load this content type from GitHub.' };
83
+ }
84
+ }
85
+ /** Create a new entry: validate the slug, refuse to clobber, and redirect to the editor. */
86
+ async function createAction(event) {
87
+ sessionOf(event);
88
+ const concept = conceptOf(runtime, event.params);
89
+ const form = await event.request.formData();
90
+ const raw = String(form.get('slug') ?? '').trim() || slugify(String(form.get('title') ?? ''));
91
+ const bounce = (msg) => {
92
+ throw redirect(303, `/admin/${concept.id}?error=${encodeURIComponent(msg)}`);
93
+ };
94
+ if (!isValidId(raw))
95
+ bounce('Enter a valid slug: lowercase letters, numbers, and hyphens.');
96
+ const token = await mintToken(event.platform?.env ?? {});
97
+ const existing = await readRaw(runtime.backend, `${concept.dir}/${filenameFromId(raw)}`, token);
98
+ if (existing !== null)
99
+ bounce('An entry with that slug already exists.');
100
+ throw redirect(303, `/admin/${concept.id}/${raw}?new=1`);
101
+ }
102
+ /** Coerce parsed frontmatter to the form-ready values the editor inputs expect. */
103
+ function formValues(fields, frontmatter) {
104
+ const out = {};
105
+ for (const field of fields) {
106
+ const value = frontmatter[field.name];
107
+ if (field.type === 'date')
108
+ out[field.name] = dateInputValue(value);
109
+ else if (field.type === 'boolean')
110
+ out[field.name] = value === true;
111
+ else if (field.type === 'tags' || field.type === 'freetags')
112
+ out[field.name] = Array.isArray(value) ? value.map(String) : [];
113
+ else
114
+ out[field.name] = typeof value === 'string' ? value : value == null ? '' : String(value);
115
+ }
116
+ return out;
117
+ }
118
+ /** Open a file for editing. A `?new=1` miss yields a blank document; any other miss is a 404. */
119
+ async function editLoad(event) {
120
+ sessionOf(event);
121
+ const concept = conceptOf(runtime, event.params);
122
+ const id = event.params.id ?? '';
123
+ if (!isValidId(id))
124
+ throw error(400, 'Invalid entry id');
125
+ const isNew = event.url.searchParams.get('new') === '1';
126
+ const token = await mintToken(event.platform?.env ?? {});
127
+ const raw = await readRaw(runtime.backend, `${concept.dir}/${filenameFromId(id)}`, token);
128
+ if (raw === null && !isNew)
129
+ throw error(404, 'Entry not found');
130
+ const parsed = raw === null ? { frontmatter: {}, body: '' } : parseMarkdown(raw);
131
+ const title = typeof parsed.frontmatter.title === 'string' && parsed.frontmatter.title.trim() ? parsed.frontmatter.title : id;
132
+ return {
133
+ conceptId: concept.id,
134
+ id,
135
+ label: concept.label,
136
+ fields: concept.fields,
137
+ frontmatter: formValues(concept.fields, parsed.frontmatter),
138
+ body: parsed.body,
139
+ title,
140
+ isNew,
141
+ saved: event.url.searchParams.get('saved') === '1',
142
+ error: event.url.searchParams.get('error'),
143
+ };
144
+ }
145
+ /** Match a commit conflict by class and by name (bundling can alias the class identity). */
146
+ function isConflict(err) {
147
+ return err instanceof CommitConflictError || err?.name === 'CommitConflictError';
148
+ }
149
+ /** Save an edit: validate, then commit with the session editor as author. Fails safe on 409. */
150
+ async function saveAction(event) {
151
+ const editor = sessionOf(event);
152
+ const concept = conceptOf(runtime, event.params);
153
+ const id = event.params.id ?? '';
154
+ // Confine the commit path to the concept dir, built from a validated id (the App token can
155
+ // write anywhere in the repo). Reject before touching GitHub.
156
+ if (!isValidId(id))
157
+ throw error(400, 'Invalid entry id');
158
+ const path = `${concept.dir}/${filenameFromId(id)}`;
159
+ const form = await event.request.formData();
160
+ const body = String(form.get('body') ?? '');
161
+ const isNew = form.get('new') === '1';
162
+ const suffix = isNew ? '&new=1' : '';
163
+ const result = concept.validate(frontmatterFromForm(concept.fields, form), body);
164
+ if (!result.ok) {
165
+ const message = Object.values(result.errors)[0] ?? 'Invalid frontmatter';
166
+ throw redirect(303, `/admin/${concept.id}/${id}?error=${encodeURIComponent(message)}${suffix}`);
167
+ }
168
+ const markdown = serializeMarkdown(result.data, body);
169
+ const token = await mintToken(event.platform?.env ?? {});
170
+ try {
171
+ await commitFile(runtime.backend, path, markdown, { message: `Update ${concept.label.toLowerCase()}: ${id}`, author: { name: editor.displayName, email: editor.email } }, token);
172
+ }
173
+ catch (err) {
174
+ if (isConflict(err)) {
175
+ const message = 'This file changed since you opened it. Reload and reapply your edits.';
176
+ throw redirect(303, `/admin/${concept.id}/${id}?error=${encodeURIComponent(message)}${suffix}`);
177
+ }
178
+ throw err;
179
+ }
180
+ throw redirect(303, `/admin/${concept.id}/${id}?saved=1`);
181
+ }
182
+ return { layoutLoad, indexRedirect, listLoad, createAction, editLoad, saveAction, mintToken };
183
+ }
@@ -0,0 +1,24 @@
1
+ import type { Editor } from '../auth/types.js';
2
+ import type { RequestContext } from './types.js';
3
+ export declare function createEditorRoutes(): {
4
+ editorsLoad: (event: RequestContext) => Promise<{
5
+ editors: Editor[];
6
+ self: string;
7
+ }>;
8
+ addEditorAction: (event: RequestContext) => Promise<import("@sveltejs/kit").ActionFailure<{
9
+ error: string;
10
+ }> | {
11
+ ok: true;
12
+ }>;
13
+ removeEditorAction: (event: RequestContext) => Promise<import("@sveltejs/kit").ActionFailure<{
14
+ error: string;
15
+ }> | {
16
+ ok: true;
17
+ }>;
18
+ setRoleAction: (event: RequestContext) => Promise<import("@sveltejs/kit").ActionFailure<{
19
+ error: string;
20
+ }> | {
21
+ ok: true;
22
+ }>;
23
+ };
24
+ //# sourceMappingURL=editors-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editors-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/editors-routes.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAQjD,wBAAgB,kBAAkB;yBAEE,cAAc,KAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;6BAOzD,cAAc;;;;;gCAcX,cAAc;;;;;2BAgBnB,cAAc;;;;;EAiBnD"}
@@ -0,0 +1,73 @@
1
+ // Owner-gated editor management. The editor table is the allowlist, so add and remove are
2
+ // insert and delete. The anti-lockout rule is the last remaining owner: the system refuses to
3
+ // drop below one owner (spec 7.1), enforced in the store by an atomic guarded write rather
4
+ // than a separate count, so concurrent removals cannot strand the allowlist at zero owners.
5
+ import { fail } from '@sveltejs/kit';
6
+ import { requireOwner } from './guard.js';
7
+ import { requireDb } from '../env.js';
8
+ import { listEditors, findEditor, insertEditor, deleteEditor, setEditorRole, removeOwnerIfNotLast, demoteOwnerIfNotLast, } from '../auth/store.js';
9
+ const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
10
+ function parseRole(value) {
11
+ return value === 'owner' ? 'owner' : 'editor';
12
+ }
13
+ export function createEditorRoutes() {
14
+ /** GET /admin/editors. Owner-only. Returns the allowlist and the acting owner's email. */
15
+ async function editorsLoad(event) {
16
+ const owner = requireOwner(event);
17
+ const editors = await listEditors(requireDb(event.platform?.env ?? {}));
18
+ return { editors, self: owner.email };
19
+ }
20
+ /** POST add an editor. Owner-only. */
21
+ async function addEditorAction(event) {
22
+ requireOwner(event);
23
+ const db = requireDb(event.platform?.env ?? {});
24
+ const form = await event.request.formData();
25
+ const email = String(form.get('email') ?? '').trim().toLowerCase();
26
+ const name = String(form.get('name') ?? '').trim();
27
+ const role = parseRole(form.get('role'));
28
+ if (!EMAIL_RE.test(email) || !name)
29
+ return fail(400, { error: 'Enter a valid email and name' });
30
+ if (await findEditor(db, email))
31
+ return fail(400, { error: 'That editor already exists' });
32
+ await insertEditor(db, email, name, role, Date.now());
33
+ return { ok: true };
34
+ }
35
+ /** POST remove an editor. Owner-only. Refuses the last owner, atomically. */
36
+ async function removeEditorAction(event) {
37
+ requireOwner(event);
38
+ const db = requireDb(event.platform?.env ?? {});
39
+ const form = await event.request.formData();
40
+ const email = String(form.get('email') ?? '').trim().toLowerCase();
41
+ const target = await findEditor(db, email);
42
+ if (!target)
43
+ return fail(400, { error: 'No such editor' });
44
+ if (target.role === 'owner') {
45
+ if (!(await removeOwnerIfNotLast(db, email)))
46
+ return fail(400, { error: 'You cannot remove the last owner' });
47
+ }
48
+ else {
49
+ await deleteEditor(db, email);
50
+ }
51
+ return { ok: true };
52
+ }
53
+ /** POST change an editor's role. Owner-only. Refuses demoting the last owner, atomically. */
54
+ async function setRoleAction(event) {
55
+ requireOwner(event);
56
+ const db = requireDb(event.platform?.env ?? {});
57
+ const form = await event.request.formData();
58
+ const email = String(form.get('email') ?? '').trim().toLowerCase();
59
+ const role = parseRole(form.get('role'));
60
+ const target = await findEditor(db, email);
61
+ if (!target)
62
+ return fail(400, { error: 'No such editor' });
63
+ if (role === 'editor' && target.role === 'owner') {
64
+ if (!(await demoteOwnerIfNotLast(db, email)))
65
+ return fail(400, { error: 'You cannot demote the last owner' });
66
+ }
67
+ else {
68
+ await setEditorRole(db, email, role);
69
+ }
70
+ return { ok: true };
71
+ }
72
+ return { editorsLoad, addEditorAction, removeEditorAction, setRoleAction };
73
+ }
@@ -0,0 +1,9 @@
1
+ import type { Editor } from '../auth/types.js';
2
+ import type { HandleInput, RequestContext } from './types.js';
3
+ /** The SvelteKit `Handle` that guards `/admin/**`. */
4
+ export declare function createAuthGuard(): ({ event, resolve }: HandleInput) => Promise<Response>;
5
+ /** For a protected load/action: the session the guard already resolved, or a login redirect. */
6
+ export declare function requireSession(event: RequestContext): Editor;
7
+ /** For the management surface: a signed-in owner, or 403 for an editor. */
8
+ export declare function requireOwner(event: RequestContext): Editor;
9
+ //# sourceMappingURL=guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/guard.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAW9D,sDAAsD;AACtD,wBAAgB,eAAe,KACA,oBAAoB,WAAW,KAAG,OAAO,CAAC,QAAQ,CAAC,CAYjF;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAI5D;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAI1D"}
@@ -0,0 +1,43 @@
1
+ // The /admin guard, plus the per-load owner/session gates. A site's hooks.server.ts sets
2
+ // `export const handle = createAuthGuard()`. Events are typed structurally, so the engine
3
+ // stays free of a site's App.* ambient types.
4
+ import { redirect, error } from '@sveltejs/kit';
5
+ import { resolveSession } from '../auth/store.js';
6
+ import { COOKIE_NAME } from '../auth/crypto.js';
7
+ /** The login page and the auth endpoints are public; everything else under /admin is gated. */
8
+ function isPublicAdminPath(pathname) {
9
+ return pathname === '/admin/login' || pathname.startsWith('/admin/auth/');
10
+ }
11
+ function isAdminPath(pathname) {
12
+ return pathname === '/admin' || pathname.startsWith('/admin/');
13
+ }
14
+ /** The SvelteKit `Handle` that guards `/admin/**`. */
15
+ export function createAuthGuard() {
16
+ return async function handle({ event, resolve }) {
17
+ const { pathname } = event.url;
18
+ if (!isAdminPath(pathname) || isPublicAdminPath(pathname)) {
19
+ return resolve(event);
20
+ }
21
+ const env = event.platform?.env ?? {};
22
+ const id = event.cookies.get(COOKIE_NAME);
23
+ const editor = id && env.AUTH_DB ? await resolveSession(env.AUTH_DB, id, Date.now()) : null;
24
+ if (!editor)
25
+ throw redirect(303, '/admin/login');
26
+ event.locals.editor = editor;
27
+ return resolve(event);
28
+ };
29
+ }
30
+ /** For a protected load/action: the session the guard already resolved, or a login redirect. */
31
+ export function requireSession(event) {
32
+ const editor = event.locals.editor;
33
+ if (!editor)
34
+ throw redirect(303, '/admin/login');
35
+ return editor;
36
+ }
37
+ /** For the management surface: a signed-in owner, or 403 for an editor. */
38
+ export function requireOwner(event) {
39
+ const editor = requireSession(event);
40
+ if (editor.role !== 'owner')
41
+ throw error(403, 'Owner access required');
42
+ return editor;
43
+ }
@@ -0,0 +1,19 @@
1
+ import type { CairnRuntime } from '../content/types.js';
2
+ import type { GithubKeyEnv } from '../github/credentials.js';
3
+ /** The `/admin/healthz` payload. */
4
+ export interface HealthData {
5
+ ok: boolean;
6
+ checks: {
7
+ githubAppSigning: {
8
+ ok: boolean;
9
+ detail?: string;
10
+ };
11
+ };
12
+ }
13
+ /** Run the signing self-test against the configured App id and the Worker's key secret. */
14
+ export declare function healthLoad(event: {
15
+ platform?: {
16
+ env?: GithubKeyEnv;
17
+ };
18
+ }, runtime: CairnRuntime): Promise<HealthData>;
19
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/health.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,oCAAoC;AACpC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE;QAAE,gBAAgB,EAAE;YAAE,EAAE,EAAE,OAAO,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CAChE;AAED,2FAA2F;AAC3F,wBAAsB,UAAU,CAC9B,KAAK,EAAE;IAAE,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,YAAY,CAAA;KAAE,CAAA;CAAE,EAC5C,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,UAAU,CAAC,CAMrB"}
@@ -0,0 +1,12 @@
1
+ // GET /admin/healthz. Signs a dummy JWT through the real App-signing path so a broken
2
+ // PKCS#1-to-PKCS#8 conversion is caught early (spec §7.8). The payload is pass/fail and a
3
+ // coarse detail only; it never carries the key or a token.
4
+ import { signingSelfTest } from '../github/signing.js';
5
+ /** Run the signing self-test against the configured App id and the Worker's key secret. */
6
+ export async function healthLoad(event, runtime) {
7
+ const key = event.platform?.env?.GITHUB_APP_PRIVATE_KEY_B64;
8
+ const githubAppSigning = key
9
+ ? await signingSelfTest(runtime.backend.appId, key)
10
+ : { ok: false, detail: 'GITHUB_APP_PRIVATE_KEY_B64 is not configured' };
11
+ return { ok: githubAppSigning.ok, checks: { githubAppSigning } };
12
+ }