@glw907/cairn-cms 0.3.1 → 0.4.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 (57) hide show
  1. package/README.md +14 -6
  2. package/dist/auth/admins.d.ts +33 -0
  3. package/dist/auth/admins.d.ts.map +1 -0
  4. package/dist/auth/admins.js +90 -0
  5. package/dist/auth/config.d.ts +2097 -0
  6. package/dist/auth/config.d.ts.map +1 -0
  7. package/dist/auth/config.js +78 -0
  8. package/dist/auth/guard.d.ts +34 -0
  9. package/dist/auth/guard.d.ts.map +1 -0
  10. package/dist/auth/guard.js +47 -0
  11. package/dist/auth/index.d.ts +4 -0
  12. package/dist/auth/index.d.ts.map +1 -0
  13. package/dist/auth/index.js +6 -0
  14. package/dist/auth/schema.d.ts +750 -0
  15. package/dist/auth/schema.d.ts.map +1 -0
  16. package/dist/auth/schema.js +93 -0
  17. package/dist/components/AdminLayout.svelte +6 -6
  18. package/dist/components/AdminLayout.svelte.d.ts +2 -2
  19. package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
  20. package/dist/components/ConfirmPage.svelte +31 -0
  21. package/dist/components/ConfirmPage.svelte.d.ts +11 -0
  22. package/dist/components/ConfirmPage.svelte.d.ts.map +1 -0
  23. package/dist/components/LoginPage.svelte +35 -18
  24. package/dist/components/LoginPage.svelte.d.ts +0 -2
  25. package/dist/components/LoginPage.svelte.d.ts.map +1 -1
  26. package/dist/components/ManageAdmins.svelte +1 -1
  27. package/dist/components/ManageAdmins.svelte.d.ts +1 -1
  28. package/dist/components/ManageAdmins.svelte.d.ts.map +1 -1
  29. package/dist/components/index.d.ts +1 -0
  30. package/dist/components/index.d.ts.map +1 -1
  31. package/dist/components/index.js +1 -0
  32. package/dist/email.d.ts.map +1 -1
  33. package/dist/email.js +15 -7
  34. package/dist/index.d.ts +0 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -2
  37. package/dist/sveltekit/index.d.ts +5 -58
  38. package/dist/sveltekit/index.d.ts.map +1 -1
  39. package/dist/sveltekit/index.js +9 -153
  40. package/package.json +34 -4
  41. package/src/lib/auth/admins.ts +106 -0
  42. package/src/lib/auth/config.ts +108 -0
  43. package/src/lib/auth/guard.ts +60 -0
  44. package/src/lib/auth/index.ts +6 -0
  45. package/src/lib/auth/schema.ts +112 -0
  46. package/src/lib/components/AdminLayout.svelte +6 -6
  47. package/src/lib/components/ConfirmPage.svelte +31 -0
  48. package/src/lib/components/LoginPage.svelte +35 -18
  49. package/src/lib/components/ManageAdmins.svelte +1 -1
  50. package/src/lib/components/index.ts +1 -0
  51. package/src/lib/email.ts +14 -7
  52. package/src/lib/index.ts +2 -2
  53. package/src/lib/sveltekit/index.ts +15 -224
  54. package/dist/auth.d.ts +0 -25
  55. package/dist/auth.d.ts.map +0 -1
  56. package/dist/auth.js +0 -132
  57. package/src/lib/auth.ts +0 -185
package/README.md CHANGED
@@ -13,12 +13,20 @@ sites with completely different markdown pipelines — e.g. [ecnordic.ski](https
13
13
 
14
14
  ## Status
15
15
 
16
- **Early (`0.1.x`)works, API not yet frozen.** The core was built *inside ecnordic.ski first*
17
- (the richer proving ground) with the cairn-core ↔ site-adapter seams designed in from day one,
18
- then extracted into this package and validated on a second design (907.life). The auth, GitHub
19
- commit path, Carta preview, the adapter contract, and the shared admin shell (`/sveltekit`
20
- server logic + `/components` Svelte UI) all run on both sites. The adapter API may still change
21
- before `1.0` (pending a forward-compatibility review) pin a caret range and expect 0.x churn.
16
+ **`0.4.x` — auth on [better-auth](https://better-auth.com); API not yet frozen.** The core was
17
+ built *inside ecnordic.ski first* (the richer proving ground) with the cairn-core ↔ site-adapter
18
+ seams designed in from day one, then extracted into this package and validated on a second design
19
+ (907.life). Editor auth runs on **better-auth (Cloudflare D1 + magic-link)** behind a scanner-safe
20
+ **POST-confirm** flow, with two-tier `owner`/`editor` roles; the GitHub-App commit signer stays
21
+ bespoke. The GitHub commit path, Carta preview, the adapter contract, and the shared admin shell
22
+ (`/sveltekit` server logic + `/components` Svelte UI + `/auth`) all run on both sites. Pin a caret
23
+ range and expect 0.x churn.
24
+
25
+ > **Breaking in `0.4.0`** (from `0.3.x`): editor auth moved off the hand-rolled magic-link/KV/
26
+ > signed-cookie stack onto better-auth. Each site now needs a **D1 binding** (`AUTH_DB`) +
27
+ > committed migrations, an `AUTH_SECRET`, a `/api/auth/[...all]` catch-all + `/admin/auth/confirm`
28
+ > shims, and the new `better-auth` + `drizzle-orm` peer deps. Magic links are now POST-confirm
29
+ > (a confirm page, not a GET link).
22
30
 
23
31
  ## Install
24
32
 
@@ -0,0 +1,33 @@
1
+ import type { Auth } from './config';
2
+ import type { CairnUser } from './guard';
3
+ export interface AdminsData {
4
+ admins: CairnUser[];
5
+ /** Acting owner's email, so the UI can disable self-targeted remove/demote. */
6
+ self: string;
7
+ saved: boolean;
8
+ error: string | null;
9
+ }
10
+ /**
11
+ * The privilege-escalation gate. better-auth's admin API also enforces this server-side (only
12
+ * `owner` holds the admin statements), but checking `locals.user` here gives clean redirect/403
13
+ * UX and lets the mutations guard self-lockout before calling the API. Returns the acting owner.
14
+ */
15
+ export declare function requireOwner(user: CairnUser | null): CairnUser;
16
+ type Ev = {
17
+ locals: {
18
+ auth: Auth;
19
+ user: CairnUser | null;
20
+ };
21
+ request: Request;
22
+ url: URL;
23
+ };
24
+ /** List the allowlist for the manage-editors page. Owner-only. */
25
+ export declare function adminsLoad(event: Ev): Promise<AdminsData>;
26
+ /** Add an editor (create the user). Owner-only. */
27
+ export declare function addAdmin(event: Ev): Promise<never>;
28
+ /** Remove an editor (delete the user). Owner-only; owners can't remove themselves (anti-lockout). */
29
+ export declare function removeAdmin(event: Ev): Promise<never>;
30
+ /** Change an editor's role. Owner-only; owners can't demote themselves (anti-lockout). */
31
+ export declare function setAdminRole(event: Ev): Promise<never>;
32
+ export {};
33
+ //# sourceMappingURL=admins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admins.d.ts","sourceRoot":"","sources":["../../src/lib/auth/admins.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAID;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,CAI9D;AAED,KAAK,EAAE,GAAG;IAAE,MAAM,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,CAAC;AAgBzF,kEAAkE;AAClE,wBAAsB,UAAU,CAAC,KAAK,EAAE,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAa/D;AAED,mDAAmD;AACnD,wBAAsB,QAAQ,CAAC,KAAK,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAYxD;AAED,qGAAqG;AACrG,wBAAsB,WAAW,CAAC,KAAK,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAW3D;AAED,0FAA0F;AAC1F,wBAAsB,YAAY,CAAC,KAAK,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAc5D"}
@@ -0,0 +1,90 @@
1
+ // cairn-core: owner-gated editor management, on better-auth's admin API. The `user` table IS
2
+ // the allowlist (disableSignUp ⇒ only listed emails can sign in), so add/remove editor = create/
3
+ // remove user; role flips go through the admin plugin's access-control roles (owner/editor).
4
+ // These run as SvelteKit form actions; each verifies the acting user is an owner first.
5
+ import { redirect, error } from '@sveltejs/kit';
6
+ const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
7
+ /**
8
+ * The privilege-escalation gate. better-auth's admin API also enforces this server-side (only
9
+ * `owner` holds the admin statements), but checking `locals.user` here gives clean redirect/403
10
+ * UX and lets the mutations guard self-lockout before calling the API. Returns the acting owner.
11
+ */
12
+ export function requireOwner(user) {
13
+ if (!user)
14
+ throw error(401, 'Not signed in');
15
+ if (user.role !== 'owner')
16
+ throw error(403, 'Owner access required');
17
+ return user;
18
+ }
19
+ function asCairnUser(u) {
20
+ return { id: u.id, email: u.email, name: u.name, role: u.role === 'owner' ? 'owner' : 'editor' };
21
+ }
22
+ /** Find an editor by exact (lowercased) email, or undefined. */
23
+ async function findByEmail(event, email) {
24
+ const res = await event.locals.auth.api.listUsers({
25
+ query: { searchValue: email, searchField: 'email', limit: 100 },
26
+ headers: event.request.headers,
27
+ });
28
+ const match = (res.users ?? []).find((u) => u.email.toLowerCase() === email);
29
+ return match ? asCairnUser(match) : undefined;
30
+ }
31
+ /** List the allowlist for the manage-editors page. Owner-only. */
32
+ export async function adminsLoad(event) {
33
+ const owner = requireOwner(event.locals.user);
34
+ const res = await event.locals.auth.api.listUsers({
35
+ query: { limit: 200 },
36
+ headers: event.request.headers,
37
+ });
38
+ const admins = (res.users ?? []).map(asCairnUser).sort((a, b) => a.email.localeCompare(b.email));
39
+ return {
40
+ admins,
41
+ self: owner.email,
42
+ saved: event.url.searchParams.get('saved') === '1',
43
+ error: event.url.searchParams.get('error'),
44
+ };
45
+ }
46
+ /** Add an editor (create the user). Owner-only. */
47
+ export async function addAdmin(event) {
48
+ requireOwner(event.locals.user);
49
+ const form = await event.request.formData();
50
+ const email = String(form.get('email') ?? '').trim().toLowerCase();
51
+ const name = String(form.get('name') ?? '').trim();
52
+ const role = form.get('role') === 'owner' ? 'owner' : 'editor';
53
+ if (!EMAIL_RE.test(email) || !name) {
54
+ throw redirect(303, `/admin/admins?error=${encodeURIComponent('Enter a valid email and name')}`);
55
+ }
56
+ // No password: a magic-link-only user (no credential account), per better-auth's createUser.
57
+ await event.locals.auth.api.createUser({ body: { email, name, role }, headers: event.request.headers });
58
+ throw redirect(303, '/admin/admins?saved=1');
59
+ }
60
+ /** Remove an editor (delete the user). Owner-only; owners can't remove themselves (anti-lockout). */
61
+ export async function removeAdmin(event) {
62
+ const owner = requireOwner(event.locals.user);
63
+ const form = await event.request.formData();
64
+ const email = String(form.get('email') ?? '').trim().toLowerCase();
65
+ if (email === owner.email) {
66
+ throw redirect(303, `/admin/admins?error=${encodeURIComponent("You can't remove yourself")}`);
67
+ }
68
+ const target = await findByEmail(event, email);
69
+ if (!target)
70
+ throw redirect(303, `/admin/admins?error=${encodeURIComponent('No such editor')}`);
71
+ await event.locals.auth.api.removeUser({ body: { userId: target.id }, headers: event.request.headers });
72
+ throw redirect(303, '/admin/admins?saved=1');
73
+ }
74
+ /** Change an editor's role. Owner-only; owners can't demote themselves (anti-lockout). */
75
+ export async function setAdminRole(event) {
76
+ const owner = requireOwner(event.locals.user);
77
+ const form = await event.request.formData();
78
+ const email = String(form.get('email') ?? '').trim().toLowerCase();
79
+ const role = form.get('role') === 'owner' ? 'owner' : 'editor';
80
+ if (email === owner.email && role !== 'owner') {
81
+ throw redirect(303, `/admin/admins?error=${encodeURIComponent("You can't demote yourself")}`);
82
+ }
83
+ const target = await findByEmail(event, email);
84
+ if (!target)
85
+ throw redirect(303, `/admin/admins?error=${encodeURIComponent('No such editor')}`);
86
+ await event.locals.auth.api.setRole({ body: { userId: target.id, role }, headers: event.request.headers });
87
+ // M3: revoke a demoted editor's live sessions so the privilege drop takes effect immediately.
88
+ await event.locals.auth.api.revokeUserSessions({ body: { userId: target.id }, headers: event.request.headers });
89
+ throw redirect(303, '/admin/admins?saved=1');
90
+ }