@glw907/cairn-cms 0.41.0 → 0.50.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/CHANGELOG.md +42 -0
- package/README.md +2 -2
- package/dist/ambient.d.ts +9 -0
- package/dist/ambient.js +1 -0
- package/dist/components/AdminLayout.svelte +6 -8
- package/dist/components/CairnAdmin.svelte +67 -0
- package/dist/components/CairnAdmin.svelte.d.ts +35 -0
- package/dist/components/ConceptList.svelte +4 -5
- package/dist/components/ConceptList.svelte.d.ts +4 -8
- package/dist/components/ConfirmPage.svelte +1 -1
- package/dist/components/EditPage.svelte +13 -9
- package/dist/components/EditPage.svelte.d.ts +4 -9
- package/dist/components/LoginPage.svelte +2 -2
- package/dist/components/LoginPage.svelte.d.ts +1 -1
- package/dist/components/ManageEditors.svelte +4 -3
- package/dist/components/ManageEditors.svelte.d.ts +2 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-format.d.ts +0 -8
- package/dist/components/markdown-format.js +0 -28
- package/dist/content/links.d.ts +8 -0
- package/dist/content/links.js +28 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/delivery/data.d.ts +3 -5
- package/dist/delivery/data.js +2 -3
- package/dist/delivery/feeds.js +1 -7
- package/dist/delivery/index.d.ts +2 -2
- package/dist/delivery/index.js +1 -1
- package/dist/delivery/manifest.d.ts +0 -5
- package/dist/delivery/manifest.js +5 -16
- package/dist/{sveltekit → delivery}/public-routes.d.ts +4 -4
- package/dist/{sveltekit → delivery}/public-routes.js +7 -7
- package/dist/delivery/site-indexes.d.ts +3 -3
- package/dist/delivery/site-indexes.js +3 -3
- package/dist/delivery/{site-index.d.ts → site-resolver.d.ts} +7 -3
- package/dist/delivery/{site-index.js → site-resolver.js} +13 -3
- package/dist/delivery/sitemap.js +1 -3
- package/dist/delivery/xml.d.ts +2 -0
- package/dist/delivery/xml.js +11 -0
- package/dist/email.js +4 -11
- package/dist/env.d.ts +1 -1
- package/dist/env.js +3 -2
- package/dist/escape.d.ts +2 -0
- package/dist/escape.js +11 -0
- package/dist/github/credentials.d.ts +2 -1
- package/dist/github/credentials.js +10 -2
- package/dist/github/types.d.ts +2 -0
- package/dist/github/types.js +4 -0
- package/dist/log/events.d.ts +1 -1
- package/dist/nav/site-config.d.ts +2 -0
- package/dist/nav/site-config.js +2 -0
- package/dist/sveltekit/admin-dispatch.d.ts +28 -0
- package/dist/sveltekit/admin-dispatch.js +62 -0
- package/dist/sveltekit/cairn-admin.d.ts +94 -0
- package/dist/sveltekit/cairn-admin.js +126 -0
- package/dist/sveltekit/condition-response.d.ts +1 -0
- package/dist/sveltekit/condition-response.js +25 -0
- package/dist/sveltekit/content-routes.d.ts +34 -14
- package/dist/sveltekit/content-routes.js +59 -33
- package/dist/sveltekit/guard.js +15 -3
- package/dist/sveltekit/https-required-page.js +2 -1
- package/dist/sveltekit/index.d.ts +3 -1
- package/dist/sveltekit/index.js +2 -0
- package/dist/sveltekit/nav-routes.d.ts +3 -1
- package/dist/sveltekit/nav-routes.js +19 -10
- package/dist/sveltekit/static-admin-page.d.ts +0 -2
- package/dist/sveltekit/static-admin-page.js +1 -8
- package/dist/sveltekit/types.d.ts +18 -11
- package/package.json +5 -1
- package/src/lib/ambient.ts +19 -0
- package/src/lib/components/AdminLayout.svelte +6 -8
- package/src/lib/components/CairnAdmin.svelte +67 -0
- package/src/lib/components/ConceptList.svelte +4 -5
- package/src/lib/components/ConfirmPage.svelte +1 -1
- package/src/lib/components/EditPage.svelte +13 -9
- package/src/lib/components/LoginPage.svelte +2 -2
- package/src/lib/components/ManageEditors.svelte +4 -3
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-format.ts +0 -27
- package/src/lib/content/links.ts +28 -0
- package/src/lib/content/types.ts +2 -2
- package/src/lib/delivery/data.ts +3 -5
- package/src/lib/delivery/feeds.ts +1 -8
- package/src/lib/delivery/index.ts +2 -2
- package/src/lib/delivery/manifest.ts +5 -18
- package/src/lib/{sveltekit → delivery}/public-routes.ts +11 -11
- package/src/lib/delivery/site-indexes.ts +6 -6
- package/src/lib/delivery/{site-index.ts → site-resolver.ts} +20 -8
- package/src/lib/delivery/sitemap.ts +1 -4
- package/src/lib/delivery/xml.ts +12 -0
- package/src/lib/email.ts +4 -11
- package/src/lib/env.ts +3 -2
- package/src/lib/escape.ts +12 -0
- package/src/lib/github/credentials.ts +6 -2
- package/src/lib/github/types.ts +5 -0
- package/src/lib/log/events.ts +1 -0
- package/src/lib/nav/site-config.ts +3 -0
- package/src/lib/sveltekit/admin-dispatch.ts +75 -0
- package/src/lib/sveltekit/cairn-admin.ts +177 -0
- package/src/lib/sveltekit/condition-response.ts +27 -1
- package/src/lib/sveltekit/content-routes.ts +102 -45
- package/src/lib/sveltekit/guard.ts +16 -3
- package/src/lib/sveltekit/https-required-page.ts +2 -1
- package/src/lib/sveltekit/index.ts +6 -0
- package/src/lib/sveltekit/nav-routes.ts +21 -11
- package/src/lib/sveltekit/static-admin-page.ts +1 -9
- package/src/lib/sveltekit/types.ts +16 -7
- package/dist/delivery/paginate.d.ts +0 -12
- package/dist/delivery/paginate.js +0 -20
- package/dist/render/index.d.ts +0 -5
- package/dist/render/index.js +0 -8
- package/src/lib/delivery/paginate.ts +0 -32
- package/src/lib/render/index.ts +0 -8
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import type { Manifest } from '../content/manifest.js';
|
|
2
|
-
import type { LinkResolve } from '../content/links.js';
|
|
3
|
-
import type { SiteIndex } from './site-index.js';
|
|
4
2
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
5
3
|
import type { CairnAdapter } from '../content/types.js';
|
|
6
4
|
import type { SiteGlobs } from './site-indexes.js';
|
|
7
5
|
/** Build the whole-corpus manifest from a site's adapter, config, and per-concept globs. Drafts are
|
|
8
6
|
* included and flagged, so the admin picker and the guards see the full graph. */
|
|
9
7
|
export declare function buildSiteManifest<A extends CairnAdapter>(adapter: A, config: SiteConfig, globs: SiteGlobs<A>): Manifest;
|
|
10
|
-
/** A resolver backed by the site index, for the build. A miss throws, so a dangling cairn: token
|
|
11
|
-
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
|
|
12
|
-
export declare function buildLinkResolver(site: SiteIndex): LinkResolve;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// cairn-cms: the build-side manifest builder
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// the build (the backstop). The admin preview uses manifestLinkResolver instead.
|
|
1
|
+
// cairn-cms: the build-side manifest builder (content-graph design). buildSiteManifest mirrors
|
|
2
|
+
// createSiteIndexes: it maps the site descriptors over the per-concept globs and projects each
|
|
3
|
+
// file to a manifest row. The build-time cairn: link resolver lives beside the site resolver in
|
|
4
|
+
// site-resolver.ts; the admin preview uses manifestLinkResolver instead.
|
|
6
5
|
import { siteDescriptors } from './site-descriptors.js';
|
|
7
6
|
import { fromGlob } from './content-index.js';
|
|
8
7
|
import { parseMarkdown } from '../content/frontmatter.js';
|
|
@@ -15,7 +14,7 @@ export function buildSiteManifest(adapter, config, globs) {
|
|
|
15
14
|
for (const descriptor of siteDescriptors(adapter, config)) {
|
|
16
15
|
const record = globRecord[descriptor.id] ?? {};
|
|
17
16
|
for (const file of fromGlob(record)) {
|
|
18
|
-
// Validate the same way createContentIndex does, so the manifest and the site
|
|
17
|
+
// Validate the same way createContentIndex does, so the manifest and the site resolver agree on
|
|
19
18
|
// which entries exist. A validation failure is excluded from both; otherwise the preview would
|
|
20
19
|
// resolve a link the build then rejects as a missing target.
|
|
21
20
|
const { frontmatter, body } = parseMarkdown(file.raw);
|
|
@@ -26,13 +25,3 @@ export function buildSiteManifest(adapter, config, globs) {
|
|
|
26
25
|
}
|
|
27
26
|
return manifest;
|
|
28
27
|
}
|
|
29
|
-
/** A resolver backed by the site index, for the build. A miss throws, so a dangling cairn: token
|
|
30
|
-
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
|
|
31
|
-
export function buildLinkResolver(site) {
|
|
32
|
-
return (ref) => {
|
|
33
|
-
const url = site.concept(ref.concept)?.byId(ref.id)?.permalink;
|
|
34
|
-
if (!url)
|
|
35
|
-
throw new Error(`cairn link target not found: cairn:${ref.concept}/${ref.id}`);
|
|
36
|
-
return url;
|
|
37
|
-
};
|
|
38
|
-
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { ContentSummary, ContentEntry } from '
|
|
2
|
-
import type {
|
|
3
|
-
import type { SeoMeta } from '
|
|
1
|
+
import type { ContentSummary, ContentEntry } from './content-index.js';
|
|
2
|
+
import type { SiteResolver } from './site-resolver.js';
|
|
3
|
+
import type { SeoMeta } from './seo.js';
|
|
4
4
|
import type { LinkResolve } from '../content/links.js';
|
|
5
5
|
/** Injected dependencies for the public loaders. */
|
|
6
6
|
export interface PublicRoutesDeps {
|
|
7
|
-
site:
|
|
7
|
+
site: SiteResolver;
|
|
8
8
|
render: (md: string, opts?: {
|
|
9
9
|
stagger?: boolean;
|
|
10
10
|
resolve?: LinkResolve;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// cairn-cms: public route loaders (dated-slug design). The factory closes over the site
|
|
2
|
-
//
|
|
1
|
+
// cairn-cms: public route loaders (dated-slug design). The factory closes over the site
|
|
2
|
+
// resolver, the runtime render, and the origin. entryLoad and entries are site-wide: one catch-all
|
|
3
3
|
// `[...path]` route resolves any concept by request path through `byPermalink`. The archive, tag,
|
|
4
|
-
// and tag-index loaders stay concept-scoped, keyed by concept id. The
|
|
5
|
-
// from globs, so it stays in the prerender graph and out of the runtime Worker.
|
|
4
|
+
// and tag-index loaders stay concept-scoped, keyed by concept id. The resolver is built in site
|
|
5
|
+
// code from globs, so it stays in the prerender graph and out of the runtime Worker.
|
|
6
6
|
import { error } from '@sveltejs/kit';
|
|
7
|
-
import { buildSeoMeta } from '
|
|
8
|
-
import { readSeoFields, resolveImageUrl } from '
|
|
9
|
-
import { buildLinkResolver } from '
|
|
7
|
+
import { buildSeoMeta } from './seo.js';
|
|
8
|
+
import { readSeoFields, resolveImageUrl } from './seo-fields.js';
|
|
9
|
+
import { buildLinkResolver } from './site-resolver.js';
|
|
10
10
|
/** Build the public loaders for a site's unified index. */
|
|
11
11
|
export function createPublicRoutes(deps) {
|
|
12
12
|
const { site, render, origin, siteName, description, feeds, defaultImage } = deps;
|
|
@@ -2,7 +2,7 @@ import type { CairnAdapter, ConceptConfig } from '../content/types.js';
|
|
|
2
2
|
import type { Infer } from '../content/schema.js';
|
|
3
3
|
import type { SiteConfig } from '../nav/site-config.js';
|
|
4
4
|
import type { ContentIndex } from './content-index.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { SiteResolver } from './site-resolver.js';
|
|
6
6
|
/** A per-concept raw glob record (`{ path: raw }`) keyed by concept id, from `import.meta.glob`. */
|
|
7
7
|
export type SiteGlobs<A extends CairnAdapter> = {
|
|
8
8
|
[K in keyof A['content']]?: Record<string, string>;
|
|
@@ -12,13 +12,13 @@ export type SiteGlobs<A extends CairnAdapter> = {
|
|
|
12
12
|
export type SiteIndexes<A extends CairnAdapter> = {
|
|
13
13
|
[K in keyof A['content']]: ContentIndex<NonNullable<A['content'][K]> extends ConceptConfig<infer S> ? Infer<S> : Record<string, unknown>>;
|
|
14
14
|
} & {
|
|
15
|
-
readonly site:
|
|
15
|
+
readonly site: SiteResolver;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
18
|
* Build typed per-concept indexes and a site resolver from one adapter. Pass the per-concept raw
|
|
19
19
|
* globs as `{ posts: import.meta.glob('...?raw', { eager: true }), ... }`; Vite needs the literal
|
|
20
20
|
* glob at the call site, so the engine cannot glob on the site's behalf. `validate: false` opts out
|
|
21
|
-
* of the build gate, exactly as on `
|
|
21
|
+
* of the build gate, exactly as on `createSiteResolver`.
|
|
22
22
|
*/
|
|
23
23
|
export declare function createSiteIndexes<const A extends CairnAdapter>(adapter: A, config: SiteConfig, globs: SiteGlobs<A>, opts?: {
|
|
24
24
|
validate?: boolean;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { siteDescriptors } from './site-descriptors.js';
|
|
2
2
|
import { createContentIndex, fromGlob } from './content-index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { createSiteResolver } from './site-resolver.js';
|
|
4
4
|
/**
|
|
5
5
|
* Build typed per-concept indexes and a site resolver from one adapter. Pass the per-concept raw
|
|
6
6
|
* globs as `{ posts: import.meta.glob('...?raw', { eager: true }), ... }`; Vite needs the literal
|
|
7
7
|
* glob at the call site, so the engine cannot glob on the site's behalf. `validate: false` opts out
|
|
8
|
-
* of the build gate, exactly as on `
|
|
8
|
+
* of the build gate, exactly as on `createSiteResolver`.
|
|
9
9
|
*/
|
|
10
10
|
export function createSiteIndexes(adapter, config, globs, opts = {}) {
|
|
11
11
|
const descriptors = siteDescriptors(adapter, config);
|
|
@@ -25,6 +25,6 @@ export function createSiteIndexes(adapter, config, globs, opts = {}) {
|
|
|
25
25
|
byConcept[descriptor.id] = index;
|
|
26
26
|
conceptIndexes.push({ descriptor, index });
|
|
27
27
|
}
|
|
28
|
-
const site =
|
|
28
|
+
const site = createSiteResolver(conceptIndexes, opts);
|
|
29
29
|
return { ...byConcept, site };
|
|
30
30
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ConceptDescriptor } from '../content/types.js';
|
|
2
2
|
import type { ContentEntry, ContentIndex, ContentSummary } from './content-index.js';
|
|
3
|
+
import type { LinkResolve } from '../content/links.js';
|
|
3
4
|
/** One concept's descriptor paired with its built index. */
|
|
4
5
|
export interface ConceptIndex {
|
|
5
6
|
descriptor: ConceptDescriptor;
|
|
6
7
|
index: ContentIndex;
|
|
7
8
|
}
|
|
8
9
|
/** The cross-concept query surface a catch-all route and the sitemap read. */
|
|
9
|
-
export interface
|
|
10
|
+
export interface SiteResolver {
|
|
10
11
|
/** Resolve a request path (with or without a trailing slash) to its entry, or undefined. */
|
|
11
12
|
byPermalink(path: string): ContentEntry | undefined;
|
|
12
13
|
/** Newer/older neighbors within the entry's own concept, for prev/next links. */
|
|
@@ -28,6 +29,9 @@ export interface SiteIndex {
|
|
|
28
29
|
* unless `validate` is `false`, on any non-draft entry whose frontmatter fails its concept's
|
|
29
30
|
* validator, so malformed content fails the build instead of shipping.
|
|
30
31
|
*/
|
|
31
|
-
export declare function
|
|
32
|
+
export declare function createSiteResolver(concepts: ConceptIndex[], opts?: {
|
|
32
33
|
validate?: boolean;
|
|
33
|
-
}):
|
|
34
|
+
}): SiteResolver;
|
|
35
|
+
/** A resolver backed by the site resolver, for the build. A miss throws, so a dangling cairn: token
|
|
36
|
+
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
|
|
37
|
+
export declare function buildLinkResolver(site: SiteResolver): LinkResolve;
|
|
@@ -21,11 +21,11 @@ function siteProblems(concepts) {
|
|
|
21
21
|
* unless `validate` is `false`, on any non-draft entry whose frontmatter fails its concept's
|
|
22
22
|
* validator, so malformed content fails the build instead of shipping.
|
|
23
23
|
*/
|
|
24
|
-
export function
|
|
24
|
+
export function createSiteResolver(concepts, opts = {}) {
|
|
25
25
|
if (opts.validate !== false) {
|
|
26
26
|
const problems = siteProblems(concepts);
|
|
27
27
|
if (problems.length > 0) {
|
|
28
|
-
throw new Error(`site
|
|
28
|
+
throw new Error(`site resolver: ${problems.length} invalid frontmatter field(s):\n ${problems.join('\n ')}`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
const byPath = new Map();
|
|
@@ -35,7 +35,7 @@ export function createSiteIndex(concepts, opts = {}) {
|
|
|
35
35
|
for (const summary of index.all()) {
|
|
36
36
|
const existing = byPath.get(summary.permalink);
|
|
37
37
|
if (existing) {
|
|
38
|
-
throw new Error(`site
|
|
38
|
+
throw new Error(`site resolver: "${existing.id}" and "${summary.id}" both resolve to "${summary.permalink}"`);
|
|
39
39
|
}
|
|
40
40
|
byPath.set(summary.permalink, { index, id: summary.id });
|
|
41
41
|
}
|
|
@@ -60,3 +60,13 @@ export function createSiteIndex(concepts, opts = {}) {
|
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/** A resolver backed by the site resolver, for the build. A miss throws, so a dangling cairn: token
|
|
64
|
+
* fails the prerender (the build backstop). The preview uses manifestLinkResolver, which marks. */
|
|
65
|
+
export function buildLinkResolver(site) {
|
|
66
|
+
return (ref) => {
|
|
67
|
+
const url = site.concept(ref.concept)?.byId(ref.id)?.permalink;
|
|
68
|
+
if (!url)
|
|
69
|
+
throw new Error(`cairn link target not found: cairn:${ref.concept}/${ref.id}`);
|
|
70
|
+
return url;
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/delivery/sitemap.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// cairn-cms: sitemap builder (public-delivery design). Pure over a URL list; the caller
|
|
2
2
|
// derives the list from the content index and the routable concepts.
|
|
3
|
-
|
|
4
|
-
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
5
|
-
}
|
|
3
|
+
import { escapeXml } from './xml.js';
|
|
6
4
|
/** Build a sitemap XML document from a list of URLs. */
|
|
7
5
|
export function buildSitemap(urls) {
|
|
8
6
|
const entries = urls
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// cairn-cms: the one XML text escape the feed and sitemap builders share. The strongest of the
|
|
2
|
+
// two copies it replaced (the old sitemap copy skipped quotes), so both documents stay safe in
|
|
3
|
+
// element text and double-quoted attributes.
|
|
4
|
+
/** Escape the XML-significant characters for element text and double-quoted attribute values. */
|
|
5
|
+
export function escapeXml(value) {
|
|
6
|
+
return value
|
|
7
|
+
.replaceAll('&', '&')
|
|
8
|
+
.replaceAll('<', '<')
|
|
9
|
+
.replaceAll('>', '>')
|
|
10
|
+
.replaceAll('"', '"');
|
|
11
|
+
}
|
package/dist/email.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { CairnError } from './diagnostics/index.js';
|
|
2
|
-
|
|
3
|
-
function escapeHtml(value) {
|
|
4
|
-
return value
|
|
5
|
-
.replaceAll('&', '&')
|
|
6
|
-
.replaceAll('<', '<')
|
|
7
|
-
.replaceAll('>', '>')
|
|
8
|
-
.replaceAll('"', '"')
|
|
9
|
-
.replaceAll("'", ''');
|
|
10
|
-
}
|
|
2
|
+
import { escapeHtml } from './escape.js';
|
|
11
3
|
/** Build the confirmation email. The link is the only action; the copy stays plain. */
|
|
12
4
|
export function buildMagicLinkMessage(input) {
|
|
13
5
|
const { to, branding, link } = input;
|
|
@@ -20,8 +12,9 @@ export function buildMagicLinkMessage(input) {
|
|
|
20
12
|
}
|
|
21
13
|
/** The production send: Cloudflare Email Sending through the EMAIL binding. */
|
|
22
14
|
export const cloudflareSend = async (env, message) => {
|
|
23
|
-
if (!env.EMAIL)
|
|
24
|
-
throw new
|
|
15
|
+
if (!env.EMAIL) {
|
|
16
|
+
throw new CairnError('config.bindings-missing', { message: 'EMAIL binding is not configured' });
|
|
17
|
+
}
|
|
25
18
|
await env.EMAIL.send(message);
|
|
26
19
|
};
|
|
27
20
|
/**
|
package/dist/env.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare function requireOrigin(env: {
|
|
|
16
16
|
* The handlers read D1 off `event.platform.env`; without this a misconfigured binding
|
|
17
17
|
* surfaces as a raw `TypeError` deep in a store call. This gives the failure a name.
|
|
18
18
|
*
|
|
19
|
-
* @throws
|
|
19
|
+
* @throws CairnError (`config.bindings-missing`) when `AUTH_DB` is missing.
|
|
20
20
|
*/
|
|
21
21
|
export declare function requireDb(env: {
|
|
22
22
|
AUTH_DB?: D1Database;
|
package/dist/env.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CairnError } from './diagnostics/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Returns the site's public origin from configuration.
|
|
3
4
|
*
|
|
@@ -33,11 +34,11 @@ export function requireOrigin(env) {
|
|
|
33
34
|
* The handlers read D1 off `event.platform.env`; without this a misconfigured binding
|
|
34
35
|
* surfaces as a raw `TypeError` deep in a store call. This gives the failure a name.
|
|
35
36
|
*
|
|
36
|
-
* @throws
|
|
37
|
+
* @throws CairnError (`config.bindings-missing`) when `AUTH_DB` is missing.
|
|
37
38
|
*/
|
|
38
39
|
export function requireDb(env) {
|
|
39
40
|
if (!env.AUTH_DB) {
|
|
40
|
-
throw new
|
|
41
|
+
throw new CairnError('config.bindings-missing', { message: 'AUTH_DB binding is not configured' });
|
|
41
42
|
}
|
|
42
43
|
return env.AUTH_DB;
|
|
43
44
|
}
|
package/dist/escape.d.ts
ADDED
package/dist/escape.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// cairn-cms: the one HTML text escape. A leaf module with no imports, so the email builder and
|
|
2
|
+
// the edge-served admin pages share it without either arm reaching into the other.
|
|
3
|
+
/** Escape the five HTML-significant characters for text and quoted attribute values. */
|
|
4
|
+
export function escapeHtml(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replaceAll('&', '&')
|
|
7
|
+
.replaceAll('<', '<')
|
|
8
|
+
.replaceAll('>', '>')
|
|
9
|
+
.replaceAll('"', '"')
|
|
10
|
+
.replaceAll("'", ''');
|
|
11
|
+
}
|
|
@@ -6,6 +6,7 @@ export interface GithubKeyEnv {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Assemble the `AppCredentials` the signer needs from the adapter's `backend` (app id,
|
|
9
|
-
* installation) and the Worker's private-key secret. Throws
|
|
9
|
+
* installation) and the Worker's private-key secret. Throws a CairnError naming
|
|
10
|
+
* `github.app-unreachable` when the secret is unset, since the App cannot authenticate without it.
|
|
10
11
|
*/
|
|
11
12
|
export declare function appCredentials(backend: Pick<BackendConfig, 'appId' | 'installationId'>, env: GithubKeyEnv): AppCredentials;
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
// cairn-cms: the bridge from the adapter's backend config and the Worker's secret to the
|
|
2
|
+
// App signer's input. One tested place owns the join and the missing-secret failure, so the
|
|
3
|
+
// save action (Plan 05) stays thin and a misconfigured Worker fails by name, not with a deep
|
|
4
|
+
// TypeError. Mirrors requireDb/requireOrigin in env.ts.
|
|
5
|
+
import { CairnError } from '../diagnostics/index.js';
|
|
1
6
|
/**
|
|
2
7
|
* Assemble the `AppCredentials` the signer needs from the adapter's `backend` (app id,
|
|
3
|
-
* installation) and the Worker's private-key secret. Throws
|
|
8
|
+
* installation) and the Worker's private-key secret. Throws a CairnError naming
|
|
9
|
+
* `github.app-unreachable` when the secret is unset, since the App cannot authenticate without it.
|
|
4
10
|
*/
|
|
5
11
|
export function appCredentials(backend, env) {
|
|
6
12
|
const privateKeyB64 = env.GITHUB_APP_PRIVATE_KEY_B64;
|
|
7
13
|
if (!privateKeyB64) {
|
|
8
|
-
throw new
|
|
14
|
+
throw new CairnError('github.app-unreachable', {
|
|
15
|
+
message: 'GITHUB_APP_PRIVATE_KEY_B64 is not configured',
|
|
16
|
+
});
|
|
9
17
|
}
|
|
10
18
|
return { appId: backend.appId, installationId: backend.installationId, privateKeyB64 };
|
|
11
19
|
}
|
package/dist/github/types.d.ts
CHANGED
|
@@ -32,3 +32,5 @@ export declare class CommitConflictError extends Error {
|
|
|
32
32
|
readonly path: string;
|
|
33
33
|
constructor(path: string);
|
|
34
34
|
}
|
|
35
|
+
/** Match a commit conflict by class and by name (bundling can alias the class identity). */
|
|
36
|
+
export declare function isConflict(err: unknown): boolean;
|
package/dist/github/types.js
CHANGED
|
@@ -16,3 +16,7 @@ export class CommitConflictError extends Error {
|
|
|
16
16
|
this.name = 'CommitConflictError';
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
/** Match a commit conflict by class and by name (bundling can alias the class identity). */
|
|
20
|
+
export function isConflict(err) {
|
|
21
|
+
return err instanceof CommitConflictError || err?.name === 'CommitConflictError';
|
|
22
|
+
}
|
package/dist/log/events.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type CairnLogEvent = 'auth.link.requested' | 'auth.link.send_failed' | 'auth.token.minted' | 'auth.token.confirmed' | 'auth.session.created' | 'auth.session.destroyed' | 'commit.succeeded' | 'commit.failed' | 'entry.published' | 'entry.discarded' | 'publish.failed' | 'github.unreachable' | 'guard.rejected';
|
|
1
|
+
export type CairnLogEvent = 'auth.link.requested' | 'auth.link.send_failed' | 'auth.token.minted' | 'auth.token.confirmed' | 'auth.session.created' | 'auth.session.destroyed' | 'commit.succeeded' | 'commit.failed' | 'config.invalid' | 'entry.published' | 'entry.discarded' | 'publish.failed' | 'github.unreachable' | 'guard.rejected';
|
|
@@ -37,6 +37,8 @@ export interface SiteConfig {
|
|
|
37
37
|
[key: string]: unknown;
|
|
38
38
|
}
|
|
39
39
|
export declare class SiteConfigError extends Error {
|
|
40
|
+
/** The registered diagnostic condition a malformed site config maps to (mirrors CairnError). */
|
|
41
|
+
readonly conditionId = "config.site-config-invalid";
|
|
40
42
|
constructor(message: string);
|
|
41
43
|
}
|
|
42
44
|
/** Parse the YAML site-config text into a typed object. Throws SiteConfigError on a malformed root. */
|
package/dist/nav/site-config.js
CHANGED
|
@@ -60,6 +60,8 @@ export function validateNavTree(value, maxDepth) {
|
|
|
60
60
|
return walk(value, 1);
|
|
61
61
|
}
|
|
62
62
|
export class SiteConfigError extends Error {
|
|
63
|
+
/** The registered diagnostic condition a malformed site config maps to (mirrors CairnError). */
|
|
64
|
+
conditionId = 'config.site-config-invalid';
|
|
63
65
|
constructor(message) {
|
|
64
66
|
super(message);
|
|
65
67
|
this.name = 'SiteConfigError';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ConceptDescriptor } from '../content/types.js';
|
|
2
|
+
/** The views the single-mount admin can render, discriminated for the dispatcher's switch. */
|
|
3
|
+
export type AdminView = {
|
|
4
|
+
view: 'index';
|
|
5
|
+
} | {
|
|
6
|
+
view: 'login';
|
|
7
|
+
} | {
|
|
8
|
+
view: 'confirm';
|
|
9
|
+
} | {
|
|
10
|
+
view: 'list';
|
|
11
|
+
concept: ConceptDescriptor;
|
|
12
|
+
} | {
|
|
13
|
+
view: 'edit';
|
|
14
|
+
concept: ConceptDescriptor;
|
|
15
|
+
id: string;
|
|
16
|
+
} | {
|
|
17
|
+
view: 'editors';
|
|
18
|
+
} | {
|
|
19
|
+
view: 'nav';
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Parse a raw `URL.pathname` (the caller passes `event.url.pathname`, never a SvelteKit rest
|
|
23
|
+
* param) into the admin view it names. A single trailing slash is tolerated everywhere; empty
|
|
24
|
+
* internal segments are not. Each segment is percent-decoded individually, so an encoded slash
|
|
25
|
+
* stays inside its segment, where it can never match a concept id or pass `isValidId` and so
|
|
26
|
+
* falls through to null.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseAdminPath(pathname: string, concepts: ConceptDescriptor[]): AdminView | null;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { findConcept } from '../content/concepts.js';
|
|
2
|
+
import { isValidId } from '../content/ids.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fixed first segments that never resolve as concepts. The engine only allows posts and pages
|
|
5
|
+
* today, so no collision is possible, but the parser does not depend on that: a reserved
|
|
6
|
+
* segment wins before concept lookup. `settings` has no view yet; AdminLayout already links
|
|
7
|
+
* the sidebar to /admin/settings, so the URL is spoken for.
|
|
8
|
+
*/
|
|
9
|
+
const RESERVED_SEGMENTS = new Set(['login', 'auth', 'editors', 'nav', 'settings']);
|
|
10
|
+
/**
|
|
11
|
+
* Parse a raw `URL.pathname` (the caller passes `event.url.pathname`, never a SvelteKit rest
|
|
12
|
+
* param) into the admin view it names. A single trailing slash is tolerated everywhere; empty
|
|
13
|
+
* internal segments are not. Each segment is percent-decoded individually, so an encoded slash
|
|
14
|
+
* stays inside its segment, where it can never match a concept id or pass `isValidId` and so
|
|
15
|
+
* falls through to null.
|
|
16
|
+
*/
|
|
17
|
+
export function parseAdminPath(pathname, concepts) {
|
|
18
|
+
if (pathname !== '/admin' && !pathname.startsWith('/admin/'))
|
|
19
|
+
return null;
|
|
20
|
+
let rest = pathname.slice('/admin'.length);
|
|
21
|
+
// Tolerate exactly one trailing slash; a doubled one leaves an empty segment behind.
|
|
22
|
+
if (rest.endsWith('/'))
|
|
23
|
+
rest = rest.slice(0, -1);
|
|
24
|
+
if (rest === '')
|
|
25
|
+
return { view: 'index' };
|
|
26
|
+
const rawSegments = rest.slice(1).split('/');
|
|
27
|
+
if (rawSegments.includes(''))
|
|
28
|
+
return null;
|
|
29
|
+
let segments;
|
|
30
|
+
try {
|
|
31
|
+
segments = rawSegments.map((segment) => decodeURIComponent(segment));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Malformed percent encoding is an unrecognized shape, not a server error.
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (segments.length === 1) {
|
|
38
|
+
const [head] = segments;
|
|
39
|
+
if (head === 'login')
|
|
40
|
+
return { view: 'login' };
|
|
41
|
+
if (head === 'editors')
|
|
42
|
+
return { view: 'editors' };
|
|
43
|
+
if (head === 'nav')
|
|
44
|
+
return { view: 'nav' };
|
|
45
|
+
if (RESERVED_SEGMENTS.has(head))
|
|
46
|
+
return null;
|
|
47
|
+
const concept = findConcept(concepts, head);
|
|
48
|
+
return concept ? { view: 'list', concept } : null;
|
|
49
|
+
}
|
|
50
|
+
if (segments.length === 2) {
|
|
51
|
+
const [head, tail] = segments;
|
|
52
|
+
if (head === 'auth')
|
|
53
|
+
return tail === 'confirm' ? { view: 'confirm' } : null;
|
|
54
|
+
if (RESERVED_SEGMENTS.has(head))
|
|
55
|
+
return null;
|
|
56
|
+
const concept = findConcept(concepts, head);
|
|
57
|
+
if (!concept || !isValidId(tail))
|
|
58
|
+
return null;
|
|
59
|
+
return { view: 'edit', concept, id: tail };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type ContentRoutesDeps, type LayoutData, type ListData, type EditData } from './content-routes.js';
|
|
2
|
+
import { type NavLoadData } from './nav-routes.js';
|
|
3
|
+
import type { AuthBranding, SendMagicLink } from '../email.js';
|
|
4
|
+
import type { AuthEnv, Editor } from '../auth/types.js';
|
|
5
|
+
import type { GithubKeyEnv } from '../github/credentials.js';
|
|
6
|
+
import type { CairnRuntime } from '../content/types.js';
|
|
7
|
+
import type { CookieJar, EventBase } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* The structural event the single-mount load reads: the union of what the wrapped loads need
|
|
10
|
+
* (ContentEvent minus params, which the dispatcher synthesizes, plus RequestContext's cookies
|
|
11
|
+
* and setHeaders). A real SvelteKit RequestEvent satisfies it.
|
|
12
|
+
*/
|
|
13
|
+
export interface AdminEvent extends EventBase<GithubKeyEnv & AuthEnv> {
|
|
14
|
+
cookies: CookieJar;
|
|
15
|
+
setHeaders(headers: Record<string, string>): void;
|
|
16
|
+
}
|
|
17
|
+
/** Injectable dependencies. Branding defaults from the runtime's siteName and sender, so a
|
|
18
|
+
* site overrides it only to change the magic-link email identity; `send` and `mintToken`
|
|
19
|
+
* are the same seams the underlying factories take. */
|
|
20
|
+
export interface CairnAdminDeps {
|
|
21
|
+
branding?: AuthBranding;
|
|
22
|
+
send?: SendMagicLink;
|
|
23
|
+
mintToken?: ContentRoutesDeps['mintToken'];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One admin view's data, discriminated for the admin page component's switch. The public
|
|
27
|
+
* views (login, confirm) carry no layout; every authed view pairs the shared layout with its
|
|
28
|
+
* page data, the same shapes the per-surface loads have always returned.
|
|
29
|
+
*/
|
|
30
|
+
export type AdminData = {
|
|
31
|
+
view: 'login';
|
|
32
|
+
page: {
|
|
33
|
+
siteName: string;
|
|
34
|
+
error: string | null;
|
|
35
|
+
csrf: string;
|
|
36
|
+
};
|
|
37
|
+
} | {
|
|
38
|
+
view: 'confirm';
|
|
39
|
+
page: {
|
|
40
|
+
token: string;
|
|
41
|
+
siteName: string;
|
|
42
|
+
error: string | null;
|
|
43
|
+
csrf: string;
|
|
44
|
+
};
|
|
45
|
+
} | {
|
|
46
|
+
view: 'list';
|
|
47
|
+
layout: LayoutData;
|
|
48
|
+
page: ListData;
|
|
49
|
+
} | {
|
|
50
|
+
view: 'edit';
|
|
51
|
+
layout: LayoutData;
|
|
52
|
+
page: EditData;
|
|
53
|
+
} | {
|
|
54
|
+
view: 'editors';
|
|
55
|
+
layout: LayoutData;
|
|
56
|
+
page: {
|
|
57
|
+
editors: Editor[];
|
|
58
|
+
self: string;
|
|
59
|
+
};
|
|
60
|
+
} | {
|
|
61
|
+
view: 'nav';
|
|
62
|
+
layout: LayoutData;
|
|
63
|
+
page: NavLoadData;
|
|
64
|
+
};
|
|
65
|
+
export declare function createCairnAdmin(runtime: CairnRuntime, deps?: CairnAdminDeps): {
|
|
66
|
+
load: (event: AdminEvent) => Promise<AdminData>;
|
|
67
|
+
actions: {
|
|
68
|
+
request: (event: AdminEvent) => Promise<import("./auth-routes.js").RequestResult>;
|
|
69
|
+
confirm: (event: AdminEvent) => Promise<never>;
|
|
70
|
+
logout: (event: AdminEvent) => Promise<never>;
|
|
71
|
+
create: (event: AdminEvent) => Promise<never>;
|
|
72
|
+
save: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
73
|
+
publish: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
74
|
+
discard: (event: AdminEvent) => Promise<never>;
|
|
75
|
+
rename: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
76
|
+
delete: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
77
|
+
publishAll: (event: AdminEvent) => Promise<never>;
|
|
78
|
+
addEditor: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<{
|
|
79
|
+
error: string;
|
|
80
|
+
}> | {
|
|
81
|
+
ok: true;
|
|
82
|
+
}>;
|
|
83
|
+
removeEditor: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<{
|
|
84
|
+
error: string;
|
|
85
|
+
}> | {
|
|
86
|
+
ok: true;
|
|
87
|
+
}>;
|
|
88
|
+
setRole: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<{
|
|
89
|
+
error: string;
|
|
90
|
+
}> | {
|
|
91
|
+
ok: true;
|
|
92
|
+
}>;
|
|
93
|
+
};
|
|
94
|
+
};
|