@glw907/cairn-cms 0.4.0 → 0.5.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 +4 -4
- package/dist/adapter.d.ts +10 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/auth/config.d.ts +9 -9
- package/dist/auth/config.d.ts.map +1 -1
- package/dist/auth/config.js +5 -5
- package/dist/auth/guard.d.ts +1 -1
- package/dist/auth/guard.d.ts.map +1 -1
- package/dist/auth/guard.js +2 -2
- package/dist/carta.d.ts +1 -1
- package/dist/carta.d.ts.map +1 -1
- package/dist/components/AdminLayout.svelte +3 -3
- package/dist/components/AdminList.svelte +1 -1
- package/dist/components/ConfirmPage.svelte +2 -2
- package/dist/components/EditPage.svelte +5 -5
- package/dist/components/LoginPage.svelte +5 -5
- package/dist/email.js +4 -4
- package/dist/github.d.ts +22 -2
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +40 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/render/glyph.d.ts +6 -0
- package/dist/render/glyph.d.ts.map +1 -0
- package/dist/render/glyph.js +5 -0
- package/dist/render/index.d.ts +6 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +8 -0
- package/dist/render/pipeline.d.ts +16 -0
- package/dist/render/pipeline.d.ts.map +1 -0
- package/dist/render/pipeline.js +29 -0
- package/dist/render/registry.d.ts +28 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +11 -0
- package/dist/render/rehype-dispatch.d.ts +24 -0
- package/dist/render/rehype-dispatch.d.ts.map +1 -0
- package/dist/render/rehype-dispatch.js +86 -0
- package/dist/render/remark-directives.d.ts +4 -0
- package/dist/render/remark-directives.d.ts.map +1 -0
- package/dist/render/remark-directives.js +74 -0
- package/dist/sveltekit/index.d.ts +17 -2
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +33 -6
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -2
- package/package.json +15 -3
- package/src/lib/adapter.ts +12 -3
- package/src/lib/auth/config.ts +6 -6
- package/src/lib/auth/guard.ts +3 -3
- package/src/lib/carta.ts +2 -2
- package/src/lib/components/AdminLayout.svelte +3 -3
- package/src/lib/components/AdminList.svelte +1 -1
- package/src/lib/components/ConfirmPage.svelte +2 -2
- package/src/lib/components/EditPage.svelte +5 -5
- package/src/lib/components/LoginPage.svelte +5 -5
- package/src/lib/email.ts +4 -4
- package/src/lib/github.ts +38 -6
- package/src/lib/index.ts +1 -0
- package/src/lib/render/glyph.ts +14 -0
- package/src/lib/render/index.ts +8 -0
- package/src/lib/render/pipeline.ts +37 -0
- package/src/lib/render/registry.ts +36 -0
- package/src/lib/render/rehype-dispatch.ts +97 -0
- package/src/lib/render/remark-directives.ts +71 -0
- package/src/lib/sveltekit/index.ts +54 -13
- package/src/lib/utils.ts +2 -2
package/README.md
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
An embedded, **magic-link**, GitHub-committing CMS for SvelteKit + Cloudflare sites.
|
|
4
4
|
Non-technical authors log in by email (no GitHub account, no password), edit **raw
|
|
5
|
-
markdown** in a [Carta](https://github.com/BearToCode/carta) editor, and save
|
|
5
|
+
markdown** in a [Carta](https://github.com/BearToCode/carta) editor, and save. Each save
|
|
6
6
|
commits to `main` via a **GitHub App** (committer = `cairn-cms[bot]`, author = the editor)
|
|
7
7
|
and auto-deploys.
|
|
8
8
|
|
|
9
9
|
It is **design-agnostic**: each consumer site supplies an adapter (collections, slug
|
|
10
10
|
convention, frontmatter schema, and its own `renderPreview(md)`), so the same engine drives
|
|
11
|
-
sites with completely different markdown pipelines
|
|
12
|
-
(remark→rehype directive pipeline) and [907.life](https://907.life) (plain `remark-html`).
|
|
11
|
+
sites with completely different markdown pipelines (e.g. [ecnordic.ski](https://ecnordic.ski)
|
|
12
|
+
(remark→rehype directive pipeline) and [907.life](https://907.life) (plain `remark-html`)).
|
|
13
13
|
|
|
14
14
|
## Status
|
|
15
15
|
|
|
16
|
-
**`0.4.x
|
|
16
|
+
**`0.4.x`: auth on [better-auth](https://better-auth.com); API not yet frozen.** The core was
|
|
17
17
|
built *inside ecnordic.ski first* (the richer proving ground) with the cairn-core ↔ site-adapter
|
|
18
18
|
seams designed in from day one, then extracted into this package and validated on a second design
|
|
19
19
|
(907.life). Editor auth runs on **better-auth (Cloudflare D1 + magic-link)** behind a scanner-safe
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PreviewPlugins } from './carta';
|
|
2
2
|
import type { RepoRef } from './github';
|
|
3
|
+
import type { ComponentRegistry } from './render';
|
|
3
4
|
interface FieldBase {
|
|
4
5
|
/** Frontmatter key and form input name. */
|
|
5
6
|
name: string;
|
|
@@ -44,13 +45,21 @@ export interface CairnCollection {
|
|
|
44
45
|
export interface CairnAdapter {
|
|
45
46
|
/** Branding + magic-link email copy. */
|
|
46
47
|
siteName: string;
|
|
47
|
-
/** From: address for magic-link email
|
|
48
|
+
/** From: address for magic-link email (must be a domain-authenticated sender). */
|
|
48
49
|
sender: string;
|
|
49
50
|
/** The repository the admin reads content from and commits to. */
|
|
50
51
|
backend: RepoRef;
|
|
51
52
|
/** Site plugin set for the Carta preview (parity with the live render). */
|
|
52
53
|
preview: PreviewPlugins;
|
|
53
54
|
collections: CairnCollection[];
|
|
55
|
+
/**
|
|
56
|
+
* The site's component registry: the single declaration of its directive
|
|
57
|
+
* components (R10a). Rendering parity already flows through `preview`; this
|
|
58
|
+
* exposes the same registry so the editor's insert-component palette can read
|
|
59
|
+
* `registry.defs`. Optional: a site with no rich components (e.g. 907.life) may
|
|
60
|
+
* omit it or supply an empty registry.
|
|
61
|
+
*/
|
|
62
|
+
registry?: ComponentRegistry;
|
|
54
63
|
}
|
|
55
64
|
/** Look up a collection by its route segment, or undefined if the segment is unknown. */
|
|
56
65
|
export declare function findCollection(adapter: CairnAdapter, type: string): CairnCollection | undefined;
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/lib/adapter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/lib/adapter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,UAAU,SAAS;IACjB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,IAAI,EAAE,SAAS,CAAC;CACjB;AACD,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AACD,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,SAAS,GACT,aAAa,GACb,YAAY,GACZ,SAAS,GACT,aAAa,CAAC;AAElB,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,6CAA6C;IAC7C,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACjE;AAED,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,OAAO,EAAE,cAAc,CAAC;IACxB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAE/F;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,eAAe,EAC3B,IAAI,EAAE,QAAQ,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB"}
|
package/dist/auth/config.d.ts
CHANGED
|
@@ -17,16 +17,16 @@ export interface AuthBranding {
|
|
|
17
17
|
/** The `From:` address used when sending magic-link emails. */
|
|
18
18
|
sender: string;
|
|
19
19
|
}
|
|
20
|
-
/** The drizzle adapter result `betterAuth` consumes
|
|
20
|
+
/** The drizzle adapter result `betterAuth` consumes (same provider/schema everywhere). */
|
|
21
21
|
type DrizzleDb = Parameters<typeof drizzleAdapter>[0];
|
|
22
22
|
/**
|
|
23
23
|
* The shared better-auth config. Kept separate from `createAuth` so the test harness can run
|
|
24
24
|
* the EXACT plugin set (allowlist semantics, expiry, POST-confirm send) over an in-memory
|
|
25
|
-
* SQLite instead of D1. `disableSignUp:true` makes the `user` table the editor allowlist
|
|
25
|
+
* SQLite instead of D1. `disableSignUp:true` makes the `user` table the editor allowlist:
|
|
26
26
|
* magic-link never auto-creates, so the only way in is the owner-gated admin `createUser`
|
|
27
27
|
* (see auth/admins.ts). `adminRoles:['owner']` lets owners (not the default `admin` role)
|
|
28
28
|
* drive the admin API. Tokens are stored hashed and consumed atomically on first verify
|
|
29
|
-
* (better-auth GHSA-hc7v-rggr-4hvx)
|
|
29
|
+
* (better-auth GHSA-hc7v-rggr-4hvx), single-use by construction (C1).
|
|
30
30
|
*/
|
|
31
31
|
export declare function buildAuth(opts: {
|
|
32
32
|
database: DrizzleDb;
|
|
@@ -185,7 +185,7 @@ export declare function buildAuth(opts: {
|
|
|
185
185
|
userId: string;
|
|
186
186
|
expiresAt: Date;
|
|
187
187
|
token: string;
|
|
188
|
-
ipAddress
|
|
188
|
+
ipAddress?: string | null | undefined;
|
|
189
189
|
userAgent?: string | null | undefined;
|
|
190
190
|
} & Record<string, unknown>, ctx: import("better-auth").GenericEndpointContext | null): Promise<void>;
|
|
191
191
|
};
|
|
@@ -956,8 +956,8 @@ export declare function buildAuth(opts: {
|
|
|
956
956
|
$Infer: {
|
|
957
957
|
body: {
|
|
958
958
|
permissions: {
|
|
959
|
-
readonly user?: ("
|
|
960
|
-
readonly session?: ("
|
|
959
|
+
readonly user?: ("delete" | "list" | "create" | "set-role" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "get" | "update")[] | undefined;
|
|
960
|
+
readonly session?: ("delete" | "list" | "revoke")[] | undefined;
|
|
961
961
|
};
|
|
962
962
|
} & {
|
|
963
963
|
userId?: string | undefined;
|
|
@@ -1217,7 +1217,7 @@ export declare function createAuth(env: AuthEnv, branding: AuthBranding): import
|
|
|
1217
1217
|
userId: string;
|
|
1218
1218
|
expiresAt: Date;
|
|
1219
1219
|
token: string;
|
|
1220
|
-
ipAddress
|
|
1220
|
+
ipAddress?: string | null | undefined;
|
|
1221
1221
|
userAgent?: string | null | undefined;
|
|
1222
1222
|
} & Record<string, unknown>, ctx: import("better-auth").GenericEndpointContext | null): Promise<void>;
|
|
1223
1223
|
};
|
|
@@ -1988,8 +1988,8 @@ export declare function createAuth(env: AuthEnv, branding: AuthBranding): import
|
|
|
1988
1988
|
$Infer: {
|
|
1989
1989
|
body: {
|
|
1990
1990
|
permissions: {
|
|
1991
|
-
readonly user?: ("
|
|
1992
|
-
readonly session?: ("
|
|
1991
|
+
readonly user?: ("delete" | "list" | "create" | "set-role" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "get" | "update")[] | undefined;
|
|
1992
|
+
readonly session?: ("delete" | "list" | "revoke")[] | undefined;
|
|
1993
1993
|
};
|
|
1994
1994
|
} & {
|
|
1995
1995
|
userId?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/auth/config.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAK9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAU3D,2FAA2F;AAC3F,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4FAA4F;IAC5F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,qFAAqF;AACrF,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/auth/config.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// cairn-core: the better-auth instance. Auth is engine code (engine-fat rule), so the whole
|
|
2
|
-
// config
|
|
2
|
+
// config lives here: Drizzle/D1 adapter, magic-link (POST-confirm-shaped send), admin roles.
|
|
3
3
|
// Instantiated PER REQUEST in hooks.server.ts (the D1 binding is request-scoped); the factory
|
|
4
4
|
// is cheap (no I/O at construction).
|
|
5
5
|
import { betterAuth } from 'better-auth';
|
|
@@ -12,18 +12,18 @@ import { sendMagicLink } from '../email';
|
|
|
12
12
|
import * as schema from './schema';
|
|
13
13
|
// Two-tier roles on the admin plugin's access-control system: `owner` holds every admin
|
|
14
14
|
// statement (manage editors, revoke sessions); `editor` holds none (content-only). `adminRoles`
|
|
15
|
-
// must name a role defined here, so owner
|
|
15
|
+
// must name a role defined here, so owner (not the plugin's built-in `admin`) is the gate.
|
|
16
16
|
const ac = createAccessControl(defaultStatements);
|
|
17
17
|
const owner = ac.newRole(defaultStatements);
|
|
18
18
|
const editor = ac.newRole({});
|
|
19
19
|
/**
|
|
20
20
|
* The shared better-auth config. Kept separate from `createAuth` so the test harness can run
|
|
21
21
|
* the EXACT plugin set (allowlist semantics, expiry, POST-confirm send) over an in-memory
|
|
22
|
-
* SQLite instead of D1. `disableSignUp:true` makes the `user` table the editor allowlist
|
|
22
|
+
* SQLite instead of D1. `disableSignUp:true` makes the `user` table the editor allowlist:
|
|
23
23
|
* magic-link never auto-creates, so the only way in is the owner-gated admin `createUser`
|
|
24
24
|
* (see auth/admins.ts). `adminRoles:['owner']` lets owners (not the default `admin` role)
|
|
25
25
|
* drive the admin API. Tokens are stored hashed and consumed atomically on first verify
|
|
26
|
-
* (better-auth GHSA-hc7v-rggr-4hvx)
|
|
26
|
+
* (better-auth GHSA-hc7v-rggr-4hvx), single-use by construction (C1).
|
|
27
27
|
*/
|
|
28
28
|
export function buildAuth(opts) {
|
|
29
29
|
return betterAuth({
|
|
@@ -40,7 +40,7 @@ export function buildAuth(opts) {
|
|
|
40
40
|
sendMagicLink: async ({ email, token }, ctx) => {
|
|
41
41
|
// Allowlist gate: better-auth always fires this callback (even for unknown emails, to
|
|
42
42
|
// avoid enumeration) and only blocks user creation at verify. So gate the actual send
|
|
43
|
-
// here
|
|
43
|
+
// here. Never email a non-editor. The login UI shows neutral copy either way, so this
|
|
44
44
|
// leaks nothing; it just stops strangers receiving a dead link.
|
|
45
45
|
const existing = await ctx?.context.internalAdapter.findUserByEmail(email);
|
|
46
46
|
if (!existing?.user)
|
package/dist/auth/guard.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Auth } from './config';
|
|
2
|
-
/** The session shape the whole admin reads
|
|
2
|
+
/** The session shape the whole admin reads: layout, guards, content fns, manage-editors. */
|
|
3
3
|
export interface CairnUser {
|
|
4
4
|
id: string;
|
|
5
5
|
email: string;
|
package/dist/auth/guard.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/lib/auth/guard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAErC,
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/lib/auth/guard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAErC,4FAA4F;AAC5F,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC1B;AAED,gEAAgE;AAChE,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAKzF;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,CAGhE;AAED,KAAK,YAAY,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,CAAC;AAE3E;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAa1E;AAED,4FAA4F;AAC5F,wBAAsB,OAAO,CAAC,KAAK,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,CAAA;CAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAQpG"}
|
package/dist/auth/guard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cairn-core: server-side auth helpers the site route shims delegate to. Each takes the
|
|
2
|
-
// SvelteKit event
|
|
3
|
-
// `App.*` ambient types
|
|
2
|
+
// SvelteKit event, typed structurally so the package never depends on a site's generated
|
|
3
|
+
// `App.*` ambient types, plus the per-request `Auth` from `locals`.
|
|
4
4
|
import { redirect } from '@sveltejs/kit';
|
|
5
5
|
/** Read the better-auth session into a cairn user (or null). */
|
|
6
6
|
export async function loadSession(auth, request) {
|
package/dist/carta.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ interface PreviewTransformer {
|
|
|
18
18
|
* so this ordering reproduces render.ts exactly. Pure (no Carta) so it is unit-testable.
|
|
19
19
|
*/
|
|
20
20
|
export declare function previewTransformers({ remarkPlugins, rehypePlugins }: PreviewPlugins): PreviewTransformer[];
|
|
21
|
-
/** Minimal Options subset we populate
|
|
21
|
+
/** Minimal Options subset we populate (avoids importing carta-md, which re-exports Svelte components). */
|
|
22
22
|
interface PreviewCartaOptions {
|
|
23
23
|
sanitizer: false;
|
|
24
24
|
rehypeOptions: {
|
package/dist/carta.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"carta.d.ts","sourceRoot":"","sources":["../src/lib/carta.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,aAAa,EAAE,SAAS,EAAE,CAAC;IAC3B,oDAAoD;IACpD,aAAa,EAAE,SAAS,EAAE,CAAC;CAC5B;AAED,UAAU,kBAAkB;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,SAAS,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,SAAS,CAAA;KAAE,KAAK,IAAI,CAAC;CACpD;AAYD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,cAAc,GAAG,kBAAkB,EAAE,CAE1G;AAED,
|
|
1
|
+
{"version":3,"file":"carta.d.ts","sourceRoot":"","sources":["../src/lib/carta.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,aAAa,EAAE,SAAS,EAAE,CAAC;IAC3B,oDAAoD;IACpD,aAAa,EAAE,SAAS,EAAE,CAAC;CAC5B;AAED,UAAU,kBAAkB;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,SAAS,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,SAAS,CAAA;KAAE,KAAK,IAAI,CAAC;CACpD;AAYD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,cAAc,GAAG,kBAAkB,EAAE,CAE1G;AAED,0GAA0G;AAC1G,UAAU,mBAAmB;IAC3B,SAAS,EAAE,KAAK,CAAC;IACjB,aAAa,EAAE;QAAE,kBAAkB,EAAE,OAAO,CAAA;KAAE,CAAC;IAC/C,UAAU,EAAE,KAAK,CAAC;QAAE,YAAY,EAAE,kBAAkB,EAAE,CAAA;KAAE,CAAC,CAAC;CAC3D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,cAAc,GAAG,mBAAmB,CAMhF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// Neutral admin chrome, shared across sites so the tool looks identical everywhere (only the
|
|
3
3
|
// adapter's siteName varies). When signed in it's a responsive DaisyUI drawer+navbar shell
|
|
4
|
-
// (`drawer lg:drawer-open
|
|
4
|
+
// (`drawer lg:drawer-open`, sidebar pinned on desktop, slide-over + hamburger on mobile),
|
|
5
5
|
// patterned on scosman/CMSaasStarter's `(admin)/(menu)` layout. The nav is data-driven and
|
|
6
6
|
// role-gated, so a new surface is one entry in `nav` (plus its route + component). Signed out
|
|
7
7
|
// (the login page lives under this layout) it falls back to a minimal centered shell.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
label: string;
|
|
23
23
|
icon: Snippet;
|
|
24
24
|
active: boolean;
|
|
25
|
-
/** Owner-only surface
|
|
25
|
+
/** Owner-only surface; hidden from regular editors. */
|
|
26
26
|
owner?: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
<input id="admin-drawer" type="checkbox" class="drawer-toggle" />
|
|
74
74
|
|
|
75
75
|
<div class="drawer-content">
|
|
76
|
-
<!-- Mobile top bar
|
|
76
|
+
<!-- Mobile top bar; the desktop sidebar replaces this at lg. -->
|
|
77
77
|
<div class="navbar bg-base-100 lg:hidden">
|
|
78
78
|
<div class="flex-1">
|
|
79
79
|
<span class="px-2 text-xl font-bold">{data.siteName} CMS</span>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// The /admin content list: every collection's files, linking into the editor. Data comes
|
|
3
3
|
// from `adminListLoad` (collections) merged with `adminLayoutLoad` (siteName). The shell
|
|
4
|
-
// (AdminLayout) owns the chrome
|
|
4
|
+
// (AdminLayout) owns the chrome (site title, signed-in identity, nav, sign out), so this
|
|
5
5
|
// page renders only the content body.
|
|
6
6
|
import type { AdminCollectionList } from '../sveltekit';
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// The scanner-safe confirm surface (C2). A GET renders this static page
|
|
3
|
-
// The token rides in a hidden field; only the explicit form POST (the route's default action
|
|
2
|
+
// The scanner-safe confirm surface (C2). A GET renders this static page and consumes nothing.
|
|
3
|
+
// The token rides in a hidden field; only the explicit form POST (the route's default action,
|
|
4
4
|
// confirmSignIn) verifies it. Mail scanners GET URLs but don't submit forms, so prefetch can't
|
|
5
5
|
// burn the link. JS-free by design.
|
|
6
6
|
interface Props {
|
|
@@ -13,21 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
// Body is editable state; the Carta editor's preview runs the exact site plugin set, so it
|
|
15
15
|
// matches the live page. A hidden input carries the current value into the form.
|
|
16
|
-
// svelte-ignore state_referenced_locally
|
|
16
|
+
// svelte-ignore state_referenced_locally (seeding from the initial load is intended)
|
|
17
17
|
let body = $state(data.body);
|
|
18
18
|
|
|
19
|
-
// svelte-ignore state_referenced_locally
|
|
19
|
+
// svelte-ignore state_referenced_locally (the preview plugin set is fixed for the load)
|
|
20
20
|
const carta = new Carta(previewCartaOptions(preview));
|
|
21
21
|
|
|
22
22
|
// Carta's MarkdownEditor must not render on the worker (it pulls Shiki). onMount fires only
|
|
23
|
-
// in the browser, so SSR renders the plain textarea and the client swaps in the editor
|
|
24
|
-
// the kit-free equivalent of the per-site route's `$app/environment` `browser` guard.
|
|
23
|
+
// in the browser, so SSR renders the plain textarea and the client swaps in the editor.
|
|
24
|
+
// This is the kit-free equivalent of the per-site route's `$app/environment` `browser` guard.
|
|
25
25
|
let mounted = $state(false);
|
|
26
26
|
onMount(() => {
|
|
27
27
|
mounted = true;
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
// svelte-ignore state_referenced_locally
|
|
30
|
+
// svelte-ignore state_referenced_locally (form defaults from the initial load)
|
|
31
31
|
const fm = data.frontmatter as Record<string, unknown>;
|
|
32
32
|
|
|
33
33
|
function fmString(key: string): string {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// The magic-link sign-in page. Requests a link via the better-auth client (client-side, same
|
|
3
|
-
// origin). To avoid enumeration the UI shows the
|
|
4
|
-
// on the allowlist
|
|
3
|
+
// origin). To avoid enumeration the UI shows the same neutral copy whether or not the email is
|
|
4
|
+
// on the allowlist. The server only emails actual editors (see auth/config.ts send gate).
|
|
5
5
|
import { createAuthClient } from 'better-auth/svelte';
|
|
6
6
|
import { magicLinkClient } from 'better-auth/client/plugins';
|
|
7
7
|
|
|
8
8
|
// The browser client lives in the one component that needs it (requesting a link). Sign-out
|
|
9
|
-
// and editor management go through server endpoints, so no shared client module is needed
|
|
10
|
-
//
|
|
9
|
+
// and editor management go through server endpoints, so no shared client module is needed.
|
|
10
|
+
// A component-local const keeps better-auth's deep client types out of the packaged .d.ts.
|
|
11
11
|
const authClient = createAuthClient({ plugins: [magicLinkClient()] });
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
event.preventDefault();
|
|
24
24
|
busy = true;
|
|
25
25
|
// The magic-link email points at our /admin/auth/confirm page (built in config.ts), not a
|
|
26
|
-
// GET-verify URL
|
|
26
|
+
// GET-verify URL, so the result is the same regardless of allowlist membership.
|
|
27
27
|
await authClient.signIn.magicLink({ email });
|
|
28
28
|
busy = false;
|
|
29
29
|
requested = true;
|
package/dist/email.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// cairn-core: pluggable magic-link email sender.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// recipients)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// The default adapter targets Cloudflare Email Service (Email Sending, transactional,
|
|
4
|
+
// arbitrary recipients), distinct from Email Routing's recipient-restricted `EmailMessage`
|
|
5
|
+
// flow. Both share the same `send_email` binding (configured without a destination_address)
|
|
6
|
+
// but use a different call shape: `binding.send({ to, from, ... })`.
|
|
7
7
|
// Resend can slot in behind the same `sendMagicLink` signature if needed.
|
|
8
8
|
export async function sendMagicLink(sender, to, link, siteName, from) {
|
|
9
9
|
const expiry = "This link expires in 10 minutes and works only once. If you didn't request it, ignore this email.";
|
package/dist/github.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare function appJwt(appId: string, privateKeyPem: string): Promise<st
|
|
|
27
27
|
export interface AppCredentials {
|
|
28
28
|
appId: string;
|
|
29
29
|
installationId: string;
|
|
30
|
-
/** The stored GITHUB_APP_PRIVATE_KEY_B64
|
|
30
|
+
/** The stored GITHUB_APP_PRIVATE_KEY_B64: base64 of the PEM, single line. */
|
|
31
31
|
privateKeyB64: string;
|
|
32
32
|
}
|
|
33
33
|
/** Exchange the App JWT for a short-lived installation access token. */
|
|
@@ -38,15 +38,35 @@ export interface CommitAuthor {
|
|
|
38
38
|
name: string;
|
|
39
39
|
email: string;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A concurrent edit lost the SHA race (C3): the file changed between the read and the PUT,
|
|
43
|
+
* from another editor or the site's own CI. Thrown so callers can fail safe (re-fetch and ask
|
|
44
|
+
* the editor to reapply) instead of surfacing a raw 409. Defined and caught inside the package
|
|
45
|
+
* so `instanceof` is reliable (no peer-boundary identity split, unlike kit's `redirect`/`error`).
|
|
46
|
+
*/
|
|
47
|
+
export declare class CommitConflictError extends Error {
|
|
48
|
+
readonly path: string;
|
|
49
|
+
constructor(path: string);
|
|
50
|
+
}
|
|
41
51
|
/**
|
|
42
52
|
* Commit `content` to `path` on the configured branch via the contents API. Author is the
|
|
43
53
|
* editor; committer is omitted so GitHub attributes it to the App (cairn-cms[bot]). Updates
|
|
44
54
|
* the file in place when it exists (passing its sha), creates it otherwise. Returns the
|
|
45
|
-
* commit sha.
|
|
55
|
+
* commit sha. A stale-sha 409 (someone committed in between) becomes a `CommitConflictError`.
|
|
46
56
|
*/
|
|
47
57
|
export declare function commitFile(repo: RepoRef, path: string, content: string, opts: {
|
|
48
58
|
message: string;
|
|
49
59
|
author: CommitAuthor;
|
|
50
60
|
}, token: string): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Deploy-time self-test for the GitHub App signer (M2): sign a dummy JWT with the configured
|
|
63
|
+
* private key. Exercises the brittle PKCS#1→PKCS#8 conversion + Web Crypto import/sign without
|
|
64
|
+
* any network call or secret in the result, so `/admin/healthz` catches a bad/rotated key
|
|
65
|
+
* before an editor's save fails. Returns `{ ok: false, detail }` rather than throwing.
|
|
66
|
+
*/
|
|
67
|
+
export declare function signingSelfTest(appId: string, privateKeyB64: string): Promise<{
|
|
68
|
+
ok: boolean;
|
|
69
|
+
detail?: string;
|
|
70
|
+
}>;
|
|
51
71
|
export {};
|
|
52
72
|
//# sourceMappingURL=github.d.ts.map
|
package/dist/github.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../src/lib/github.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,4FAA4F;AAC5F,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAcD,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,QAAQ,EAAE,CAKlE;AAED,yDAAyD;AACzD,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAIlG;AAED,iEAAiE;AACjE,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKjG;AAqCD,wFAAwF;AACxF,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclF;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,
|
|
1
|
+
{"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../src/lib/github.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,4FAA4F;AAC5F,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAcD,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,QAAQ,EAAE,CAKlE;AAED,yDAAyD;AACzD,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAIlG;AAED,iEAAiE;AACjE,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKjG;AAqCD,wFAAwF;AACxF,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclF;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wEAAwE;AACxE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ9E;AAOD,+EAA+E;AAC/E,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKhG;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAIzC;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,EAC/C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH"}
|
package/dist/github.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Reads (Pass B) list a collection directory and fetch a file's raw markdown; the token
|
|
4
4
|
// is optional because ecnordic's repo is public. Writes (Pass C) mint a short-lived
|
|
5
|
-
// GitHub App installation token
|
|
6
|
-
// dependency
|
|
5
|
+
// GitHub App installation token (App JWT, RS256 signed with Web Crypto, no octokit
|
|
6
|
+
// dependency) and commit through the contents API with author = editor, committer = the
|
|
7
7
|
// App (cairn-cms[bot]). The same token also lifts reads to the authenticated rate limit
|
|
8
8
|
// and unlocks private repos (e.g. 907-life).
|
|
9
9
|
import { bytesToB64url } from './utils';
|
|
@@ -64,7 +64,7 @@ function derLength(n) {
|
|
|
64
64
|
}
|
|
65
65
|
// AlgorithmIdentifier for rsaEncryption (OID 1.2.840.113549.1.1.1) with NULL parameters.
|
|
66
66
|
const RSA_ALG_ID = [0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00];
|
|
67
|
-
/** Wrap a PKCS#1 RSAPrivateKey (DER) as PKCS#8
|
|
67
|
+
/** Wrap a PKCS#1 RSAPrivateKey (DER) as PKCS#8 (the only RSA form Web Crypto importKey takes). */
|
|
68
68
|
function pkcs1ToPkcs8(pkcs1) {
|
|
69
69
|
const octet = [0x04, ...derLength(pkcs1.length), ...pkcs1];
|
|
70
70
|
const body = [0x02, 0x01, 0x00, ...RSA_ALG_ID, ...octet];
|
|
@@ -97,7 +97,7 @@ export async function installationToken(creds) {
|
|
|
97
97
|
throw new Error(`GitHub installation token failed: ${res.status}`);
|
|
98
98
|
return (await res.json()).token;
|
|
99
99
|
}
|
|
100
|
-
/** Standard (padded) base64 of UTF-8 text
|
|
100
|
+
/** Standard (padded) base64 of UTF-8 text, as the contents API expects. */
|
|
101
101
|
function toBase64(text) {
|
|
102
102
|
return btoa(Array.from(encoder.encode(text), (b) => String.fromCharCode(b)).join(''));
|
|
103
103
|
}
|
|
@@ -110,11 +110,25 @@ export async function fileSha(repo, path, token) {
|
|
|
110
110
|
throw new Error(`GitHub stat ${path} failed: ${res.status}`);
|
|
111
111
|
return (await res.json()).sha;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* A concurrent edit lost the SHA race (C3): the file changed between the read and the PUT,
|
|
115
|
+
* from another editor or the site's own CI. Thrown so callers can fail safe (re-fetch and ask
|
|
116
|
+
* the editor to reapply) instead of surfacing a raw 409. Defined and caught inside the package
|
|
117
|
+
* so `instanceof` is reliable (no peer-boundary identity split, unlike kit's `redirect`/`error`).
|
|
118
|
+
*/
|
|
119
|
+
export class CommitConflictError extends Error {
|
|
120
|
+
path;
|
|
121
|
+
constructor(path) {
|
|
122
|
+
super(`Commit conflict on ${path}: it changed since it was opened`);
|
|
123
|
+
this.path = path;
|
|
124
|
+
this.name = 'CommitConflictError';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
113
127
|
/**
|
|
114
128
|
* Commit `content` to `path` on the configured branch via the contents API. Author is the
|
|
115
129
|
* editor; committer is omitted so GitHub attributes it to the App (cairn-cms[bot]). Updates
|
|
116
130
|
* the file in place when it exists (passing its sha), creates it otherwise. Returns the
|
|
117
|
-
* commit sha.
|
|
131
|
+
* commit sha. A stale-sha 409 (someone committed in between) becomes a `CommitConflictError`.
|
|
118
132
|
*/
|
|
119
133
|
export async function commitFile(repo, path, content, opts, token) {
|
|
120
134
|
const sha = await fileSha(repo, path, token);
|
|
@@ -130,7 +144,28 @@ export async function commitFile(repo, path, content, opts, token) {
|
|
|
130
144
|
...(sha ? { sha } : {}),
|
|
131
145
|
}),
|
|
132
146
|
});
|
|
147
|
+
// 409 = the blob sha we read is no longer current. Fail safe: the caller re-fetches and the
|
|
148
|
+
// editor reapplies. (Full three-way merge stays out of scope; see ARCHITECTURE §5.)
|
|
149
|
+
if (res.status === 409)
|
|
150
|
+
throw new CommitConflictError(path);
|
|
133
151
|
if (!res.ok)
|
|
134
152
|
throw new Error(`GitHub commit ${path} failed: ${res.status} ${await res.text()}`);
|
|
135
153
|
return (await res.json()).commit.sha;
|
|
136
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Deploy-time self-test for the GitHub App signer (M2): sign a dummy JWT with the configured
|
|
157
|
+
* private key. Exercises the brittle PKCS#1→PKCS#8 conversion + Web Crypto import/sign without
|
|
158
|
+
* any network call or secret in the result, so `/admin/healthz` catches a bad/rotated key
|
|
159
|
+
* before an editor's save fails. Returns `{ ok: false, detail }` rather than throwing.
|
|
160
|
+
*/
|
|
161
|
+
export async function signingSelfTest(appId, privateKeyB64) {
|
|
162
|
+
try {
|
|
163
|
+
const jwt = await appJwt(appId, atob(privateKeyB64));
|
|
164
|
+
if (jwt.split('.').length !== 3)
|
|
165
|
+
return { ok: false, detail: 'malformed JWT' };
|
|
166
|
+
return { ok: true };
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
return { ok: false, detail: err instanceof Error ? err.message : 'sign failed' };
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAEA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAEA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Element } from 'hast';
|
|
2
|
+
/** A glyph name → SVG path-data map (the site owns the icon set). */
|
|
3
|
+
export type IconSet = Record<string, string>;
|
|
4
|
+
/** Inline SVG glyph as a real hast node: class ec-glyph, 256 viewBox, currentColor fill. */
|
|
5
|
+
export declare function glyph(name: string, icons: IconSet): Element;
|
|
6
|
+
//# sourceMappingURL=glyph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glyph.d.ts","sourceRoot":"","sources":["../../src/lib/render/glyph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,qEAAqE;AACrE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE7C,4FAA4F;AAC5F,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAM3D"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { s } from 'hastscript';
|
|
2
|
+
/** Inline SVG glyph as a real hast node: class ec-glyph, 256 viewBox, currentColor fill. */
|
|
3
|
+
export function glyph(name, icons) {
|
|
4
|
+
return s('svg', { className: ['ec-glyph'], viewBox: '0 0 256 256', fill: 'currentColor', ariaHidden: 'true' }, [s('path', { d: icons[name] })]);
|
|
5
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/render/index.ts"],"names":[],"mappings":"AAGA,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// cairn-cms render engine: a directive-driven markdown → HTML pipeline whose
|
|
2
|
+
// component vocabulary is supplied by a site's component registry. The site owns the
|
|
3
|
+
// component builders, class names, icon set, and CSS; the engine owns the machinery.
|
|
4
|
+
export * from './registry';
|
|
5
|
+
export * from './glyph';
|
|
6
|
+
export * from './remark-directives';
|
|
7
|
+
export * from './rehype-dispatch';
|
|
8
|
+
export * from './pipeline';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type PluggableList } from 'unified';
|
|
2
|
+
import type { ComponentRegistry } from './registry';
|
|
3
|
+
export interface RendererOptions {
|
|
4
|
+
/** A site's per-index motion formula for the top-level rise stagger
|
|
5
|
+
* (e.g. ecnordic's `(i) => '--rise:' + …`). Omit for no stagger. */
|
|
6
|
+
rise?: (idx: number) => string;
|
|
7
|
+
}
|
|
8
|
+
/** Compose a site's render pipeline from its component registry: directive syntax →
|
|
9
|
+
* stamped markers → registry-built hast. Returns `renderMarkdown` plus the remark/
|
|
10
|
+
* rehype plugin arrays (so the Carta editor preview can reuse the exact same set). */
|
|
11
|
+
export declare function createRenderer(registry: ComponentRegistry, options?: RendererOptions): {
|
|
12
|
+
remarkPlugins: PluggableList;
|
|
13
|
+
rehypePlugins: PluggableList;
|
|
14
|
+
renderMarkdown: (content: string) => Promise<string>;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/render/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAUtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC/B;yEACqE;IACrE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/B;AAED;;uFAEuF;AACvF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,GAAE,eAAoB;;;8BAavD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;EAEzD"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import remarkDirective from 'remark-directive';
|
|
5
|
+
import remarkRehype from 'remark-rehype';
|
|
6
|
+
import rehypeRaw from 'rehype-raw';
|
|
7
|
+
import rehypeSlug from 'rehype-slug';
|
|
8
|
+
import rehypeStringify from 'rehype-stringify';
|
|
9
|
+
import { remarkDirectiveStamp } from './remark-directives';
|
|
10
|
+
import { rehypeDispatch } from './rehype-dispatch';
|
|
11
|
+
/** Compose a site's render pipeline from its component registry: directive syntax →
|
|
12
|
+
* stamped markers → registry-built hast. Returns `renderMarkdown` plus the remark/
|
|
13
|
+
* rehype plugin arrays (so the Carta editor preview can reuse the exact same set). */
|
|
14
|
+
export function createRenderer(registry, options = {}) {
|
|
15
|
+
const remarkPlugins = [remarkDirective, [remarkDirectiveStamp, registry]];
|
|
16
|
+
const rehypePlugins = [rehypeRaw, [rehypeDispatch, registry, options.rise], rehypeSlug];
|
|
17
|
+
const processor = unified()
|
|
18
|
+
.use(remarkParse)
|
|
19
|
+
.use(remarkGfm)
|
|
20
|
+
.use(remarkPlugins)
|
|
21
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
|
22
|
+
.use(rehypePlugins)
|
|
23
|
+
.use(rehypeStringify);
|
|
24
|
+
return {
|
|
25
|
+
remarkPlugins,
|
|
26
|
+
rehypePlugins,
|
|
27
|
+
renderMarkdown: async (content) => String(await processor.process(content)),
|
|
28
|
+
};
|
|
29
|
+
}
|