@glw907/cairn-cms 0.3.0 → 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.
- package/README.md +14 -6
- package/dist/auth/admins.d.ts +33 -0
- package/dist/auth/admins.d.ts.map +1 -0
- package/dist/auth/admins.js +90 -0
- package/dist/auth/config.d.ts +2097 -0
- package/dist/auth/config.d.ts.map +1 -0
- package/dist/auth/config.js +78 -0
- package/dist/auth/guard.d.ts +34 -0
- package/dist/auth/guard.d.ts.map +1 -0
- package/dist/auth/guard.js +47 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/schema.d.ts +750 -0
- package/dist/auth/schema.d.ts.map +1 -0
- package/dist/auth/schema.js +93 -0
- package/dist/components/AdminLayout.svelte +6 -6
- package/dist/components/AdminLayout.svelte.d.ts +2 -2
- package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
- package/dist/components/ConfirmPage.svelte +31 -0
- package/dist/components/ConfirmPage.svelte.d.ts +11 -0
- package/dist/components/ConfirmPage.svelte.d.ts.map +1 -0
- package/dist/components/LoginPage.svelte +35 -18
- package/dist/components/LoginPage.svelte.d.ts +0 -2
- package/dist/components/LoginPage.svelte.d.ts.map +1 -1
- package/dist/components/ManageAdmins.svelte +1 -1
- package/dist/components/ManageAdmins.svelte.d.ts +1 -1
- package/dist/components/ManageAdmins.svelte.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +15 -7
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/sveltekit/index.d.ts +7 -60
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +37 -157
- package/package.json +34 -4
- package/src/lib/auth/admins.ts +106 -0
- package/src/lib/auth/config.ts +108 -0
- package/src/lib/auth/guard.ts +60 -0
- package/src/lib/auth/index.ts +6 -0
- package/src/lib/auth/schema.ts +112 -0
- package/src/lib/components/AdminLayout.svelte +6 -6
- package/src/lib/components/ConfirmPage.svelte +31 -0
- package/src/lib/components/LoginPage.svelte +35 -18
- package/src/lib/components/ManageAdmins.svelte +1 -1
- package/src/lib/components/index.ts +1 -0
- package/src/lib/email.ts +14 -7
- package/src/lib/index.ts +2 -2
- package/src/lib/sveltekit/index.ts +46 -228
- package/dist/auth.d.ts +0 -25
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -132
- 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
|
-
|
|
17
|
-
(the richer proving ground) with the cairn-core ↔ site-adapter
|
|
18
|
-
then extracted into this package and validated on a second design
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
}
|