@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
@@ -1,84 +1,10 @@
1
- import type { CairnUser } from '../auth/guard';
2
- import { type RepoFile } from '../github';
3
- import { type CairnAdapter, type CairnField } from '../adapter';
4
- /** The `platform.env` bindings the content routes read. All optional; the handlers guard. */
5
- export interface AdminEnv {
6
- GITHUB_APP_ID?: string;
7
- GITHUB_APP_INSTALLATION_ID?: string;
8
- GITHUB_APP_PRIVATE_KEY_B64?: string;
9
- }
10
- interface PlatformEvent {
11
- platform?: {
12
- env?: AdminEnv;
13
- };
14
- }
15
- export interface AdminLayoutData {
16
- user: CairnUser | null;
17
- siteName: string;
18
- pathname: string;
19
- }
20
- /**
21
- * Branding + session for every admin page. `siteName` flows from the adapter without pulling
22
- * its plugin graph into client bundles; the import stays server-side in the layout load.
23
- * `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
24
- * (those kit virtual modules have no types outside a kit app, so they can't live in the
25
- * package); reading `event.url` here also opts the layout load into rerunning on navigation.
26
- */
27
- export declare function adminLayoutLoad(event: {
28
- locals: {
29
- user: CairnUser | null;
30
- };
31
- url: URL;
32
- }, adapter: CairnAdapter): AdminLayoutData;
33
- export interface AdminCollectionList {
34
- type: string;
35
- label: string;
36
- files: RepoFile[];
37
- error?: string;
38
- }
39
- /** List every collection's markdown files. A failed listing degrades to an inline error. */
40
- export declare function adminListLoad(event: PlatformEvent, adapter: CairnAdapter): Promise<{
41
- collections: AdminCollectionList[];
42
- }>;
43
- export interface EditData {
44
- type: string;
45
- id: string;
46
- label: string;
47
- fields: CairnField[];
48
- path: string;
49
- body: string;
50
- frontmatter: Record<string, unknown>;
51
- title: string;
52
- saved: boolean;
53
- error: string | null;
54
- }
55
- export declare function editLoad(event: PlatformEvent & {
56
- params: {
57
- type: string;
58
- id: string;
59
- };
60
- url: URL;
61
- }, adapter: CairnAdapter): Promise<EditData>;
62
- export declare function saveCommit(event: PlatformEvent & {
63
- request: Request;
64
- locals: {
65
- user: CairnUser | null;
66
- };
67
- }, adapter: CairnAdapter): Promise<never>;
68
- export interface HealthData {
69
- ok: boolean;
70
- checks: {
71
- githubAppSigning: {
72
- ok: boolean;
73
- detail?: string;
74
- };
75
- };
76
- }
77
- /**
78
- * Deploy-time health check (M2): signs a dummy App JWT to prove the GitHub App key loads and
79
- * the PKCS#1→PKCS#8 conversion still works, before an editor hits it on save. Behind the
80
- * `/admin` guard (signed-in editors only); returns ok/fail with no secret in the body.
81
- */
82
- export declare function healthLoad(event: PlatformEvent): Promise<HealthData>;
83
- export {};
1
+ export { createAuthGuard, requireSession, requireOwner } from './guard.js';
2
+ export { createAuthRoutes, type AuthRoutesConfig } from './auth-routes.js';
3
+ export { createEditorRoutes } from './editors-routes.js';
4
+ export { createContentRoutes } from './content-routes.js';
5
+ export type { NavConcept, LayoutData, EntrySummary, ListData, EditData, ContentEvent, ContentRoutesDeps, } from './content-routes.js';
6
+ export { createNavRoutes } from './nav-routes.js';
7
+ export type { NavLoadData, NavPageOption, NavRoutesDeps } from './nav-routes.js';
8
+ export { healthLoad, type HealthData } from './health.js';
9
+ export type { RequestContext, CookieJar, HandleInput } from './types.js';
84
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAOL,KAAK,QAAQ,EACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAuC,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAErG,6FAA6F;AAC7F,MAAM,WAAW,QAAQ;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,QAAQ,CAAA;KAAE,CAAC;CAC/B;AA2BD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE;IAAE,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,EACvD,OAAO,EAAE,YAAY,GACpB,eAAe,CAEjB;AAID,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,4FAA4F;AAC5F,wBAAsB,aAAa,CACjC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,WAAW,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC,CAajD;AAID,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,EACzE,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,QAAQ,CAAC,CAyBnB;AAID,wBAAsB,UAAU,CAC9B,KAAK,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,EAC/E,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,KAAK,CAAC,CAoDhB;AAID,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE;QAAE,gBAAgB,EAAE;YAAE,EAAE,EAAE,OAAO,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CAChE;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAS1E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
@@ -1,149 +1,8 @@
1
- // cairn-core: the SvelteKit content-route server logic, extracted so each site's `admin/**`
2
- // route files are thin shims (`export const load = (event) => editLoad(event, cairn)`).
3
- //
4
- // SvelteKit's filesystem routing requires the route *files* to live in each site's
5
- // `src/routes/`, but their bodies are identical across sites. Only the adapter differs.
6
- // These functions take the SvelteKit event (typed structurally, to avoid depending on the
7
- // site-generated `App.*` ambient types) plus the site `CairnAdapter`, and throw
8
- // `redirect`/`error` from `@sveltejs/kit` (a peer dependency, so the thrown objects share
9
- // class identity with the host's runtime; otherwise the redirect 500s). Auth/session/manage-editors
10
- // logic lives under `@glw907/cairn-cms/auth`; this module is content-only (list/edit/save).
11
- import { redirect, error } from '@sveltejs/kit';
12
- import matter from 'gray-matter';
13
- import { listMarkdown, readRaw, commitFile, installationToken, signingSelfTest, CommitConflictError, } from '../github';
14
- import { serializeMarkdown } from '../content';
15
- import { findCollection, frontmatterFromForm } from '../adapter';
16
- /**
17
- * Mint a GitHub App installation token for *reads* when the App is configured, else undefined
18
- * (reads then fall back to anonymous). Authenticated reads get the 5000/hr limit; anonymous
19
- * reads share GitHub's 60/hr-per-IP budget across Cloudflare's egress IPs, so they 403 in prod.
20
- * A mint failure degrades gracefully to anonymous rather than 500ing. Unlike the commit path,
21
- * where a missing App is fatal, a read can still succeed unauthenticated.
22
- */
23
- async function readToken(env) {
24
- if (!env?.GITHUB_APP_ID || !env.GITHUB_APP_INSTALLATION_ID || !env.GITHUB_APP_PRIVATE_KEY_B64) {
25
- return undefined;
26
- }
27
- try {
28
- return await installationToken({
29
- appId: env.GITHUB_APP_ID,
30
- installationId: env.GITHUB_APP_INSTALLATION_ID,
31
- privateKeyB64: env.GITHUB_APP_PRIVATE_KEY_B64,
32
- });
33
- }
34
- catch (err) {
35
- console.error('read token mint failed; falling back to anonymous read:', err);
36
- return undefined;
37
- }
38
- }
39
- /**
40
- * Branding + session for every admin page. `siteName` flows from the adapter without pulling
41
- * its plugin graph into client bundles; the import stays server-side in the layout load.
42
- * `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
43
- * (those kit virtual modules have no types outside a kit app, so they can't live in the
44
- * package); reading `event.url` here also opts the layout load into rerunning on navigation.
45
- */
46
- export function adminLayoutLoad(event, adapter) {
47
- return { user: event.locals.user, siteName: adapter.siteName, pathname: event.url.pathname };
48
- }
49
- /** List every collection's markdown files. A failed listing degrades to an inline error. */
50
- export async function adminListLoad(event, adapter) {
51
- const token = await readToken(event.platform?.env);
52
- const collections = await Promise.all(adapter.collections.map(async ({ type, label, dir }) => {
53
- try {
54
- return { type, label, files: await listMarkdown(adapter.backend, dir, token) };
55
- }
56
- catch (err) {
57
- // A failed listing (rate limit, network) shouldn't 500 the whole admin.
58
- return { type, label, files: [], error: err instanceof Error ? err.message : 'Failed to load' };
59
- }
60
- }));
61
- return { collections };
62
- }
63
- export async function editLoad(event, adapter) {
64
- const collection = findCollection(adapter, event.params.type);
65
- if (!collection)
66
- throw error(404, 'Unknown collection');
67
- const token = await readToken(event.platform?.env);
68
- const path = `${collection.dir}/${event.params.id}.md`;
69
- const raw = await readRaw(adapter.backend, path, token);
70
- if (raw === null)
71
- throw error(404, 'Content not found');
72
- // Split frontmatter from body server-side; the editor form binds to the frontmatter and
73
- // the Carta editor binds to the body, and /admin/save reassembles them on commit.
74
- const { data: frontmatter, content: body } = matter(raw);
75
- return {
76
- type: event.params.type,
77
- id: event.params.id,
78
- label: collection.label,
79
- fields: collection.fields,
80
- path,
81
- body,
82
- frontmatter,
83
- title: typeof frontmatter.title === 'string' ? frontmatter.title : event.params.id,
84
- saved: event.url.searchParams.get('saved') === '1',
85
- error: event.url.searchParams.get('error'),
86
- };
87
- }
88
- // ── /admin/save (POST) ──────────────────────────────────────────────────────
89
- export async function saveCommit(event, adapter) {
90
- const user = event.locals.user;
91
- if (!user)
92
- throw error(401, 'Not signed in');
93
- const env = event.platform?.env;
94
- if (!env?.GITHUB_APP_ID || !env.GITHUB_APP_INSTALLATION_ID || !env.GITHUB_APP_PRIVATE_KEY_B64) {
95
- throw error(500, 'GitHub App is not configured');
96
- }
97
- const form = await event.request.formData();
98
- const type = String(form.get('type') ?? '');
99
- const id = String(form.get('id') ?? '');
100
- const body = String(form.get('body') ?? '');
101
- const collection = findCollection(adapter, type);
102
- if (!collection || !id)
103
- throw error(400, 'Bad request');
104
- // Build frontmatter from the posted fields and validate against the collection's schema; a
105
- // bad field bounces back to the editor with the validator's message rather than 500ing.
106
- let frontmatter;
107
- try {
108
- frontmatter = collection.validate(frontmatterFromForm(collection, form), `${id}.md`);
109
- }
110
- catch (err) {
111
- const message = err instanceof Error ? err.message : 'Invalid frontmatter';
112
- throw redirect(303, `/admin/edit/${type}/${id}?error=${encodeURIComponent(message)}`);
113
- }
114
- const markdown = serializeMarkdown(frontmatter, body);
115
- const token = await installationToken({
116
- appId: env.GITHUB_APP_ID,
117
- installationId: env.GITHUB_APP_INSTALLATION_ID,
118
- privateKeyB64: env.GITHUB_APP_PRIVATE_KEY_B64,
119
- });
120
- try {
121
- await commitFile(adapter.backend, `${collection.dir}/${id}.md`, markdown, { message: `Update ${collection.label.toLowerCase()}: ${id}`, author: { name: user.name, email: user.email } }, token);
122
- }
123
- catch (err) {
124
- // Concurrent-edit 409 (C3): fail safe. Bounce back with a reload prompt; the editor reloads
125
- // the current version and reapplies. Any other error is unexpected, so rethrow.
126
- if (err instanceof CommitConflictError) {
127
- const message = 'This file changed since you opened it. Reload and reapply your edits.';
128
- throw redirect(303, `/admin/edit/${type}/${id}?error=${encodeURIComponent(message)}`);
129
- }
130
- throw err;
131
- }
132
- throw redirect(303, `/admin/edit/${type}/${id}?saved=1`);
133
- }
134
- /**
135
- * Deploy-time health check (M2): signs a dummy App JWT to prove the GitHub App key loads and
136
- * the PKCS#1→PKCS#8 conversion still works, before an editor hits it on save. Behind the
137
- * `/admin` guard (signed-in editors only); returns ok/fail with no secret in the body.
138
- */
139
- export async function healthLoad(event) {
140
- const env = event.platform?.env;
141
- let githubAppSigning;
142
- if (env?.GITHUB_APP_ID && env.GITHUB_APP_PRIVATE_KEY_B64) {
143
- githubAppSigning = await signingSelfTest(env.GITHUB_APP_ID, env.GITHUB_APP_PRIVATE_KEY_B64);
144
- }
145
- else {
146
- githubAppSigning = { ok: false, detail: 'GitHub App not configured' };
147
- }
148
- return { ok: githubAppSigning.ok, checks: { githubAppSigning } };
149
- }
1
+ // SvelteKit server logic consumed by site route shims: the guard plus the auth, editor,
2
+ // content, and health route factories and functions.
3
+ export { createAuthGuard, requireSession, requireOwner } from './guard.js';
4
+ export { createAuthRoutes } from './auth-routes.js';
5
+ export { createEditorRoutes } from './editors-routes.js';
6
+ export { createContentRoutes } from './content-routes.js';
7
+ export { createNavRoutes } from './nav-routes.js';
8
+ export { healthLoad } from './health.js';
@@ -0,0 +1,30 @@
1
+ import { type GithubKeyEnv } from '../github/credentials.js';
2
+ import { type NavNode } from '../nav/site-config.js';
3
+ import type { CairnRuntime } from '../content/types.js';
4
+ import type { ContentEvent } from './content-routes.js';
5
+ /** One page option for the URL picker datalist. */
6
+ export interface NavPageOption {
7
+ label: string;
8
+ url: string;
9
+ }
10
+ /** The nav editor's load data: the menu meta, the current tree, page options, and flags. */
11
+ export interface NavLoadData {
12
+ menu: {
13
+ name: string;
14
+ label: string;
15
+ maxDepth: number;
16
+ };
17
+ tree: NavNode[];
18
+ pages: NavPageOption[];
19
+ saved: boolean;
20
+ error: string | null;
21
+ }
22
+ /** Injectable dependencies; tests stub the token mint to avoid signing a real key. */
23
+ export interface NavRoutesDeps {
24
+ mintToken?: (env: GithubKeyEnv) => Promise<string>;
25
+ }
26
+ export declare function createNavRoutes(runtime: CairnRuntime, deps?: NavRoutesDeps): {
27
+ navLoad: (event: ContentEvent) => Promise<NavLoadData>;
28
+ navSave: (event: ContentEvent) => Promise<never>;
29
+ };
30
+ //# sourceMappingURL=nav-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav-routes.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/nav-routes.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAI7E,OAAO,EAA0D,KAAK,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAC7G,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,4FAA4F;AAC5F,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACpD;AAcD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,GAAE,aAAkB;qBAqB/C,YAAY,KAAG,OAAO,CAAC,WAAW,CAAC;qBAiCnC,YAAY,KAAG,OAAO,CAAC,KAAK,CAAC;EAuC5D"}
@@ -0,0 +1,103 @@
1
+ // The admin nav-editing routes: the load and save a site's /admin/nav shim calls. A factory closes
2
+ // over the composed runtime and the GitHub token mint, mirroring createContentRoutes, so the read
3
+ // and commit paths are unit-testable against a fetch double with an injected token.
4
+ import { redirect, error } from '@sveltejs/kit';
5
+ import { appCredentials } from '../github/credentials.js';
6
+ import { installationToken } from '../github/signing.js';
7
+ import { listMarkdown, readRaw, commitFile } from '../github/repo.js';
8
+ import { CommitConflictError } from '../github/types.js';
9
+ import { parseSiteConfig, extractMenu, validateNavTree, setMenu } from '../nav/site-config.js';
10
+ /** The signed-in editor the guard resolved, or a login redirect. */
11
+ function sessionOf(event) {
12
+ const editor = event.locals.editor;
13
+ if (!editor)
14
+ throw redirect(303, '/admin/login');
15
+ return editor;
16
+ }
17
+ /** Match a commit conflict by class and by name (bundling can alias the class identity). */
18
+ function isConflict(err) {
19
+ return err instanceof CommitConflictError || err?.name === 'CommitConflictError';
20
+ }
21
+ export function createNavRoutes(runtime, deps = {}) {
22
+ const mintToken = deps.mintToken ?? ((env) => installationToken(appCredentials(runtime.backend, env)));
23
+ /** List page-like concepts (routable, not dated) for the URL picker. Best-effort per concept. */
24
+ async function pageOptions(token) {
25
+ const pageConcepts = runtime.concepts.filter((c) => c.routing.routable && !c.routing.dated);
26
+ const lists = await Promise.all(pageConcepts.map(async (c) => {
27
+ try {
28
+ const files = await listMarkdown(runtime.backend, c.dir, token);
29
+ return files.map((f) => ({ label: f.id, url: `/${f.id}` }));
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ }));
35
+ return lists.flat();
36
+ }
37
+ /** Load the nav editor. A missing or unparsable config degrades to an empty tree so it still opens. */
38
+ async function navLoad(event) {
39
+ sessionOf(event);
40
+ const config = runtime.navMenu;
41
+ if (!config)
42
+ throw error(404, 'No navigation menu configured');
43
+ const maxDepth = config.maxDepth ?? 2;
44
+ const menu = { name: config.menuName, label: config.label, maxDepth };
45
+ let token;
46
+ try {
47
+ token = await mintToken(event.platform?.env ?? {});
48
+ }
49
+ catch {
50
+ return { menu, tree: [], pages: [], saved: false, error: 'Could not authenticate with GitHub.' };
51
+ }
52
+ let tree = [];
53
+ try {
54
+ const raw = await readRaw(runtime.backend, config.configPath, token);
55
+ if (raw !== null)
56
+ tree = extractMenu(parseSiteConfig(raw), config.menuName, maxDepth);
57
+ }
58
+ catch {
59
+ // A malformed or unreadable config degrades to an empty tree; the first save writes a clean menu.
60
+ tree = [];
61
+ }
62
+ return {
63
+ menu,
64
+ tree,
65
+ pages: await pageOptions(token),
66
+ saved: event.url.searchParams.get('saved') === '1',
67
+ error: event.url.searchParams.get('error'),
68
+ };
69
+ }
70
+ /** Save the nav tree: validate, then read-modify-commit the one menu with the session editor as author. */
71
+ async function navSave(event) {
72
+ const editor = sessionOf(event);
73
+ const config = runtime.navMenu;
74
+ if (!config)
75
+ throw error(404, 'No navigation menu configured');
76
+ const maxDepth = config.maxDepth ?? 2;
77
+ const form = await event.request.formData();
78
+ let tree;
79
+ try {
80
+ tree = validateNavTree(JSON.parse(String(form.get('tree') ?? '[]')), maxDepth);
81
+ }
82
+ catch (err) {
83
+ const message = err instanceof Error ? err.message : 'Invalid navigation';
84
+ throw redirect(303, `/admin/nav?error=${encodeURIComponent(message)}`);
85
+ }
86
+ const token = await mintToken(event.platform?.env ?? {});
87
+ const raw = await readRaw(runtime.backend, config.configPath, token);
88
+ if (raw === null)
89
+ throw error(404, 'Site config not found');
90
+ try {
91
+ await commitFile(runtime.backend, config.configPath, setMenu(raw, config.menuName, tree), { message: `Update ${config.label.toLowerCase()}`, author: { name: editor.displayName, email: editor.email } }, token);
92
+ }
93
+ catch (err) {
94
+ if (isConflict(err)) {
95
+ const message = 'The site config changed since you opened it. Reload and reapply your edits.';
96
+ throw redirect(303, `/admin/nav?error=${encodeURIComponent(message)}`);
97
+ }
98
+ throw err;
99
+ }
100
+ throw redirect(303, '/admin/nav?saved=1');
101
+ }
102
+ return { navLoad, navSave };
103
+ }
@@ -0,0 +1,32 @@
1
+ import type { AuthEnv, Editor } from '../auth/types.js';
2
+ export interface CookieSetOptions {
3
+ path: string;
4
+ httpOnly?: boolean;
5
+ secure?: boolean;
6
+ sameSite?: 'lax' | 'strict' | 'none';
7
+ maxAge?: number;
8
+ }
9
+ export interface CookieJar {
10
+ get(name: string): string | undefined;
11
+ set(name: string, value: string, opts: CookieSetOptions): void;
12
+ delete(name: string, opts: {
13
+ path: string;
14
+ }): void;
15
+ }
16
+ export interface RequestContext {
17
+ url: URL;
18
+ request: Request;
19
+ cookies: CookieJar;
20
+ locals: {
21
+ editor?: Editor | null;
22
+ };
23
+ platform?: {
24
+ env?: AuthEnv;
25
+ };
26
+ setHeaders(headers: Record<string, string>): void;
27
+ }
28
+ export interface HandleInput {
29
+ event: RequestContext;
30
+ resolve(event: RequestContext): Promise<Response> | Response;
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACtC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC/D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,SAAS,CAAC;IACnB,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACnC,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAG7B,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACnD;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,cAAc,CAAC;IACtB,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;CAC9D"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@glw907/cairn-cms",
3
- "version": "0.5.0",
3
+ "version": "0.6.0-rc.0",
4
4
  "description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
5
5
  "type": "module",
6
+ "sideEffects": [
7
+ "**/*.svelte",
8
+ "**/*.css"
9
+ ],
6
10
  "license": "MIT",
7
11
  "author": "Geoff Wright",
8
12
  "repository": {
@@ -19,75 +23,47 @@
19
23
  ],
20
24
  "scripts": {
21
25
  "package": "svelte-package",
22
- "package:watch": "svelte-package --watch",
23
- "prepublishOnly": "svelte-package",
26
+ "check:package": "npm run package && publint --strict && attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm internal-resolution-error",
27
+ "prepare": "svelte-package",
28
+ "check": "svelte-check --tsconfig ./tsconfig.json",
24
29
  "test": "vitest run",
25
30
  "test:watch": "vitest",
26
- "auth:schema": "better-auth generate --config auth.cli.ts --output src/lib/auth/schema.ts -y",
27
- "auth:sql": "drizzle-kit generate"
31
+ "test:unit": "vitest run --project unit",
32
+ "test:integration": "vitest run --project integration",
33
+ "test:component": "vitest run --project component"
28
34
  },
29
35
  "exports": {
30
36
  ".": {
31
- "types": "./src/lib/index.ts",
32
- "svelte": "./src/lib/index.ts",
33
- "default": "./src/lib/index.ts"
37
+ "types": "./dist/index.d.ts",
38
+ "svelte": "./dist/index.js",
39
+ "default": "./dist/index.js"
34
40
  },
35
41
  "./sveltekit": {
36
- "types": "./src/lib/sveltekit/index.ts",
37
- "svelte": "./src/lib/sveltekit/index.ts",
38
- "default": "./src/lib/sveltekit/index.ts"
42
+ "types": "./dist/sveltekit/index.d.ts",
43
+ "svelte": "./dist/sveltekit/index.js",
44
+ "default": "./dist/sveltekit/index.js"
39
45
  },
40
46
  "./components": {
41
- "types": "./src/lib/components/index.ts",
42
- "svelte": "./src/lib/components/index.ts",
43
- "default": "./src/lib/components/index.ts"
44
- },
45
- "./auth": {
46
- "types": "./src/lib/auth/index.ts",
47
- "svelte": "./src/lib/auth/index.ts",
48
- "default": "./src/lib/auth/index.ts"
47
+ "types": "./dist/components/index.d.ts",
48
+ "svelte": "./dist/components/index.js",
49
+ "default": "./dist/components/index.js"
49
50
  },
50
51
  "./package.json": "./package.json"
51
52
  },
52
- "publishConfig": {
53
- "exports": {
54
- ".": {
55
- "types": "./dist/index.d.ts",
56
- "svelte": "./dist/index.js",
57
- "default": "./dist/index.js"
58
- },
59
- "./sveltekit": {
60
- "types": "./dist/sveltekit/index.d.ts",
61
- "svelte": "./dist/sveltekit/index.js",
62
- "default": "./dist/sveltekit/index.js"
63
- },
64
- "./components": {
65
- "types": "./dist/components/index.d.ts",
66
- "svelte": "./dist/components/index.js",
67
- "default": "./dist/components/index.js"
68
- },
69
- "./auth": {
70
- "types": "./dist/auth/index.d.ts",
71
- "svelte": "./dist/auth/index.js",
72
- "default": "./dist/auth/index.js"
73
- },
74
- "./package.json": "./package.json"
75
- }
76
- },
77
53
  "files": [
78
54
  "dist",
79
55
  "src/lib"
80
56
  ],
81
57
  "peerDependencies": {
82
58
  "@sveltejs/kit": "^2",
83
- "better-auth": "^1.6",
84
59
  "carta-md": "^4.11",
85
- "drizzle-orm": ">=0.40 <1",
86
60
  "svelte": "^5.0.0"
87
61
  },
88
62
  "dependencies": {
63
+ "@rodrigodagostino/svelte-sortable-list": "^2.1.17",
89
64
  "@types/hast": "^3.0.4",
90
65
  "@types/mdast": "^4.0.4",
66
+ "dompurify": "^3.4.7",
91
67
  "gray-matter": "^4",
92
68
  "hastscript": "^9.0.1",
93
69
  "mdast-util-directive": "^3.1.0",
@@ -99,23 +75,27 @@
99
75
  "remark-parse": "^11.0.0",
100
76
  "remark-rehype": "^11.1.2",
101
77
  "unified": "^11.0.5",
102
- "unist-util-visit": "^5.1.0"
78
+ "unist-util-visit": "^5.1.0",
79
+ "yaml": "^2"
103
80
  },
104
81
  "devDependencies": {
105
- "@better-auth/cli": "^1.4.21",
106
- "@cloudflare/workers-types": "^4.20260405.1",
107
- "@sveltejs/kit": "^2",
82
+ "@arethetypeswrong/cli": "^0.18.3",
83
+ "@cloudflare/vitest-pool-workers": "^0.16",
84
+ "@cloudflare/workers-types": "^4.20260501.0",
85
+ "@sveltejs/kit": "^2.61",
108
86
  "@sveltejs/package": "^2",
109
- "@sveltejs/vite-plugin-svelte": "^7",
110
- "@types/better-sqlite3": "^7.6.13",
111
- "better-auth": "^1.6.11",
112
- "better-sqlite3": "^12.10.0",
87
+ "@sveltejs/vite-plugin-svelte": "^7.1",
88
+ "@vitest/browser": "^4.1.7",
89
+ "@vitest/browser-playwright": "^4.1.7",
113
90
  "carta-md": "^4.11",
114
- "drizzle-kit": "^0.31.10",
115
- "drizzle-orm": "^0.45.2",
116
- "svelte": "^5",
91
+ "playwright": "^1.60.0",
92
+ "publint": "^0.3.21",
93
+ "svelte": "^5.55",
117
94
  "svelte-check": "^4",
118
95
  "typescript": "^6.0.3",
119
- "vitest": "^4.1.6"
96
+ "vite": "^8.0",
97
+ "vitest": "^4.1",
98
+ "vitest-browser-svelte": "^2.1.1",
99
+ "wrangler": "^4"
120
100
  }
121
101
  }
@@ -0,0 +1,37 @@
1
+ // Token and session-id generation plus SHA-256 token hashing, on Web Crypto so the
2
+ // code runs unchanged in workerd. The store keeps only the hash of a token, never the
3
+ // token itself (spec 7.1).
4
+
5
+ /** The session cookie name. */
6
+ export const COOKIE_NAME = 'cairn_session';
7
+
8
+ /** Magic-link tokens live 10 minutes. */
9
+ export const TOKEN_TTL_MS = 10 * 60 * 1000;
10
+
11
+ /** Sessions live 30 days. */
12
+ export const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
13
+
14
+ function randomBase64Url(byteLength = 32): string {
15
+ const bytes = new Uint8Array(byteLength);
16
+ crypto.getRandomValues(bytes);
17
+ let binary = '';
18
+ for (const b of bytes) binary += String.fromCharCode(b);
19
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
20
+ }
21
+
22
+ /** A fresh 256-bit magic-link token, url-safe. */
23
+ export function generateToken(): string {
24
+ return randomBase64Url(32);
25
+ }
26
+
27
+ /** A fresh 256-bit session id, url-safe. */
28
+ export function generateSessionId(): string {
29
+ return randomBase64Url(32);
30
+ }
31
+
32
+ /** The lowercase hex SHA-256 of a token, for storage and lookup. */
33
+ export async function hashToken(token: string): Promise<string> {
34
+ const data = new TextEncoder().encode(token);
35
+ const digest = await crypto.subtle.digest('SHA-256', data);
36
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
37
+ }