@glw907/cairn-cms 0.62.2 → 0.76.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 +216 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/auth/types.d.ts +7 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +22 -11
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/ConceptList.svelte +25 -4
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +179 -2
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.d.ts +15 -0
- package/dist/content/field-rules.js +38 -0
- package/dist/content/fields.d.ts +169 -0
- package/dist/content/fields.js +41 -0
- package/dist/content/fieldset.d.ts +107 -0
- package/dist/content/fieldset.js +386 -0
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +10 -5
- package/dist/delivery/public-routes.js +25 -2
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +18 -10
- package/dist/index.js +9 -5
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/log/events.d.ts +1 -1
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/manifest.d.ts +11 -0
- package/dist/media/manifest.js +13 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/highlight.d.ts +9 -0
- package/dist/render/highlight.js +206 -0
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +13 -2
- package/dist/render/registry.d.ts +44 -36
- package/dist/render/registry.js +47 -6
- package/dist/render/rehype-dispatch.d.ts +6 -10
- package/dist/render/rehype-dispatch.js +38 -17
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.d.ts +10 -0
- package/dist/render/sanitize-schema.js +30 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/guard.js +10 -0
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +17 -2
- package/src/lib/ambient.ts +7 -0
- package/src/lib/auth/types.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +26 -14
- package/src/lib/components/ConceptList.svelte +41 -4
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +39 -0
- package/src/lib/content/fields.ts +178 -0
- package/src/lib/content/fieldset.ts +470 -0
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +36 -4
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +40 -18
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/log/events.ts +1 -0
- package/src/lib/media/index.ts +1 -0
- package/src/lib/media/manifest.ts +14 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/highlight.ts +259 -0
- package/src/lib/render/pipeline.ts +13 -8
- package/src/lib/render/registry.ts +88 -42
- package/src/lib/render/rehype-dispatch.ts +47 -16
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +32 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/guard.ts +15 -0
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -89
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -167
- package/src/lib/content/validate.ts +0 -90
package/dist/sveltekit/guard.js
CHANGED
|
@@ -32,6 +32,16 @@ function isLocalHost(hostname) {
|
|
|
32
32
|
export function createAuthGuard() {
|
|
33
33
|
return async function handle({ event, resolve }) {
|
|
34
34
|
const { pathname } = event.url;
|
|
35
|
+
// Fail closed if the dev-backend flag is set in a deployed runtime. Read both env sources: a
|
|
36
|
+
// Cloudflare Worker var lands on platform.env, an adapter-node OS var on process.env. A correct
|
|
37
|
+
// production build already eliminated the dev backend (the consumer gates it on the build-foldable
|
|
38
|
+
// `dev`), so a set flag signals a polluted environment; refuse loudly.
|
|
39
|
+
const platformFlag = event.platform?.env?.CAIRN_DEV_BACKEND;
|
|
40
|
+
const processFlag = typeof process !== 'undefined' ? process.env?.CAIRN_DEV_BACKEND : undefined;
|
|
41
|
+
if (platformFlag === '1' || platformFlag === true || processFlag === '1') {
|
|
42
|
+
log.error('guard.rejected', { reason: 'dev_backend_in_prod', path: pathname });
|
|
43
|
+
return new Response('cairn: the dev backend flag is set in a deployed environment. Unset CAIRN_DEV_BACKEND.', { status: 503 });
|
|
44
|
+
}
|
|
35
45
|
// Rule 2 - non-admin: restore the framework's strict Origin check the consumer disabled when
|
|
36
46
|
// they set checkOrigin: false to hand cairn the admin CSRF authority.
|
|
37
47
|
if (!isAdminPath(pathname)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CairnRuntime } from '../content/types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { BackendEnv } from '../github/credentials.js';
|
|
3
3
|
/** The `/admin/healthz` payload. */
|
|
4
4
|
export interface HealthData {
|
|
5
5
|
ok: boolean;
|
|
@@ -10,9 +10,13 @@ export interface HealthData {
|
|
|
10
10
|
};
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Run the signing self-test against the configured App id and the Worker's key secret. The self-test
|
|
15
|
+
* is GitHub-specific, so it narrows the provider on `kind === 'github-app'` for the App id; a
|
|
16
|
+
* non-GitHub backend skips the signing check.
|
|
17
|
+
*/
|
|
14
18
|
export declare function healthLoad(event: {
|
|
15
19
|
platform?: {
|
|
16
|
-
env?:
|
|
20
|
+
env?: BackendEnv;
|
|
17
21
|
};
|
|
18
22
|
}, runtime: CairnRuntime): Promise<HealthData>;
|
package/dist/sveltekit/health.js
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
// PKCS#1-to-PKCS#8 conversion is caught early (spec §7.8). The payload is pass/fail and a
|
|
3
3
|
// coarse detail only; it never carries the key or a token.
|
|
4
4
|
import { signingSelfTest } from '../github/signing.js';
|
|
5
|
-
|
|
5
|
+
import { isGithubApp } from '../github/backend.js';
|
|
6
|
+
/**
|
|
7
|
+
* Run the signing self-test against the configured App id and the Worker's key secret. The self-test
|
|
8
|
+
* is GitHub-specific, so it narrows the provider on `kind === 'github-app'` for the App id; a
|
|
9
|
+
* non-GitHub backend skips the signing check.
|
|
10
|
+
*/
|
|
6
11
|
export async function healthLoad(event, runtime) {
|
|
7
12
|
const key = event.platform?.env?.GITHUB_APP_PRIVATE_KEY_B64;
|
|
8
|
-
const
|
|
9
|
-
|
|
13
|
+
const provider = runtime.backend;
|
|
14
|
+
const githubAppSigning = isGithubApp(provider) && key
|
|
15
|
+
? await signingSelfTest(provider.appId, key)
|
|
10
16
|
: { ok: false, detail: 'GITHUB_APP_PRIVATE_KEY_B64 is not configured' };
|
|
11
17
|
return { ok: githubAppSigning.ok, checks: { githubAppSigning } };
|
|
12
18
|
}
|
|
@@ -10,5 +10,5 @@ export { parseAdminPath, type AdminView } from './admin-dispatch.js';
|
|
|
10
10
|
export { createCairnAdmin, type CairnAdminDeps, type AdminData } from './cairn-admin.js';
|
|
11
11
|
export { healthLoad, type HealthData } from './health.js';
|
|
12
12
|
export type { RequestContext, CookieJar, HandleInput } from './types.js';
|
|
13
|
-
export type {
|
|
13
|
+
export type { BackendEnv } from '../github/credentials.js';
|
|
14
14
|
export type { AuthEnv } from '../auth/types.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type GithubKeyEnv } from '../github/credentials.js';
|
|
2
1
|
import { type NavNode } from '../nav/site-config.js';
|
|
3
2
|
import type { CairnRuntime } from '../content/types.js';
|
|
3
|
+
import type { Backend } from '../github/backend.js';
|
|
4
4
|
import type { ContentEvent } from './content-routes.js';
|
|
5
5
|
/** One page option for the URL picker datalist. */
|
|
6
6
|
export interface NavPageOption {
|
|
@@ -19,13 +19,14 @@ export interface NavLoadData {
|
|
|
19
19
|
saved: boolean;
|
|
20
20
|
error: string | null;
|
|
21
21
|
}
|
|
22
|
-
/** Injectable dependencies;
|
|
22
|
+
/** Injectable dependencies; a test injects a live `Backend` so the read and commit paths run with no real token mint. */
|
|
23
23
|
export interface NavRoutesDeps {
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Override the resolved content backend. A test injects a live `Backend` (a `makeGithubBackend`
|
|
26
|
+
* over a fetch double) so the read and commit paths run with no real token mint. When set it
|
|
27
|
+
* replaces the per-handler `locals.backend ?? runtime.backend.connect(env)` resolve.
|
|
27
28
|
*/
|
|
28
|
-
|
|
29
|
+
backend?: Backend;
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
32
|
*
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// The admin nav-editing routes: the load and save a site's /admin/nav shim calls. A factory closes
|
|
2
|
-
// over the composed runtime
|
|
3
|
-
//
|
|
2
|
+
// over the composed runtime, mirroring createContentRoutes, so the read and commit paths are
|
|
3
|
+
// unit-testable against a fetch double behind an injected Backend.
|
|
4
4
|
import { redirect, error } from '@sveltejs/kit';
|
|
5
|
-
import { appCredentials } from '../github/credentials.js';
|
|
6
|
-
import { cachedInstallationToken } from '../github/signing.js';
|
|
7
|
-
import { listMarkdown, readRaw, commitFile } from '../github/repo.js';
|
|
8
5
|
import { isConflict } from '../github/types.js';
|
|
9
6
|
import { log } from '../log/index.js';
|
|
10
7
|
import { parseSiteConfig, extractMenu, validateNavTree, setMenu } from '../nav/site-config.js';
|
|
@@ -13,13 +10,19 @@ import { requireSession } from './guard.js';
|
|
|
13
10
|
*
|
|
14
11
|
*/
|
|
15
12
|
export function createNavRoutes(runtime, deps = {}) {
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the live content backend for one request: the test seam, then the dev double's
|
|
15
|
+
* `event.locals.backend`, then the production `runtime.backend.connect(env)`.
|
|
16
|
+
*/
|
|
17
|
+
function resolveBackend(event) {
|
|
18
|
+
return deps.backend ?? event.locals.backend ?? runtime.backend.connect(event.platform?.env ?? {});
|
|
19
|
+
}
|
|
17
20
|
/** List page-like concepts (routable, not dated) for the URL picker. Best-effort per concept. */
|
|
18
|
-
async function pageOptions(
|
|
21
|
+
async function pageOptions(backend) {
|
|
19
22
|
const pageConcepts = runtime.concepts.filter((c) => c.routing.routable && !c.routing.dated);
|
|
20
23
|
const lists = await Promise.all(pageConcepts.map(async (c) => {
|
|
21
24
|
try {
|
|
22
|
-
const files = await
|
|
25
|
+
const files = await backend.readEntries(c.dir, backend.defaultBranch);
|
|
23
26
|
return files.map((f) => ({ label: f.id, url: `/${f.id}` }));
|
|
24
27
|
}
|
|
25
28
|
catch {
|
|
@@ -36,17 +39,11 @@ export function createNavRoutes(runtime, deps = {}) {
|
|
|
36
39
|
throw error(404, 'No navigation menu configured');
|
|
37
40
|
const maxDepth = config.maxDepth ?? 2;
|
|
38
41
|
const menu = { name: config.menuName, label: config.label, maxDepth };
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
token = await mintToken(event.platform?.env ?? {});
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return { menu, tree: [], pages: [], saved: false, error: 'Could not authenticate with GitHub.' };
|
|
45
|
-
}
|
|
42
|
+
const backend = resolveBackend(event);
|
|
46
43
|
let tree = [];
|
|
47
44
|
let raw = null;
|
|
48
45
|
try {
|
|
49
|
-
raw = await
|
|
46
|
+
raw = await backend.readFile(config.configPath, backend.defaultBranch);
|
|
50
47
|
}
|
|
51
48
|
catch {
|
|
52
49
|
// An unreadable config degrades to an empty tree; the first save writes a clean menu.
|
|
@@ -69,7 +66,7 @@ export function createNavRoutes(runtime, deps = {}) {
|
|
|
69
66
|
return {
|
|
70
67
|
menu,
|
|
71
68
|
tree,
|
|
72
|
-
pages: await pageOptions(
|
|
69
|
+
pages: await pageOptions(backend),
|
|
73
70
|
saved: event.url.searchParams.get('saved') === '1',
|
|
74
71
|
error: event.url.searchParams.get('error'),
|
|
75
72
|
};
|
|
@@ -90,13 +87,18 @@ export function createNavRoutes(runtime, deps = {}) {
|
|
|
90
87
|
const message = err instanceof Error ? err.message : 'Invalid navigation';
|
|
91
88
|
throw redirect(303, `/admin/nav?error=${encodeURIComponent(message)}`);
|
|
92
89
|
}
|
|
93
|
-
const
|
|
94
|
-
|
|
90
|
+
const backend = resolveBackend(event);
|
|
91
|
+
// Read the head BEFORE the content, so this expectedHead is at-or-before the bytes the commit
|
|
92
|
+
// merges. The nav write lands on the default branch and triggers a deploy, so it is fail-closed:
|
|
93
|
+
// a concurrent commit to the config moves the head off this value and the commit throws a
|
|
94
|
+
// conflict, surfacing the reload-and-reapply prompt below rather than a silent last-writer-wins.
|
|
95
|
+
const head = await backend.branchHead(backend.defaultBranch);
|
|
96
|
+
const raw = await backend.readFile(config.configPath, backend.defaultBranch);
|
|
95
97
|
if (raw === null)
|
|
96
98
|
throw error(404, 'Site config not found');
|
|
97
99
|
const commitFields = { concept: 'nav', id: 'site-config', editor: editor.email };
|
|
98
100
|
try {
|
|
99
|
-
await
|
|
101
|
+
await backend.commit(backend.defaultBranch, [{ path: config.configPath, content: setMenu(raw, config.menuName, tree) }], { name: editor.displayName, email: editor.email }, `Update ${config.label.toLowerCase()}`, head ?? undefined);
|
|
100
102
|
log.info('commit.succeeded', commitFields);
|
|
101
103
|
}
|
|
102
104
|
catch (err) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AuthEnv, Editor } from '../auth/types.js';
|
|
2
|
+
import type { Backend } from '../github/backend.js';
|
|
2
3
|
export interface CookieSetOptions {
|
|
3
4
|
path: string;
|
|
4
5
|
httpOnly?: boolean;
|
|
@@ -33,6 +34,7 @@ export interface EventBase<Env> {
|
|
|
33
34
|
request: Request;
|
|
34
35
|
locals: {
|
|
35
36
|
editor?: Editor | null;
|
|
37
|
+
backend?: Backend;
|
|
36
38
|
};
|
|
37
39
|
platform?: PlatformContext<Env>;
|
|
38
40
|
}
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -50,11 +50,11 @@ export interface AdapterFacts {
|
|
|
50
50
|
owner?: string;
|
|
51
51
|
/** `cairn.backend.repo`. */
|
|
52
52
|
repo?: string;
|
|
53
|
-
/** `cairn.
|
|
53
|
+
/** `cairn.email.from`. */
|
|
54
54
|
from?: string;
|
|
55
55
|
/**
|
|
56
|
-
* `cairn.
|
|
57
|
-
*
|
|
56
|
+
* `cairn.media.bucketBinding`, the media R2 binding name; undefined when the adapter declares no
|
|
57
|
+
* media. The doctor's conditional media-bucket check reads it.
|
|
58
58
|
*/
|
|
59
59
|
mediaBucketBinding?: string;
|
|
60
60
|
}
|
package/dist/vite/index.js
CHANGED
|
@@ -22,10 +22,16 @@ function virtualSource(opts, mode) {
|
|
|
22
22
|
.join('\n');
|
|
23
23
|
// In write mode the committed file may not exist yet, so do not import it.
|
|
24
24
|
const committedImport = mode === 'verify' ? `import committed from ${JSON.stringify(manifestPath + '?raw')};` : '';
|
|
25
|
-
|
|
25
|
+
// In verify mode, run verifyReferences after verifyManifest, inside the generated source where the
|
|
26
|
+
// built manifest is in scope. References have no prerender backstop, so this build gate is their only
|
|
27
|
+
// integrity authority; it cannot move to the verifyManifestFromVite TS call site, where `built` does
|
|
28
|
+
// not exist (it lives only in this evaluated string).
|
|
29
|
+
const resultExpr = mode === 'write'
|
|
30
|
+
? 'serializeManifest(built)'
|
|
31
|
+
: '(verifyManifest(built, committed), verifyReferences(built), "ok")';
|
|
26
32
|
return `
|
|
27
33
|
import { buildSiteManifest } from '@glw907/cairn-cms/delivery/data';
|
|
28
|
-
import { serializeManifest, verifyManifest } from '@glw907/cairn-cms';
|
|
34
|
+
import { serializeManifest, verifyManifest, verifyReferences } from '@glw907/cairn-cms';
|
|
29
35
|
import { cairn, siteConfig } from ${JSON.stringify(opts.configModule)};
|
|
30
36
|
${committedImport}
|
|
31
37
|
const globs = {
|
|
@@ -212,13 +218,16 @@ function adapterFactsSource(opts) {
|
|
|
212
218
|
return `
|
|
213
219
|
import { cairn } from ${JSON.stringify(opts.configModule)};
|
|
214
220
|
const backend = cairn?.backend ?? {};
|
|
215
|
-
const
|
|
216
|
-
const
|
|
221
|
+
const email = cairn?.email ?? {};
|
|
222
|
+
const media = cairn?.media ?? {};
|
|
217
223
|
const facts = {};
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
if (typeof
|
|
221
|
-
if (typeof
|
|
224
|
+
// The owner/repo identity is GitHub-specific, so it is read only off the github-app provider.
|
|
225
|
+
if (backend.kind === 'github-app') {
|
|
226
|
+
if (typeof backend.owner === 'string') facts.owner = backend.owner;
|
|
227
|
+
if (typeof backend.repo === 'string') facts.repo = backend.repo;
|
|
228
|
+
}
|
|
229
|
+
if (typeof email.from === 'string') facts.from = email.from;
|
|
230
|
+
if (typeof media.bucketBinding === 'string') facts.mediaBucketBinding = media.bucketBinding;
|
|
222
231
|
export const result = JSON.stringify(facts);
|
|
223
232
|
`;
|
|
224
233
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glw907/cairn-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"packages/*"
|
|
8
|
+
],
|
|
6
9
|
"sideEffects": [
|
|
7
10
|
"**/*.svelte",
|
|
8
11
|
"**/*.css"
|
|
@@ -34,15 +37,20 @@
|
|
|
34
37
|
"check:docs": "node scripts/docs-links.mjs",
|
|
35
38
|
"check:version": "node scripts/check-version.mjs",
|
|
36
39
|
"check:prose": "node scripts/check-admin-prose.mjs",
|
|
40
|
+
"check:public-tokens": "node scripts/check-public-tokens.mjs",
|
|
41
|
+
"test:reskin": "node scripts/reskin-fixture.mjs",
|
|
37
42
|
"lint": "eslint src/lib",
|
|
38
43
|
"check:comments": "bash scripts/check-comments.sh",
|
|
44
|
+
"check:dev-package": "node scripts/check-dev-package.mjs",
|
|
39
45
|
"prepare": "npm run package",
|
|
40
46
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
41
47
|
"test": "vitest run",
|
|
42
48
|
"test:watch": "vitest",
|
|
43
49
|
"test:unit": "vitest run --project unit",
|
|
44
50
|
"test:integration": "vitest run --project integration",
|
|
45
|
-
"test:component": "vitest run --project component"
|
|
51
|
+
"test:component": "vitest run --project component",
|
|
52
|
+
"emit-template": "node scripts/emit-template.mjs",
|
|
53
|
+
"test:emit": "node --test scripts/emit-template.test.mjs"
|
|
46
54
|
},
|
|
47
55
|
"exports": {
|
|
48
56
|
".": {
|
|
@@ -66,6 +74,10 @@
|
|
|
66
74
|
},
|
|
67
75
|
"./components/spellcheck-assets/spellchecker-wasm.wasm": "./dist/components/spellcheck-assets/spellchecker-wasm.wasm",
|
|
68
76
|
"./components/spellcheck-assets/dictionary-en-us.txt": "./dist/components/spellcheck-assets/dictionary-en-us.txt",
|
|
77
|
+
"./islands": {
|
|
78
|
+
"types": "./dist/islands/index.d.ts",
|
|
79
|
+
"default": "./dist/islands/index.js"
|
|
80
|
+
},
|
|
69
81
|
"./render": {
|
|
70
82
|
"types": "./dist/render/authoring.d.ts",
|
|
71
83
|
"svelte": "./dist/render/authoring.js",
|
|
@@ -131,6 +143,7 @@
|
|
|
131
143
|
"codemirror": "^6.0.2",
|
|
132
144
|
"gray-matter": "^4",
|
|
133
145
|
"hast-util-sanitize": "^5.0.2",
|
|
146
|
+
"hast-util-to-string": "^3.0.1",
|
|
134
147
|
"hastscript": "^9.0.1",
|
|
135
148
|
"heic-to": "^1.5.2",
|
|
136
149
|
"mdast-util-directive": "^3.1.0",
|
|
@@ -143,6 +156,7 @@
|
|
|
143
156
|
"remark-parse": "^11.0.0",
|
|
144
157
|
"remark-rehype": "^11.1.2",
|
|
145
158
|
"remark-stringify": "^11.0.0",
|
|
159
|
+
"shiki": "^4.3.0",
|
|
146
160
|
"spellchecker-wasm": "^0.3.3",
|
|
147
161
|
"unified": "^11.0.5",
|
|
148
162
|
"unist-util-visit": "^5.1.0",
|
|
@@ -159,6 +173,7 @@
|
|
|
159
173
|
"@types/node": "^22.19.19",
|
|
160
174
|
"@vitest/browser": "^4.1.7",
|
|
161
175
|
"@vitest/browser-playwright": "^4.1.7",
|
|
176
|
+
"culori": "^4.0.2",
|
|
162
177
|
"daisyui": "^5.5.23",
|
|
163
178
|
"eslint": "^9.39.4",
|
|
164
179
|
"eslint-plugin-jsdoc": "^63.0.7",
|
package/src/lib/ambient.ts
CHANGED
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
// hand-writes the `declare global` block. The field is optional: the engine's own structural
|
|
7
7
|
// event types read it as `editor?: Editor | null`, and a request the guard has not touched
|
|
8
8
|
// carries no editor at all.
|
|
9
|
+
//
|
|
10
|
+
// `backend` is the per-request content-store channel: the dev-backend handle sets it so the engine
|
|
11
|
+
// resolves it ahead of the real `githubApp` provider (`locals.backend ?? runtime.backend.connect`).
|
|
12
|
+
// Typing it here makes that seam a checked contract, so a mis-keyed write cannot silently fall
|
|
13
|
+
// through to the production provider. A production request never sets it.
|
|
9
14
|
import type { Editor } from './auth/types.js';
|
|
15
|
+
import type { Backend } from './github/backend.js';
|
|
10
16
|
|
|
11
17
|
declare global {
|
|
12
18
|
namespace App {
|
|
13
19
|
interface Locals {
|
|
14
20
|
editor?: Editor | null;
|
|
21
|
+
backend?: Backend;
|
|
15
22
|
}
|
|
16
23
|
}
|
|
17
24
|
}
|
package/src/lib/auth/types.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface AuthEnv {
|
|
|
14
14
|
AUTH_DB?: D1Database;
|
|
15
15
|
/** Canonical origin for confirmation links, never read from a request header (spec 7.1, risk H3). */
|
|
16
16
|
PUBLIC_ORIGIN?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Dev-backend tripwire flag. The dev backend sets this in local development; if it is ever set in
|
|
19
|
+
* a deployed runtime the guard refuses (the build-foldable `dev` gate should have eliminated the
|
|
20
|
+
* dev backend, so a set flag signals a polluted environment). A string from a Worker var or a
|
|
21
|
+
* boolean.
|
|
22
|
+
*/
|
|
23
|
+
CAIRN_DEV_BACKEND?: string | boolean;
|
|
17
24
|
/** Cloudflare Email Sending binding. */
|
|
18
25
|
EMAIL?: {
|
|
19
26
|
send(message: {
|
|
@@ -20,8 +20,7 @@ mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
|
20
20
|
import type { ContentFormFailure } from '../sveltekit/content-routes.js';
|
|
21
21
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
22
22
|
import type { IconSet } from '../render/glyph.js';
|
|
23
|
-
import type {
|
|
24
|
-
import type { MediaResolve } from '../render/resolve-media.js';
|
|
23
|
+
import type { SiteRender } from '../content/types.js';
|
|
25
24
|
|
|
26
25
|
interface Props {
|
|
27
26
|
/** The discriminated view data from `createCairnAdmin`'s load. */
|
|
@@ -37,10 +36,7 @@ mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
|
37
36
|
})
|
|
38
37
|
| null;
|
|
39
38
|
/** The site's design-accurate render pipeline, for the edit view's preview pane. */
|
|
40
|
-
render?:
|
|
41
|
-
md: string,
|
|
42
|
-
opts?: { stagger?: boolean; resolve?: LinkResolve; resolveMedia?: MediaResolve },
|
|
43
|
-
) => string | Promise<string>;
|
|
39
|
+
render?: SiteRender;
|
|
44
40
|
/** The site's component registry, for the edit view's insert palette. */
|
|
45
41
|
registry?: ComponentRegistry;
|
|
46
42
|
/** The site's icon set, for the edit view's guided form fields. */
|
|
@@ -55,7 +55,7 @@ binds out its live `values` and `incomplete` so the dialog can render that previ
|
|
|
55
55
|
values = working;
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
const attributes = $derived(def.attributes ??
|
|
58
|
+
const attributes = $derived(Object.entries(def.attributes ?? {}));
|
|
59
59
|
const flatSlots = $derived((def.slots ?? []).filter((s) => s.kind !== 'repeatable'));
|
|
60
60
|
const repeatableSlots = $derived((def.slots ?? []).filter((s) => s.kind === 'repeatable'));
|
|
61
61
|
|
|
@@ -107,7 +107,7 @@ binds out its live `values` and `incomplete` so the dialog can render that previ
|
|
|
107
107
|
function rowLabel(slot: (typeof repeatableSlots)[number], value: string, index: number): string {
|
|
108
108
|
const fallback = `${slot.label} ${index + 1}`;
|
|
109
109
|
if (!slot.itemLabel) return fallback;
|
|
110
|
-
const key = slot.itemFields
|
|
110
|
+
const key = Object.keys(slot.itemFields ?? {})[0] ?? 'text';
|
|
111
111
|
const derived = slot.itemLabel({ [key]: value }, index);
|
|
112
112
|
return derived && derived.trim() ? derived : fallback;
|
|
113
113
|
}
|
|
@@ -125,14 +125,34 @@ binds out its live `values` and `incomplete` so the dialog can render that previ
|
|
|
125
125
|
return typeof v === 'string' ? v : '';
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// The HTML input type for the text-fallback arm. ComponentForm has no dedicated number/date arm
|
|
129
|
+
// the way FieldInput does, so it folds those scalar types into the one fallback input; everything
|
|
130
|
+
// else renders a plain text box.
|
|
131
|
+
function inputType(type: string): string {
|
|
132
|
+
switch (type) {
|
|
133
|
+
case 'number':
|
|
134
|
+
return 'number';
|
|
135
|
+
case 'date':
|
|
136
|
+
return 'date';
|
|
137
|
+
case 'datetime':
|
|
138
|
+
return 'datetime-local';
|
|
139
|
+
case 'url':
|
|
140
|
+
return 'url';
|
|
141
|
+
case 'email':
|
|
142
|
+
return 'email';
|
|
143
|
+
default:
|
|
144
|
+
return 'text';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
128
148
|
// A required attribute is unmet only for a text/select/icon field left empty; a boolean is always
|
|
129
149
|
// met (its false is a real choice). A required slot is unmet when its string is empty or its
|
|
130
150
|
// repeatable list has no non-empty item. This drives the asterisk-marked fields, the disabled
|
|
131
151
|
// Insert, and (through the bound `incomplete`) the dialog's incomplete preview state.
|
|
132
152
|
const incompleteState = $derived.by(() => {
|
|
133
|
-
for (const field of attributes) {
|
|
153
|
+
for (const [name, field] of attributes) {
|
|
134
154
|
if (!field.required || field.type === 'boolean') continue;
|
|
135
|
-
if (asString(
|
|
155
|
+
if (asString(name) === '') return true;
|
|
136
156
|
}
|
|
137
157
|
for (const slot of def.slots ?? []) {
|
|
138
158
|
if (!slot.required) continue;
|
|
@@ -164,9 +184,9 @@ binds out its live `values` and `incomplete` so the dialog can render that previ
|
|
|
164
184
|
// next to the field meanwhile.
|
|
165
185
|
const errors = $derived.by(() => {
|
|
166
186
|
const out: Record<string, string> = {};
|
|
167
|
-
for (const field of attributes) {
|
|
168
|
-
if (field.required && field.type !== 'boolean' && touched[
|
|
169
|
-
out[
|
|
187
|
+
for (const [name, field] of attributes) {
|
|
188
|
+
if (field.required && field.type !== 'boolean' && touched[name] && asString(name) === '') {
|
|
189
|
+
out[name] = `${field.label} is required.`;
|
|
170
190
|
}
|
|
171
191
|
}
|
|
172
192
|
for (const slot of def.slots ?? []) {
|
|
@@ -201,16 +221,16 @@ binds out its live `values` and `incomplete` so the dialog can render that previ
|
|
|
201
221
|
</script>
|
|
202
222
|
|
|
203
223
|
<div class="flex flex-col gap-3" bind:this={formEl}>
|
|
204
|
-
{#each attributes as field (
|
|
224
|
+
{#each attributes as [name, field] (name)}
|
|
205
225
|
{#if field.type === 'boolean'}
|
|
206
226
|
<label class="label cursor-pointer justify-start gap-2">
|
|
207
227
|
<input
|
|
208
228
|
class="checkbox checkbox-sm"
|
|
209
229
|
type="checkbox"
|
|
210
|
-
aria-invalid={Boolean(errors[
|
|
211
|
-
aria-describedby={errors[
|
|
212
|
-
checked={asBool(
|
|
213
|
-
onchange={(e) => (working.attributes[
|
|
230
|
+
aria-invalid={Boolean(errors[name])}
|
|
231
|
+
aria-describedby={errors[name] ? `err-${name}` : undefined}
|
|
232
|
+
checked={asBool(name)}
|
|
233
|
+
onchange={(e) => (working.attributes[name] = e.currentTarget.checked)}
|
|
214
234
|
/>
|
|
215
235
|
<span class="text-sm">{field.label}</span>
|
|
216
236
|
</label>
|
|
@@ -220,14 +240,14 @@ binds out its live `values` and `incomplete` so the dialog can render that previ
|
|
|
220
240
|
<select
|
|
221
241
|
class="select"
|
|
222
242
|
aria-required={field.required ? 'true' : undefined}
|
|
223
|
-
aria-invalid={Boolean(errors[
|
|
224
|
-
aria-describedby={errors[
|
|
225
|
-
value={asString(
|
|
243
|
+
aria-invalid={Boolean(errors[name])}
|
|
244
|
+
aria-describedby={errors[name] ? `err-${name}` : undefined}
|
|
245
|
+
value={asString(name)}
|
|
226
246
|
onchange={(e) => {
|
|
227
|
-
working.attributes[
|
|
228
|
-
markTouched(
|
|
247
|
+
working.attributes[name] = e.currentTarget.value;
|
|
248
|
+
markTouched(name);
|
|
229
249
|
}}
|
|
230
|
-
onblur={() => markTouched(
|
|
250
|
+
onblur={() => markTouched(name)}
|
|
231
251
|
>
|
|
232
252
|
{#if !field.required}<option value="">—</option>{/if}
|
|
233
253
|
{#each field.options ?? [] as opt (opt)}<option value={opt}>{opt}</option>{/each}
|
|
@@ -239,9 +259,9 @@ binds out its live `values` and `incomplete` so the dialog can render that previ
|
|
|
239
259
|
<IconPicker
|
|
240
260
|
{icons}
|
|
241
261
|
label={field.label}
|
|
242
|
-
value={asString(
|
|
262
|
+
value={asString(name)}
|
|
243
263
|
required={field.required ?? false}
|
|
244
|
-
onChange={(
|
|
264
|
+
onChange={(glyph) => (working.attributes[name] = glyph)}
|
|
245
265
|
/>
|
|
246
266
|
</div>
|
|
247
267
|
{:else}
|
|
@@ -249,19 +269,20 @@ binds out its live `values` and `incomplete` so the dialog can render that previ
|
|
|
249
269
|
<span class="text-sm font-medium">{field.label}{#if field.required}<span data-testid="cairn-pk-req" class="text-error" aria-hidden="true">*</span>{/if}</span>
|
|
250
270
|
<input
|
|
251
271
|
class="input"
|
|
272
|
+
type={inputType(field.type)}
|
|
252
273
|
aria-required={field.required ? 'true' : undefined}
|
|
253
|
-
aria-invalid={Boolean(errors[
|
|
254
|
-
aria-describedby={errors[
|
|
255
|
-
value={asString(
|
|
274
|
+
aria-invalid={Boolean(errors[name])}
|
|
275
|
+
aria-describedby={errors[name] ? `err-${name}` : undefined}
|
|
276
|
+
value={asString(name)}
|
|
256
277
|
oninput={(e) => {
|
|
257
|
-
working.attributes[
|
|
258
|
-
markTouched(
|
|
278
|
+
working.attributes[name] = e.currentTarget.value;
|
|
279
|
+
markTouched(name);
|
|
259
280
|
}}
|
|
260
|
-
onblur={() => markTouched(
|
|
281
|
+
onblur={() => markTouched(name)}
|
|
261
282
|
/>
|
|
262
283
|
</label>
|
|
263
284
|
{/if}
|
|
264
|
-
{#if errors[
|
|
285
|
+
{#if errors[name]}<span id={`err-${name}`} role="alert" class="text-error text-xs">{errors[name]}</span>{/if}
|
|
265
286
|
{/each}
|
|
266
287
|
|
|
267
288
|
{#each flatSlots as slot (slot.name)}
|