@glw907/cairn-cms 0.40.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 +76 -0
- package/README.md +3 -3
- 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 +18 -10
- package/dist/components/ConceptList.svelte.d.ts +4 -8
- package/dist/components/ConfirmPage.svelte +1 -1
- package/dist/components/EditPage.svelte +47 -19
- package/dist/components/EditPage.svelte.d.ts +4 -9
- package/dist/components/EditorToolbar.svelte +4 -0
- 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/link-completion.js +10 -3
- 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/diagnostics/conditions.d.ts +8 -1
- package/dist/diagnostics/conditions.js +68 -1
- package/dist/doctor/bin.d.ts +2 -0
- package/dist/doctor/bin.js +44 -0
- package/dist/doctor/check-send.d.ts +3 -0
- package/dist/doctor/check-send.js +43 -0
- package/dist/doctor/checks-cloudflare.d.ts +5 -0
- package/dist/doctor/checks-cloudflare.js +200 -0
- package/dist/doctor/checks-github.d.ts +2 -0
- package/dist/doctor/checks-github.js +57 -0
- package/dist/doctor/checks-local.d.ts +5 -0
- package/dist/doctor/checks-local.js +112 -0
- package/dist/doctor/cloudflare-api.d.ts +7 -0
- package/dist/doctor/cloudflare-api.js +24 -0
- package/dist/doctor/index.d.ts +23 -0
- package/dist/doctor/index.js +68 -0
- package/dist/doctor/report.d.ts +5 -0
- package/dist/doctor/report.js +21 -0
- package/dist/doctor/run.d.ts +8 -0
- package/dist/doctor/run.js +20 -0
- package/dist/doctor/types.d.ts +41 -0
- package/dist/doctor/types.js +10 -0
- package/dist/doctor/wrangler-config.d.ts +12 -0
- package/dist/doctor/wrangler-config.js +125 -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/signing.d.ts +3 -1
- package/dist/github/signing.js +13 -5
- 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 +78 -44
- 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 +10 -4
- 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 +18 -10
- package/src/lib/components/ConfirmPage.svelte +1 -1
- package/src/lib/components/EditPage.svelte +47 -19
- package/src/lib/components/EditorToolbar.svelte +4 -0
- 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/link-completion.ts +10 -3
- 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/diagnostics/conditions.ts +75 -2
- package/src/lib/doctor/bin.ts +45 -0
- package/src/lib/doctor/check-send.ts +43 -0
- package/src/lib/doctor/checks-cloudflare.ts +222 -0
- package/src/lib/doctor/checks-github.ts +63 -0
- package/src/lib/doctor/checks-local.ts +119 -0
- package/src/lib/doctor/cloudflare-api.ts +33 -0
- package/src/lib/doctor/index.ts +93 -0
- package/src/lib/doctor/report.ts +30 -0
- package/src/lib/doctor/run.ts +23 -0
- package/src/lib/doctor/types.ts +52 -0
- package/src/lib/doctor/wrangler-config.ts +142 -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/signing.ts +13 -6
- package/src/lib/github/types.ts +5 -0
- package/src/lib/log/events.ts +2 -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 +121 -55
- 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
|
@@ -3,6 +3,7 @@ export declare const REASON_CONDITION: {
|
|
|
3
3
|
readonly https: "edge.https-not-forced";
|
|
4
4
|
readonly csrf: "auth.csrf-token-invalid";
|
|
5
5
|
readonly origin: "auth.csrf-origin-mismatch";
|
|
6
|
+
readonly bindings: "config.bindings-missing";
|
|
6
7
|
};
|
|
7
8
|
export type GuardReason = keyof typeof REASON_CONDITION;
|
|
8
9
|
/** Render the Response the guard serves for a rejection, by its condition id. */
|
|
@@ -4,13 +4,35 @@
|
|
|
4
4
|
import { brandedAdminPage } from './admin-response.js';
|
|
5
5
|
import { httpsRequiredPage } from './https-required-page.js';
|
|
6
6
|
import { csrfRequiredPage } from './csrf-required-page.js';
|
|
7
|
+
import { escapeHtml } from '../escape.js';
|
|
8
|
+
import { renderStaticAdminPage } from './static-admin-page.js';
|
|
7
9
|
import { condition } from '../diagnostics/index.js';
|
|
8
10
|
/** The guard.rejected reasons, each mapped to its registered condition id. */
|
|
9
11
|
export const REASON_CONDITION = {
|
|
10
12
|
https: 'edge.https-not-forced',
|
|
11
13
|
csrf: 'auth.csrf-token-invalid',
|
|
12
14
|
origin: 'auth.csrf-origin-mismatch',
|
|
15
|
+
bindings: 'config.bindings-missing',
|
|
13
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* A branded page for an operator fault, built straight from the registered condition's fields so
|
|
19
|
+
* the served copy, the doctor's report, and the readiness checklist say the same thing.
|
|
20
|
+
*/
|
|
21
|
+
function conditionFaultPage(cond) {
|
|
22
|
+
const inner = `
|
|
23
|
+
<span class="eyebrow">
|
|
24
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
|
25
|
+
Site setup required
|
|
26
|
+
</span>
|
|
27
|
+
<h1>${escapeHtml(cond.title)}</h1>
|
|
28
|
+
<p>${escapeHtml(cond.why)}</p>
|
|
29
|
+
|
|
30
|
+
<div class="fix">
|
|
31
|
+
<h2>If you run this site</h2>
|
|
32
|
+
<p>${escapeHtml(cond.remediation)}</p>
|
|
33
|
+
</div>`;
|
|
34
|
+
return renderStaticAdminPage({ title: `${cond.title} · Cairn`, innerHtml: inner });
|
|
35
|
+
}
|
|
14
36
|
/** Render the Response the guard serves for a rejection, by its condition id. */
|
|
15
37
|
export function renderConditionResponse(id, ctx = {}) {
|
|
16
38
|
// Assert the id is registered before rendering, keeping the renderer in 1:1 with the registry.
|
|
@@ -28,6 +50,9 @@ export function renderConditionResponse(id, ctx = {}) {
|
|
|
28
50
|
status: 403,
|
|
29
51
|
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
30
52
|
});
|
|
53
|
+
case REASON_CONDITION.bindings:
|
|
54
|
+
// An operator fault, not a request fault: the Worker deployed without its bindings.
|
|
55
|
+
return brandedAdminPage(500, conditionFaultPage(condition(id)));
|
|
31
56
|
default:
|
|
32
57
|
throw new Error(`no runtime renderer for condition: ${id}`);
|
|
33
58
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { fail } from '@sveltejs/kit';
|
|
2
2
|
import { type GithubKeyEnv } from '../github/credentials.js';
|
|
3
3
|
import { type LinkTarget, type InboundLink } from '../content/manifest.js';
|
|
4
|
-
import type { CookieJar } from './types.js';
|
|
4
|
+
import type { CookieJar, EventBase } from './types.js';
|
|
5
5
|
import type { CairnRuntime, FrontmatterField } from '../content/types.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { Role } from '../auth/types.js';
|
|
7
7
|
/** A sidebar concept entry: just enough to render the nav without shipping validators to the client. */
|
|
8
8
|
export interface NavConcept {
|
|
9
9
|
id: string;
|
|
@@ -89,25 +89,45 @@ export interface EditData {
|
|
|
89
89
|
discardedFlash: boolean;
|
|
90
90
|
}
|
|
91
91
|
/** The structural event the content routes read; a real SvelteKit RequestEvent satisfies it. */
|
|
92
|
-
export interface ContentEvent {
|
|
93
|
-
url: URL;
|
|
92
|
+
export interface ContentEvent extends EventBase<GithubKeyEnv> {
|
|
94
93
|
params: Record<string, string>;
|
|
95
|
-
request: Request;
|
|
96
|
-
locals: {
|
|
97
|
-
editor?: Editor | null;
|
|
98
|
-
};
|
|
99
|
-
platform?: {
|
|
100
|
-
env?: GithubKeyEnv;
|
|
101
|
-
};
|
|
102
94
|
/** SvelteKit's cookie jar. The layout load reads the persisted admin theme and issues the CSRF
|
|
103
95
|
* token. Optional for non-route callers. */
|
|
104
96
|
cookies?: CookieJar;
|
|
105
97
|
}
|
|
106
98
|
/** Injectable dependencies; tests stub the token mint to avoid signing a real key. */
|
|
107
99
|
export interface ContentRoutesDeps {
|
|
108
|
-
/** Mint a GitHub App installation token from the Worker env. Defaults to the real signer.
|
|
109
|
-
|
|
100
|
+
/** Mint a GitHub App installation token from the Worker env. Defaults to the real signer.
|
|
101
|
+
* A bare string works too; the routes await whatever comes back. */
|
|
102
|
+
mintToken?: (env: GithubKeyEnv) => string | Promise<string>;
|
|
103
|
+
}
|
|
104
|
+
/** A blocked save or publish: `fail(400)` when the body links to a target absent from main. */
|
|
105
|
+
export interface SaveFailure {
|
|
106
|
+
/** The one-line human summary every content action failure carries. */
|
|
107
|
+
error: string;
|
|
108
|
+
/** The cairn tokens that resolve to no entry, for the editor's fix-it banner. */
|
|
109
|
+
brokenLinks: string[];
|
|
110
|
+
/** The author's edited markdown, so the editor reseeds with the unsaved work. */
|
|
111
|
+
body: string;
|
|
112
|
+
}
|
|
113
|
+
/** A refused delete: `fail(409)` while other entries still link to this one. */
|
|
114
|
+
export interface DeleteRefusal {
|
|
115
|
+
/** The one-line human summary every content action failure carries. */
|
|
116
|
+
error: string;
|
|
117
|
+
/** The entries whose bodies link to the refused one, for the blockers list. */
|
|
118
|
+
inboundLinks: InboundLink[];
|
|
119
|
+
/** The refused entry's id, so a list view marks the right row. */
|
|
120
|
+
id: string;
|
|
121
|
+
}
|
|
122
|
+
/** A refused rename: `fail(400)` on a bad slug, `fail(409)` on a collision or pending edits. */
|
|
123
|
+
export interface RenameFailure {
|
|
124
|
+
/** The one-line human summary every content action failure carries. */
|
|
125
|
+
error: string;
|
|
110
126
|
}
|
|
127
|
+
/** What a route's single `form` export presents to a view component: whichever content action
|
|
128
|
+
* last failed, merged with every field optional. `error` is always set on a failure; the richer
|
|
129
|
+
* keys identify which guard refused. */
|
|
130
|
+
export type ContentFormFailure = Partial<SaveFailure & DeleteRefusal & RenameFailure>;
|
|
111
131
|
export declare function createContentRoutes(runtime: CairnRuntime, deps?: ContentRoutesDeps): {
|
|
112
132
|
layoutLoad: (event: ContentEvent) => Promise<LayoutData>;
|
|
113
133
|
indexRedirect: () => never;
|
|
@@ -121,5 +141,5 @@ export declare function createContentRoutes(runtime: CairnRuntime, deps?: Conten
|
|
|
121
141
|
deleteAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
122
142
|
listDeleteAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
123
143
|
renameAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
|
|
124
|
-
mintToken: (env: GithubKeyEnv) => Promise<string>;
|
|
144
|
+
mintToken: (env: GithubKeyEnv) => string | Promise<string>;
|
|
125
145
|
};
|
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
// email `send` injection in auth-routes. A shim stays one line: `export const load = routes.editLoad`.
|
|
5
5
|
import { redirect, error, fail } from '@sveltejs/kit';
|
|
6
6
|
import { findConcept } from '../content/concepts.js';
|
|
7
|
-
import { extractCairnLinks, formatCairnToken } from '../content/links.js';
|
|
7
|
+
import { extractCairnLinks, formatCairnToken, rewriteCairnLink } from '../content/links.js';
|
|
8
8
|
import { frontmatterFromForm, parseMarkdown, dateInputValue, serializeMarkdown } from '../content/frontmatter.js';
|
|
9
9
|
import { isValidId, slugify, filenameFromId, composeDatedId, slugFromId, renameId } from '../content/ids.js';
|
|
10
|
-
import { rewriteCairnLink } from '../components/markdown-format.js';
|
|
11
10
|
import { appCredentials } from '../github/credentials.js';
|
|
12
11
|
import { listMarkdown, readRaw, commitFiles } from '../github/repo.js';
|
|
13
12
|
import { branchHeadSha, createBranch, deleteBranch, listBranches } from '../github/branches.js';
|
|
14
13
|
import { PENDING_PREFIX, pendingBranch, parsePendingBranch } from '../content/pending.js';
|
|
15
14
|
import { cachedInstallationToken } from '../github/signing.js';
|
|
16
15
|
import { emptyManifest, manifestEntryFromFile, parseManifest, serializeManifest, upsertEntry, removeEntry, inboundLinks } from '../content/manifest.js';
|
|
17
|
-
import {
|
|
16
|
+
import { isConflict } from '../github/types.js';
|
|
18
17
|
import { log } from '../log/index.js';
|
|
19
18
|
import { issueCsrfToken } from './csrf.js';
|
|
20
19
|
/** The signed-in editor the guard resolved, or a login redirect. Kept local to decouple event shapes. */
|
|
@@ -72,8 +71,9 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
72
71
|
return entry ? [{ concept: entry.concept.id, id: entry.id }] : [];
|
|
73
72
|
});
|
|
74
73
|
}
|
|
75
|
-
catch {
|
|
74
|
+
catch (err) {
|
|
76
75
|
pendingEntries = null;
|
|
76
|
+
log.warn('github.unreachable', { scope: 'layout', error: String(err) });
|
|
77
77
|
}
|
|
78
78
|
return {
|
|
79
79
|
siteName: runtime.siteName,
|
|
@@ -111,9 +111,31 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
111
111
|
return { id: file.id, title: file.id, date: null, draft: false, status };
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
114
|
+
/** Read an entry's list row from its pending branch, so a pending title or draft change shows
|
|
115
|
+
* in the list instead of reading as a lost save. summarize degrades a failed or empty read to
|
|
116
|
+
* an id-only row, so a ghost ref still lists. */
|
|
117
|
+
function pendingRow(concept, id, status, token) {
|
|
118
|
+
return summarize({ id, path: `${concept.dir}/${filenameFromId(id)}` }, token, status, {
|
|
119
|
+
...runtime.backend,
|
|
120
|
+
branch: pendingBranch(concept.id, id),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** The per-file crawl, kept only for a repo with no committed manifest yet: list main's files
|
|
124
|
+
* and read each one for its row, with edited and new rows reading branch-first. */
|
|
125
|
+
async function crawlEntries(concept, pendingIds, token) {
|
|
126
|
+
const files = await listMarkdown(runtime.backend, concept.dir, token);
|
|
127
|
+
const entries = await Promise.all(files.map((f) => (pendingIds.has(f.id) ? pendingRow(concept, f.id, 'edited', token) : summarize(f, token, 'published'))));
|
|
128
|
+
// A ref with no main file is a never-published entry; its row reads from its branch.
|
|
129
|
+
const listed = new Set(files.map((f) => f.id));
|
|
130
|
+
const newRows = await Promise.all([...pendingIds].filter((id) => !listed.has(id)).map((id) => pendingRow(concept, id, 'new', token)));
|
|
131
|
+
return [...entries, ...newRows];
|
|
132
|
+
}
|
|
133
|
+
/** List a concept's entries with their publish status. Published rows project straight from
|
|
134
|
+
* main's manifest, which publish, delete, and rename keep atomically in sync with main, so
|
|
135
|
+
* the listing costs one manifest read plus one branch read per pending entry rather than one
|
|
136
|
+
* read per file. A manifest row with a pending ref is `edited` and reads branch-first; a ref
|
|
137
|
+
* with no manifest row appends a `new` row read from its branch. A listing failure degrades
|
|
138
|
+
* to an inline error, not a thrown 500. */
|
|
117
139
|
async function listLoad(event) {
|
|
118
140
|
sessionOf(event);
|
|
119
141
|
const concept = conceptOf(runtime, event.params);
|
|
@@ -129,28 +151,28 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
129
151
|
return { ...base, entries: [], error: 'Could not authenticate with GitHub.' };
|
|
130
152
|
}
|
|
131
153
|
try {
|
|
132
|
-
const [
|
|
133
|
-
|
|
154
|
+
const [manifestRaw, refs] = await Promise.all([
|
|
155
|
+
readRaw(runtime.backend, runtime.manifestPath, token),
|
|
134
156
|
listBranches(runtime.backend, `${PENDING_PREFIX}${concept.id}/`, token),
|
|
135
157
|
]);
|
|
136
158
|
const pendingIds = new Set(refs.flatMap((name) => {
|
|
137
159
|
const entry = pendingEntryOf(name);
|
|
138
160
|
return entry && entry.concept.id === concept.id ? [entry.id] : [];
|
|
139
161
|
}));
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
162
|
+
// A repo with no committed manifest yet (a fresh site before its first publish) falls back
|
|
163
|
+
// to the crawl; a manifest that parses but is empty is trusted as-is.
|
|
164
|
+
if (manifestRaw === null) {
|
|
165
|
+
return { ...base, entries: await crawlEntries(concept, pendingIds, token), error: null };
|
|
166
|
+
}
|
|
167
|
+
// Newest id first, the same order the crawl's file listing produced.
|
|
168
|
+
const rows = parseManifest(manifestRaw)
|
|
169
|
+
.entries.filter((e) => e.concept === concept.id)
|
|
170
|
+
.sort((a, b) => b.id.localeCompare(a.id));
|
|
171
|
+
const entries = await Promise.all(rows.map((e) => pendingIds.has(e.id)
|
|
172
|
+
? pendingRow(concept, e.id, 'edited', token)
|
|
173
|
+
: { id: e.id, title: e.title, date: e.date ?? null, draft: e.draft, status: 'published' }));
|
|
174
|
+
const listed = new Set(rows.map((e) => e.id));
|
|
175
|
+
const newRows = await Promise.all([...pendingIds].filter((id) => !listed.has(id)).map((id) => pendingRow(concept, id, 'new', token)));
|
|
154
176
|
return { ...base, entries: [...entries, ...newRows], error: null };
|
|
155
177
|
}
|
|
156
178
|
catch {
|
|
@@ -216,18 +238,22 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
216
238
|
const datePrefix = concept.routing.dated ? concept.datePrefix : null;
|
|
217
239
|
const path = `${concept.dir}/${filenameFromId(id)}`;
|
|
218
240
|
// A pending entry reads branch-first: the editor shows the unpublished edits. The manifest
|
|
219
|
-
// (link targets and the inbound-link guard) always reads main, the authoritative copy
|
|
220
|
-
//
|
|
241
|
+
// (link targets and the inbound-link guard) always reads main, the authoritative copy.
|
|
242
|
+
// Stage 1 runs the branch probe, the main-path read, and the manifest read concurrently,
|
|
243
|
+
// so the probe does not serialize ahead of the other two; stage 2 adds the branch read
|
|
244
|
+
// only when the probe found a branch, with the stage-1 main read serving as the published
|
|
245
|
+
// signal either way.
|
|
221
246
|
const branch = pendingBranch(concept.id, id);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
readRaw(
|
|
247
|
+
const [headSha, mainRaw, manifestRaw] = await Promise.all([
|
|
248
|
+
branchHeadSha(runtime.backend, branch, token),
|
|
249
|
+
readRaw(runtime.backend, path, token),
|
|
225
250
|
readRaw(runtime.backend, runtime.manifestPath, token),
|
|
226
|
-
pending ? readRaw(runtime.backend, path, token) : Promise.resolve(null),
|
|
227
251
|
]);
|
|
252
|
+
const pending = headSha !== null;
|
|
253
|
+
const raw = pending ? await readRaw({ ...runtime.backend, branch }, path, token) : mainRaw;
|
|
228
254
|
if (raw === null && !isNew)
|
|
229
255
|
throw error(404, 'Entry not found');
|
|
230
|
-
const published =
|
|
256
|
+
const published = mainRaw !== null;
|
|
231
257
|
const parsed = raw === null ? { frontmatter: {}, body: '' } : parseMarkdown(raw);
|
|
232
258
|
const title = typeof parsed.frontmatter.title === 'string' && parsed.frontmatter.title.trim() ? parsed.frontmatter.title : id;
|
|
233
259
|
let linkTargets = [];
|
|
@@ -265,10 +291,6 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
265
291
|
discardedFlash: event.url.searchParams.get('discarded') === '1',
|
|
266
292
|
};
|
|
267
293
|
}
|
|
268
|
-
/** Match a commit conflict by class and by name (bundling can alias the class identity). */
|
|
269
|
-
function isConflict(err) {
|
|
270
|
-
return err instanceof CommitConflictError || err?.name === 'CommitConflictError';
|
|
271
|
-
}
|
|
272
294
|
/** Log a failed commit: a conflict is the expected last-writer-wins outcome, so it warns with a
|
|
273
295
|
* reason; any other error is unexpected and logs at error with the stringified cause. Publish
|
|
274
296
|
* failures carry the same shape under their own event name. */
|
|
@@ -333,7 +355,12 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
333
355
|
draftLinks.push(formatCairnToken(ref));
|
|
334
356
|
}
|
|
335
357
|
if (absent.length) {
|
|
336
|
-
|
|
358
|
+
const noun = absent.length === 1 ? 'page' : 'pages';
|
|
359
|
+
return fail(400, {
|
|
360
|
+
error: `This page links to ${absent.length} missing ${noun}.`,
|
|
361
|
+
brokenLinks: absent,
|
|
362
|
+
body,
|
|
363
|
+
});
|
|
337
364
|
}
|
|
338
365
|
// Ensure the entry's pending branch exists (cut lazily from main's head on first save), then
|
|
339
366
|
// commit only the entry file there. Main stays untouched until publish, so the branch differs
|
|
@@ -450,11 +477,14 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
450
477
|
next = upsertEntry(next, manifestEntryFromFile(entry.concept, { path: entry.path, raw: entry.raw }));
|
|
451
478
|
published.push({ concept: entry.concept.id, id: entry.id, branch: entry.branch, sha: entry.sha });
|
|
452
479
|
}
|
|
453
|
-
if (published.length === 0)
|
|
454
|
-
|
|
480
|
+
if (published.length === 0) {
|
|
481
|
+
const message = 'Nothing to publish. Every entry is already live.';
|
|
482
|
+
throw redirect(303, `${listPage}?error=${encodeURIComponent(message)}`);
|
|
483
|
+
}
|
|
455
484
|
changes.push({ path: runtime.manifestPath, content: serializeManifest(next) });
|
|
485
|
+
const noun = published.length === 1 ? 'entry' : 'entries';
|
|
456
486
|
try {
|
|
457
|
-
await commitFiles(runtime.backend, changes, { message: `Publish ${published.length}
|
|
487
|
+
await commitFiles(runtime.backend, changes, { message: `Publish ${published.length} ${noun}`, author: { name: editor.displayName, email: editor.email } }, token);
|
|
458
488
|
for (const entry of published) {
|
|
459
489
|
log.info('entry.published', { concept: entry.concept, id: entry.id, editor: editor.email, batch: true });
|
|
460
490
|
}
|
|
@@ -517,7 +547,11 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
517
547
|
const manifest = await readManifest(token);
|
|
518
548
|
const inbound = inboundLinks(manifest, concept.id, id);
|
|
519
549
|
if (inbound.length) {
|
|
520
|
-
return fail(409, {
|
|
550
|
+
return fail(409, {
|
|
551
|
+
error: `Cannot delete ${id}: ${inbound.length} ${inbound.length === 1 ? 'page links' : 'pages link'} to it.`,
|
|
552
|
+
inboundLinks: inbound,
|
|
553
|
+
id,
|
|
554
|
+
});
|
|
521
555
|
}
|
|
522
556
|
// When the entry was never published (absent from main), the branch delete is the whole
|
|
523
557
|
// operation; main has nothing to commit, so the only honest log record is the discard of
|
|
@@ -585,19 +619,19 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
585
619
|
// Pending edits on the branch are keyed to the old id; renaming underneath them would strand
|
|
586
620
|
// them, so refuse until the editor publishes or discards.
|
|
587
621
|
if ((await branchHeadSha(runtime.backend, pendingBranch(concept.id, id), token)) !== null) {
|
|
588
|
-
return fail(409, {
|
|
622
|
+
return fail(409, { error: 'This entry has unpublished edits. Publish or discard them, then rename.' });
|
|
589
623
|
}
|
|
590
624
|
const form = await event.request.formData();
|
|
591
625
|
const newSlug = String(form.get('slug') ?? '').trim();
|
|
592
626
|
if (!isValidId(newSlug)) {
|
|
593
|
-
return fail(400, {
|
|
627
|
+
return fail(400, { error: 'Enter a valid slug: lowercase letters, numbers, and hyphens.' });
|
|
594
628
|
}
|
|
595
629
|
const datePrefix = concept.routing.dated ? concept.datePrefix : null;
|
|
596
630
|
if (concept.routing.dated && /^\d{4}-/.test(newSlug)) {
|
|
597
|
-
return fail(400, {
|
|
631
|
+
return fail(400, { error: 'Leave the date out of the slug.' });
|
|
598
632
|
}
|
|
599
633
|
if (newSlug === slugFromId(id, datePrefix)) {
|
|
600
|
-
return fail(400, {
|
|
634
|
+
return fail(400, { error: 'That is already the slug.' });
|
|
601
635
|
}
|
|
602
636
|
const newId = renameId(id, newSlug, datePrefix);
|
|
603
637
|
const oldPath = `${concept.dir}/${filenameFromId(id)}`;
|
|
@@ -607,7 +641,7 @@ export function createContentRoutes(runtime, deps = {}) {
|
|
|
607
641
|
// concurrent-rename race where another editor renamed onto this path between load and submit.
|
|
608
642
|
const clobber = await readRaw(runtime.backend, newPath, token);
|
|
609
643
|
if (clobber !== null) {
|
|
610
|
-
return fail(409, {
|
|
644
|
+
return fail(409, { error: 'An entry with that slug already exists.' });
|
|
611
645
|
}
|
|
612
646
|
const [entryRaw, manifest] = await Promise.all([
|
|
613
647
|
readRaw(runtime.backend, oldPath, token),
|
package/dist/sveltekit/guard.js
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveSession } from '../auth/store.js';
|
|
|
6
6
|
import { sessionCookieName } from '../auth/crypto.js';
|
|
7
7
|
import { isUnsafeFormRequest, originMatches, validateCsrfToken } from './csrf.js';
|
|
8
8
|
import { applySecurityHeaders } from './admin-response.js';
|
|
9
|
-
import { renderConditionResponse } from './condition-response.js';
|
|
9
|
+
import { renderConditionResponse, REASON_CONDITION } from './condition-response.js';
|
|
10
10
|
import { log } from '../log/index.js';
|
|
11
11
|
/** The login page and the auth endpoints are public; everything else under /admin is gated. */
|
|
12
12
|
function isPublicAdminPath(pathname) {
|
|
@@ -49,6 +49,19 @@ export function createAuthGuard() {
|
|
|
49
49
|
log.warn('guard.rejected', { reason: 'https', path: pathname });
|
|
50
50
|
return renderConditionResponse('edge.https-not-forced', { url: event.url });
|
|
51
51
|
}
|
|
52
|
+
// No auth store binding means no admin path can work: the gated views cannot resolve a
|
|
53
|
+
// session, and a login or confirm POST would die in its action with a raw 500. That is an
|
|
54
|
+
// operator fault, not a sign-in problem, so name the condition on every admin path, the
|
|
55
|
+
// public ones included, instead of rendering a login form that can never succeed.
|
|
56
|
+
const env = event.platform?.env ?? {};
|
|
57
|
+
if (!env.AUTH_DB) {
|
|
58
|
+
log.error('guard.rejected', {
|
|
59
|
+
reason: 'bindings',
|
|
60
|
+
conditionId: REASON_CONDITION.bindings,
|
|
61
|
+
path: pathname,
|
|
62
|
+
});
|
|
63
|
+
return renderConditionResponse(REASON_CONDITION.bindings);
|
|
64
|
+
}
|
|
52
65
|
// Rule 1 - admin: every unsafe form POST carries a valid double-submit token, else the branded
|
|
53
66
|
// 403 before resolve() runs. This covers the public login/auth posts too.
|
|
54
67
|
if (isUnsafeFormRequest(event.request) && !(await validateCsrfToken(event))) {
|
|
@@ -56,9 +69,8 @@ export function createAuthGuard() {
|
|
|
56
69
|
return renderConditionResponse('auth.csrf-token-invalid');
|
|
57
70
|
}
|
|
58
71
|
if (!isPublicAdminPath(pathname)) {
|
|
59
|
-
const env = event.platform?.env ?? {};
|
|
60
72
|
const id = event.cookies.get(sessionCookieName(event.url.protocol === 'https:'));
|
|
61
|
-
const editor = id
|
|
73
|
+
const editor = id ? await resolveSession(env.AUTH_DB, id, Date.now()) : null;
|
|
62
74
|
if (!editor)
|
|
63
75
|
throw redirect(303, '/admin/login');
|
|
64
76
|
event.locals.editor = editor;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// not match, so the editor would otherwise hit an opaque 403. This page names the problem, says why
|
|
5
5
|
// https is needed, and gives the exact Cloudflare fix. The shared shell lives in
|
|
6
6
|
// static-admin-page.ts. See guard.ts.
|
|
7
|
-
import { escapeHtml
|
|
7
|
+
import { escapeHtml } from '../escape.js';
|
|
8
|
+
import { renderStaticAdminPage } from './static-admin-page.js';
|
|
8
9
|
/**
|
|
9
10
|
* Render the full HTML document for the HTTPS-required page.
|
|
10
11
|
* @param httpsUrl The same request rebuilt over https, offered as the one-click recovery link.
|
|
@@ -2,9 +2,11 @@ export { createAuthGuard, requireSession, requireOwner } from './guard.js';
|
|
|
2
2
|
export { createAuthRoutes, type AuthRoutesConfig, type RequestResult } from './auth-routes.js';
|
|
3
3
|
export { createEditorRoutes } from './editors-routes.js';
|
|
4
4
|
export { createContentRoutes } from './content-routes.js';
|
|
5
|
-
export type { NavConcept, LayoutData, EntrySummary, ListData, EditData, ContentEvent, ContentRoutesDeps, } from './content-routes.js';
|
|
5
|
+
export type { NavConcept, LayoutData, EntrySummary, ListData, EditData, ContentEvent, ContentRoutesDeps, SaveFailure, DeleteRefusal, RenameFailure, ContentFormFailure, } from './content-routes.js';
|
|
6
6
|
export { createNavRoutes } from './nav-routes.js';
|
|
7
7
|
export type { NavLoadData, NavPageOption, NavRoutesDeps } from './nav-routes.js';
|
|
8
|
+
export { parseAdminPath, type AdminView } from './admin-dispatch.js';
|
|
9
|
+
export { createCairnAdmin, type CairnAdminDeps, type AdminData } from './cairn-admin.js';
|
|
8
10
|
export { healthLoad, type HealthData } from './health.js';
|
|
9
11
|
export type { RequestContext, CookieJar, HandleInput } from './types.js';
|
|
10
12
|
export type { GithubKeyEnv } from '../github/credentials.js';
|
package/dist/sveltekit/index.js
CHANGED
|
@@ -5,4 +5,6 @@ export { createAuthRoutes } from './auth-routes.js';
|
|
|
5
5
|
export { createEditorRoutes } from './editors-routes.js';
|
|
6
6
|
export { createContentRoutes } from './content-routes.js';
|
|
7
7
|
export { createNavRoutes } from './nav-routes.js';
|
|
8
|
+
export { parseAdminPath } from './admin-dispatch.js';
|
|
9
|
+
export { createCairnAdmin } from './cairn-admin.js';
|
|
8
10
|
export { healthLoad } from './health.js';
|
|
@@ -21,7 +21,9 @@ export interface NavLoadData {
|
|
|
21
21
|
}
|
|
22
22
|
/** Injectable dependencies; tests stub the token mint to avoid signing a real key. */
|
|
23
23
|
export interface NavRoutesDeps {
|
|
24
|
-
|
|
24
|
+
/** Mint a GitHub App installation token from the Worker env. Defaults to the real signer.
|
|
25
|
+
* A bare string works too; the routes await whatever comes back. */
|
|
26
|
+
mintToken?: (env: GithubKeyEnv) => string | Promise<string>;
|
|
25
27
|
}
|
|
26
28
|
export declare function createNavRoutes(runtime: CairnRuntime, deps?: NavRoutesDeps): {
|
|
27
29
|
navLoad: (event: ContentEvent) => Promise<NavLoadData>;
|
|
@@ -5,7 +5,7 @@ import { redirect, error } from '@sveltejs/kit';
|
|
|
5
5
|
import { appCredentials } from '../github/credentials.js';
|
|
6
6
|
import { cachedInstallationToken } from '../github/signing.js';
|
|
7
7
|
import { listMarkdown, readRaw, commitFile } from '../github/repo.js';
|
|
8
|
-
import {
|
|
8
|
+
import { isConflict } from '../github/types.js';
|
|
9
9
|
import { log } from '../log/index.js';
|
|
10
10
|
import { parseSiteConfig, extractMenu, validateNavTree, setMenu } from '../nav/site-config.js';
|
|
11
11
|
/** The signed-in editor the guard resolved, or a login redirect. */
|
|
@@ -15,10 +15,6 @@ function sessionOf(event) {
|
|
|
15
15
|
throw redirect(303, '/admin/login');
|
|
16
16
|
return editor;
|
|
17
17
|
}
|
|
18
|
-
/** Match a commit conflict by class and by name (bundling can alias the class identity). */
|
|
19
|
-
function isConflict(err) {
|
|
20
|
-
return err instanceof CommitConflictError || err?.name === 'CommitConflictError';
|
|
21
|
-
}
|
|
22
18
|
export function createNavRoutes(runtime, deps = {}) {
|
|
23
19
|
const mintToken = deps.mintToken ?? ((env) => cachedInstallationToken(appCredentials(runtime.backend, env)));
|
|
24
20
|
/** List page-like concepts (routable, not dated) for the URL picker. Best-effort per concept. */
|
|
@@ -51,14 +47,27 @@ export function createNavRoutes(runtime, deps = {}) {
|
|
|
51
47
|
return { menu, tree: [], pages: [], saved: false, error: 'Could not authenticate with GitHub.' };
|
|
52
48
|
}
|
|
53
49
|
let tree = [];
|
|
50
|
+
let raw = null;
|
|
54
51
|
try {
|
|
55
|
-
|
|
56
|
-
if (raw !== null)
|
|
57
|
-
tree = extractMenu(parseSiteConfig(raw), config.menuName, maxDepth);
|
|
52
|
+
raw = await readRaw(runtime.backend, config.configPath, token);
|
|
58
53
|
}
|
|
59
54
|
catch {
|
|
60
|
-
//
|
|
61
|
-
|
|
55
|
+
// An unreadable config degrades to an empty tree; the first save writes a clean menu.
|
|
56
|
+
raw = null;
|
|
57
|
+
}
|
|
58
|
+
if (raw !== null) {
|
|
59
|
+
try {
|
|
60
|
+
tree = extractMenu(parseSiteConfig(raw), config.menuName, maxDepth);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
// A malformed config keeps the same degrade (the nav page failing closed would be worse
|
|
64
|
+
// for the editor), but the swallow names the operator fault in the log.
|
|
65
|
+
log.error('config.invalid', {
|
|
66
|
+
conditionId: 'config.site-config-invalid',
|
|
67
|
+
error: String(err),
|
|
68
|
+
});
|
|
69
|
+
tree = [];
|
|
70
|
+
}
|
|
62
71
|
}
|
|
63
72
|
return {
|
|
64
73
|
menu,
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Escape a string for safe interpolation into HTML text and double-quoted attributes. */
|
|
2
|
-
export declare function escapeHtml(value: string): string;
|
|
3
1
|
/**
|
|
4
2
|
* Render a full self-contained admin page document. The caller supplies trusted inner HTML
|
|
5
3
|
* (eyebrow, heading, copy, CTA); the helper owns the head, the inlined style, the brand tile,
|
|
@@ -2,14 +2,7 @@
|
|
|
2
2
|
// self-contained document with inlined Warm Stone tokens for both colour schemes and the system
|
|
3
3
|
// font stack, served raw before SvelteKit renders. The cairn glyph is the same public-domain
|
|
4
4
|
// Temaki mark the admin chrome uses. See docs/internal/admin-design-system.md.
|
|
5
|
-
|
|
6
|
-
export function escapeHtml(value) {
|
|
7
|
-
return value
|
|
8
|
-
.replace(/&/g, '&')
|
|
9
|
-
.replace(/</g, '<')
|
|
10
|
-
.replace(/>/g, '>')
|
|
11
|
-
.replace(/"/g, '"');
|
|
12
|
-
}
|
|
5
|
+
import { escapeHtml } from '../escape.js';
|
|
13
6
|
// The cairn stone-stack glyph (Temaki, CC0), drawn in currentColor like CairnLogo.svelte.
|
|
14
7
|
const CAIRN_GLYPH = '<path d="M6.28 14C5.56 14 1 13.89 1 12.91C1 11.46 2.16 11.07 3.2 10.81C4.36 10.51 13.18 9.77 ' +
|
|
15
8
|
'13.76 10.07C14.46 10.43 13.52 12.49 12.44 12.77C11.28 13.07 10.21 14 8.48 14C7.05 14 9.69 14 ' +
|
|
@@ -13,22 +13,29 @@ export interface CookieJar {
|
|
|
13
13
|
path: string;
|
|
14
14
|
}): void;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
/** The Cloudflare platform wrapper an event carries; `context` is the legacy alias for `ctx`. */
|
|
17
|
+
export interface PlatformContext<Env> {
|
|
18
|
+
env?: Env;
|
|
19
|
+
ctx?: {
|
|
20
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
21
|
+
};
|
|
22
|
+
context?: {
|
|
23
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** The structural core every engine event type extends, parameterized by the Worker env the
|
|
27
|
+
* surface reads. Each shared field is defined once here; the extensions add only what their
|
|
28
|
+
* surface needs (cookies, params, setHeaders). */
|
|
29
|
+
export interface EventBase<Env> {
|
|
17
30
|
url: URL;
|
|
18
31
|
request: Request;
|
|
19
|
-
cookies: CookieJar;
|
|
20
32
|
locals: {
|
|
21
33
|
editor?: Editor | null;
|
|
22
34
|
};
|
|
23
|
-
platform?:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
28
|
-
context?: {
|
|
29
|
-
waitUntil(promise: Promise<unknown>): void;
|
|
30
|
-
};
|
|
31
|
-
};
|
|
35
|
+
platform?: PlatformContext<Env>;
|
|
36
|
+
}
|
|
37
|
+
export interface RequestContext extends EventBase<AuthEnv> {
|
|
38
|
+
cookies: CookieJar;
|
|
32
39
|
setHeaders(headers: Record<string, string>): void;
|
|
33
40
|
}
|
|
34
41
|
export interface HandleInput {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glw907/cairn-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
"markdown"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"package": "svelte-package && node scripts/build-admin-css.mjs && chmod +x dist/vite/bin.js",
|
|
29
|
+
"package": "svelte-package && node scripts/build-admin-css.mjs && chmod +x dist/vite/bin.js dist/doctor/bin.js",
|
|
30
30
|
"check:package": "npm run package && publint --strict && attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm internal-resolution-error",
|
|
31
31
|
"check:reference": "npm run package && node scripts/reference-coverage.mjs",
|
|
32
|
+
"check:readiness": "npm run package && node scripts/check-readiness.mjs",
|
|
32
33
|
"check:docs": "node scripts/docs-links.mjs",
|
|
33
34
|
"check:prose": "node scripts/check-admin-prose.mjs",
|
|
34
35
|
"prepare": "npm run package",
|
|
@@ -79,10 +80,15 @@
|
|
|
79
80
|
"types": "./dist/vite/index.d.ts",
|
|
80
81
|
"default": "./dist/vite/index.js"
|
|
81
82
|
},
|
|
83
|
+
"./ambient": {
|
|
84
|
+
"types": "./dist/ambient.d.ts",
|
|
85
|
+
"default": "./dist/ambient.js"
|
|
86
|
+
},
|
|
82
87
|
"./package.json": "./package.json"
|
|
83
88
|
},
|
|
84
89
|
"bin": {
|
|
85
|
-
"cairn-manifest": "./dist/vite/bin.js"
|
|
90
|
+
"cairn-manifest": "./dist/vite/bin.js",
|
|
91
|
+
"cairn-doctor": "./dist/doctor/bin.js"
|
|
86
92
|
},
|
|
87
93
|
"files": [
|
|
88
94
|
"dist",
|
|
@@ -90,7 +96,7 @@
|
|
|
90
96
|
"CHANGELOG.md"
|
|
91
97
|
],
|
|
92
98
|
"peerDependencies": {
|
|
93
|
-
"@sveltejs/kit": "^2",
|
|
99
|
+
"@sveltejs/kit": "^2.12",
|
|
94
100
|
"svelte": "^5.0.0"
|
|
95
101
|
},
|
|
96
102
|
"dependencies": {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The one-line App.Locals augmentation a consumer site imports from src/app.d.ts:
|
|
2
|
+
//
|
|
3
|
+
// import '@glw907/cairn-cms/ambient';
|
|
4
|
+
//
|
|
5
|
+
// The guard sets `event.locals.editor`, and this declaration types it, so a site no longer
|
|
6
|
+
// hand-writes the `declare global` block. The field is optional: the engine's own structural
|
|
7
|
+
// event types read it as `editor?: Editor | null`, and a request the guard has not touched
|
|
8
|
+
// carries no editor at all.
|
|
9
|
+
import type { Editor } from './auth/types.js';
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
namespace App {
|
|
13
|
+
interface Locals {
|
|
14
|
+
editor?: Editor | null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export {};
|