@glw907/cairn-cms 0.5.0 → 0.6.0-rc.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/dist/auth/crypto.d.ts +13 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +31 -0
- package/dist/auth/store.d.ts +41 -0
- package/dist/auth/store.d.ts.map +1 -0
- package/dist/auth/store.js +115 -0
- package/dist/auth/types.d.ts +25 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +1 -0
- package/dist/components/AdminLayout.svelte +58 -108
- package/dist/components/AdminLayout.svelte.d.ts +14 -9
- package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
- package/dist/components/ComponentPalette.svelte +50 -0
- package/dist/components/ComponentPalette.svelte.d.ts +16 -0
- package/dist/components/ComponentPalette.svelte.d.ts.map +1 -0
- package/dist/components/ConceptList.svelte +81 -0
- package/dist/components/ConceptList.svelte.d.ts +13 -0
- package/dist/components/ConceptList.svelte.d.ts.map +1 -0
- package/dist/components/ConfirmPage.svelte +23 -20
- package/dist/components/ConfirmPage.svelte.d.ts +6 -0
- package/dist/components/ConfirmPage.svelte.d.ts.map +1 -1
- package/dist/components/EditPage.svelte +160 -103
- package/dist/components/EditPage.svelte.d.ts +17 -7
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/components/LoginPage.svelte +42 -52
- package/dist/components/LoginPage.svelte.d.ts +12 -0
- package/dist/components/LoginPage.svelte.d.ts.map +1 -1
- package/dist/components/ManageEditors.svelte +81 -0
- package/dist/components/ManageEditors.svelte.d.ts +24 -0
- package/dist/components/ManageEditors.svelte.d.ts.map +1 -0
- package/dist/components/MarkdownEditor.svelte +81 -0
- package/dist/components/MarkdownEditor.svelte.d.ts +20 -0
- package/dist/components/MarkdownEditor.svelte.d.ts.map +1 -0
- package/dist/components/NavTree.svelte +138 -0
- package/dist/components/NavTree.svelte.d.ts +17 -0
- package/dist/components/NavTree.svelte.d.ts.map +1 -0
- package/dist/components/cairn-admin.css +42 -0
- package/dist/components/index.d.ts +5 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +7 -4
- package/dist/content/compose.d.ts +7 -0
- package/dist/content/compose.d.ts.map +1 -0
- package/dist/content/compose.js +32 -0
- package/dist/content/concepts.d.ts +17 -0
- package/dist/content/concepts.d.ts.map +1 -0
- package/dist/content/concepts.js +41 -0
- package/dist/content/frontmatter.d.ts +18 -0
- package/dist/content/frontmatter.d.ts.map +1 -0
- package/dist/content/frontmatter.js +58 -0
- package/dist/content/ids.d.ts +17 -0
- package/dist/content/ids.d.ts.map +1 -0
- package/dist/content/ids.js +33 -0
- package/dist/content/types.d.ts +210 -0
- package/dist/content/types.d.ts.map +1 -0
- package/dist/content/types.js +1 -0
- package/dist/content/validate.d.ts +13 -0
- package/dist/content/validate.d.ts.map +1 -0
- package/dist/content/validate.js +45 -0
- package/dist/email.d.ts +25 -12
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +24 -24
- package/dist/env.d.ts +24 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +29 -0
- package/dist/github/credentials.d.ts +12 -0
- package/dist/github/credentials.d.ts.map +1 -0
- package/dist/github/credentials.js +11 -0
- package/dist/github/repo.d.ts +49 -0
- package/dist/github/repo.d.ts.map +1 -0
- package/dist/github/repo.js +123 -0
- package/dist/github/signing.d.ts +17 -0
- package/dist/github/signing.d.ts.map +1 -0
- package/dist/github/signing.js +79 -0
- package/dist/github/types.d.ts +35 -0
- package/dist/github/types.d.ts.map +1 -0
- package/dist/github/types.js +19 -0
- package/dist/index.d.ts +27 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -8
- package/dist/nav/site-config.d.ts +50 -0
- package/dist/nav/site-config.d.ts.map +1 -0
- package/dist/nav/site-config.js +100 -0
- package/dist/render/glyph.d.ts +1 -1
- package/dist/render/glyph.d.ts.map +1 -1
- package/dist/render/index.d.ts +5 -5
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/index.js +6 -6
- package/dist/render/pipeline.d.ts +3 -3
- package/dist/render/pipeline.d.ts.map +1 -1
- package/dist/render/pipeline.js +4 -4
- package/dist/render/registry.d.ts +6 -4
- package/dist/render/registry.d.ts.map +1 -1
- package/dist/render/registry.js +8 -6
- package/dist/render/rehype-dispatch.d.ts +1 -1
- package/dist/render/rehype-dispatch.d.ts.map +1 -1
- package/dist/render/remark-directives.d.ts +1 -1
- package/dist/render/remark-directives.d.ts.map +1 -1
- package/dist/render/sanitize.d.ts +8 -0
- package/dist/render/sanitize.d.ts.map +1 -0
- package/dist/render/sanitize.js +26 -0
- package/dist/sveltekit/auth-routes.d.ts +23 -0
- package/dist/sveltekit/auth-routes.d.ts.map +1 -0
- package/dist/sveltekit/auth-routes.js +85 -0
- package/dist/sveltekit/content-routes.d.ts +80 -0
- package/dist/sveltekit/content-routes.d.ts.map +1 -0
- package/dist/sveltekit/content-routes.js +183 -0
- package/dist/sveltekit/editors-routes.d.ts +24 -0
- package/dist/sveltekit/editors-routes.d.ts.map +1 -0
- package/dist/sveltekit/editors-routes.js +73 -0
- package/dist/sveltekit/guard.d.ts +9 -0
- package/dist/sveltekit/guard.d.ts.map +1 -0
- package/dist/sveltekit/guard.js +43 -0
- package/dist/sveltekit/health.d.ts +19 -0
- package/dist/sveltekit/health.d.ts.map +1 -0
- package/dist/sveltekit/health.js +12 -0
- package/dist/sveltekit/index.d.ts +9 -83
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +8 -149
- package/dist/sveltekit/nav-routes.d.ts +30 -0
- package/dist/sveltekit/nav-routes.d.ts.map +1 -0
- package/dist/sveltekit/nav-routes.js +103 -0
- package/dist/sveltekit/types.d.ts +32 -0
- package/dist/sveltekit/types.d.ts.map +1 -0
- package/dist/sveltekit/types.js +1 -0
- package/package.json +38 -58
- package/src/lib/auth/crypto.ts +37 -0
- package/src/lib/auth/store.ts +158 -0
- package/src/lib/auth/types.ts +27 -0
- package/src/lib/components/AdminLayout.svelte +58 -108
- package/src/lib/components/ComponentPalette.svelte +50 -0
- package/src/lib/components/ConceptList.svelte +81 -0
- package/src/lib/components/ConfirmPage.svelte +23 -20
- package/src/lib/components/EditPage.svelte +160 -103
- package/src/lib/components/LoginPage.svelte +42 -52
- package/src/lib/components/ManageEditors.svelte +81 -0
- package/src/lib/components/MarkdownEditor.svelte +81 -0
- package/src/lib/components/NavTree.svelte +138 -0
- package/src/lib/components/cairn-admin.css +42 -0
- package/src/lib/components/index.ts +7 -4
- package/src/lib/content/compose.ts +39 -0
- package/src/lib/content/concepts.ts +57 -0
- package/src/lib/content/frontmatter.ts +71 -0
- package/src/lib/content/ids.ts +38 -0
- package/src/lib/content/types.ts +235 -0
- package/src/lib/content/validate.ts +51 -0
- package/src/lib/email.ts +52 -38
- package/src/lib/env.ts +32 -0
- package/src/lib/github/credentials.ts +27 -0
- package/src/lib/github/repo.ts +138 -0
- package/src/lib/github/signing.ts +97 -0
- package/src/lib/github/types.ts +46 -0
- package/src/lib/index.ts +86 -8
- package/src/lib/nav/site-config.ts +124 -0
- package/src/lib/render/glyph.ts +6 -6
- package/src/lib/render/index.ts +6 -6
- package/src/lib/render/pipeline.ts +22 -22
- package/src/lib/render/registry.ts +33 -26
- package/src/lib/render/rehype-dispatch.ts +47 -47
- package/src/lib/render/remark-directives.ts +46 -46
- package/src/lib/render/sanitize.ts +27 -0
- package/src/lib/sveltekit/auth-routes.ts +107 -0
- package/src/lib/sveltekit/content-routes.ts +261 -0
- package/src/lib/sveltekit/editors-routes.ts +82 -0
- package/src/lib/sveltekit/guard.ts +47 -0
- package/src/lib/sveltekit/health.ts +24 -0
- package/src/lib/sveltekit/index.ts +19 -235
- package/src/lib/sveltekit/nav-routes.ts +139 -0
- package/src/lib/sveltekit/types.ts +33 -0
- package/dist/adapter.d.ts +0 -69
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js +0 -30
- package/dist/auth/admins.d.ts +0 -33
- package/dist/auth/admins.d.ts.map +0 -1
- package/dist/auth/admins.js +0 -90
- package/dist/auth/config.d.ts +0 -2097
- package/dist/auth/config.d.ts.map +0 -1
- package/dist/auth/config.js +0 -78
- package/dist/auth/guard.d.ts +0 -34
- package/dist/auth/guard.d.ts.map +0 -1
- package/dist/auth/guard.js +0 -47
- package/dist/auth/index.d.ts +0 -4
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -6
- package/dist/auth/schema.d.ts +0 -750
- package/dist/auth/schema.d.ts.map +0 -1
- package/dist/auth/schema.js +0 -93
- package/dist/carta.d.ts +0 -39
- package/dist/carta.d.ts.map +0 -1
- package/dist/carta.js +0 -30
- package/dist/components/AdminList.svelte +0 -33
- package/dist/components/AdminList.svelte.d.ts +0 -10
- package/dist/components/AdminList.svelte.d.ts.map +0 -1
- package/dist/components/ManageAdmins.svelte +0 -84
- package/dist/components/ManageAdmins.svelte.d.ts +0 -10
- package/dist/components/ManageAdmins.svelte.d.ts.map +0 -1
- package/dist/content.d.ts +0 -3
- package/dist/content.d.ts.map +0 -1
- package/dist/content.js +0 -10
- package/dist/github.d.ts +0 -72
- package/dist/github.d.ts.map +0 -1
- package/dist/github.js +0 -171
- package/dist/utils.d.ts +0 -3
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -11
- package/src/lib/adapter.ts +0 -119
- package/src/lib/auth/admins.ts +0 -106
- package/src/lib/auth/config.ts +0 -108
- package/src/lib/auth/guard.ts +0 -60
- package/src/lib/auth/index.ts +0 -6
- package/src/lib/auth/schema.ts +0 -112
- package/src/lib/carta.ts +0 -59
- package/src/lib/components/AdminList.svelte +0 -33
- package/src/lib/components/ManageAdmins.svelte +0 -84
- package/src/lib/content.ts +0 -11
- package/src/lib/github.ts +0 -220
- package/src/lib/utils.ts +0 -12
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { ComponentRegistry } from '../render/registry.js';
|
|
2
|
+
/** Common to every frontmatter field: the frontmatter key, the form label, and whether it is required. */
|
|
3
|
+
interface FieldBase {
|
|
4
|
+
/** Frontmatter key and form input name. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Form label. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** A required field fails validation when empty (spec §7.4). */
|
|
9
|
+
required?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** A single-line text input. */
|
|
12
|
+
export interface TextField extends FieldBase {
|
|
13
|
+
type: 'text';
|
|
14
|
+
}
|
|
15
|
+
/** A multi-line text input. */
|
|
16
|
+
export interface TextareaField extends FieldBase {
|
|
17
|
+
type: 'textarea';
|
|
18
|
+
/** Visible rows; the editor picks a default when omitted. */
|
|
19
|
+
rows?: number;
|
|
20
|
+
}
|
|
21
|
+
/** A `YYYY-MM-DD` date input. */
|
|
22
|
+
export interface DateField extends FieldBase {
|
|
23
|
+
type: 'date';
|
|
24
|
+
}
|
|
25
|
+
/** A checkbox; absent means false. */
|
|
26
|
+
export interface BooleanField extends FieldBase {
|
|
27
|
+
type: 'boolean';
|
|
28
|
+
}
|
|
29
|
+
/** A closed-vocabulary tag set, rendered as checkboxes (ecnordic). */
|
|
30
|
+
export interface TagsField extends FieldBase {
|
|
31
|
+
type: 'tags';
|
|
32
|
+
/** The controlled vocabulary. */
|
|
33
|
+
options: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
/** Free-form tags, edited as one comma-separated input (907). */
|
|
36
|
+
export interface FreeTagsField extends FieldBase {
|
|
37
|
+
type: 'freetags';
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The discriminated union the per-concept frontmatter form is generated from. Adding a
|
|
42
|
+
* field type is one variant here plus one decode arm in `frontmatterFromForm` and one in
|
|
43
|
+
* `validateFields`.
|
|
44
|
+
*/
|
|
45
|
+
export type FrontmatterField = TextField | TextareaField | DateField | BooleanField | TagsField | FreeTagsField;
|
|
46
|
+
/**
|
|
47
|
+
* A validator's verdict. On success it carries the normalized frontmatter to commit; on
|
|
48
|
+
* failure it carries field-keyed error messages (the empty key is a form-level error).
|
|
49
|
+
* Invalid input bounces to the form and never reaches git (spec §7.4).
|
|
50
|
+
*/
|
|
51
|
+
export type ValidationResult = {
|
|
52
|
+
ok: true;
|
|
53
|
+
data: Record<string, unknown>;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
errors: Record<string, string>;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Per-site configuration for one content concept (spec §8). Concept-fixed behavior such as
|
|
60
|
+
* routability is not here; it lives in the engine's routing table (`CONCEPT_ROUTING`).
|
|
61
|
+
*/
|
|
62
|
+
export interface ConceptConfig {
|
|
63
|
+
/** Repo-relative content directory, e.g. "src/content/posts". */
|
|
64
|
+
dir: string;
|
|
65
|
+
/** Sidebar label; defaults from the concept id when omitted. */
|
|
66
|
+
label?: string;
|
|
67
|
+
/** Drives the per-concept frontmatter form, in order. */
|
|
68
|
+
fields: FrontmatterField[];
|
|
69
|
+
/** Validate submitted frontmatter before any commit. */
|
|
70
|
+
validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
|
|
71
|
+
}
|
|
72
|
+
/** The GitHub App backend a site reads from and commits to (spec §8). Plain data the GitHub engine (Plan 03) consumes. */
|
|
73
|
+
export interface BackendConfig {
|
|
74
|
+
owner: string;
|
|
75
|
+
repo: string;
|
|
76
|
+
/** Commit target, e.g. "main". */
|
|
77
|
+
branch: string;
|
|
78
|
+
appId: string;
|
|
79
|
+
installationId: string;
|
|
80
|
+
}
|
|
81
|
+
/** Magic-link sender identity for Cloudflare Email Sending. */
|
|
82
|
+
export interface SenderConfig {
|
|
83
|
+
from: string;
|
|
84
|
+
replyTo?: string;
|
|
85
|
+
}
|
|
86
|
+
/** A git-committed YAML menu this site's nav editor manages (Plan 06). */
|
|
87
|
+
export interface NavMenuConfig {
|
|
88
|
+
/** Repo-relative path to the site-config YAML, e.g. "src/lib/site.config.yaml". */
|
|
89
|
+
configPath: string;
|
|
90
|
+
/** Key within the file's menus map, e.g. "primary". */
|
|
91
|
+
menuName: string;
|
|
92
|
+
/** Sidebar label for the menu. */
|
|
93
|
+
label: string;
|
|
94
|
+
/** Max nesting depth allowed in the editor; defaults to 2. */
|
|
95
|
+
maxDepth?: number;
|
|
96
|
+
}
|
|
97
|
+
/** Reserved asset slot (seam 4). Typed and unused in the rebuild; R7/R9 read it later with no contract change. */
|
|
98
|
+
export interface AssetConfig {
|
|
99
|
+
/** Repo-relative asset roots, e.g. ["static/images"]. */
|
|
100
|
+
roots: string[];
|
|
101
|
+
/** Public URL base, e.g. "/images". */
|
|
102
|
+
publicBase: string;
|
|
103
|
+
}
|
|
104
|
+
/** The single seam the engine consumes. A site implements this at `src/lib/cairn.config.ts`. */
|
|
105
|
+
export interface CairnAdapter {
|
|
106
|
+
siteName: string;
|
|
107
|
+
/**
|
|
108
|
+
* Which content concepts this site enables. A future `fragments?` key attaches here with
|
|
109
|
+
* no reshape of the contract (seam 1). A site never has two of the same concept.
|
|
110
|
+
*/
|
|
111
|
+
content: {
|
|
112
|
+
posts?: ConceptConfig;
|
|
113
|
+
pages?: ConceptConfig;
|
|
114
|
+
};
|
|
115
|
+
backend: BackendConfig;
|
|
116
|
+
sender: SenderConfig;
|
|
117
|
+
/** Design-accurate preview: the same render pipeline the site ships. */
|
|
118
|
+
renderPreview(md: string): string | Promise<string>;
|
|
119
|
+
/** Directive component registry; the renderer and the future palette derive from it (seam 3). */
|
|
120
|
+
registry?: ComponentRegistry;
|
|
121
|
+
navMenu?: NavMenuConfig;
|
|
122
|
+
assets?: AssetConfig;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Concept-fixed routing for a normalized concept (spec §7.2). Posts are dated feed entries;
|
|
126
|
+
* pages are plain navigable structure. Not in adapter config.
|
|
127
|
+
*/
|
|
128
|
+
export interface RoutingRule {
|
|
129
|
+
/** Routable as a standalone URL. A future Fragments concept is embedded, not routable. */
|
|
130
|
+
routable: boolean;
|
|
131
|
+
/** Carries a date (posts). */
|
|
132
|
+
dated: boolean;
|
|
133
|
+
/** Appears in feeds and the sitemap (posts). */
|
|
134
|
+
inFeeds: boolean;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The engine-internal, uniform view of one concept after normalization (seam 1). The admin
|
|
138
|
+
* nav, the list views, and the editor all read this, never the raw config.
|
|
139
|
+
*/
|
|
140
|
+
export interface ConceptDescriptor {
|
|
141
|
+
/** Concept id, the key under `content`, e.g. "posts". */
|
|
142
|
+
id: string;
|
|
143
|
+
label: string;
|
|
144
|
+
dir: string;
|
|
145
|
+
routing: RoutingRule;
|
|
146
|
+
fields: FrontmatterField[];
|
|
147
|
+
validate(frontmatter: Record<string, unknown>, body: string): ValidationResult;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A site-defined admin screen contributed by an extension (Mode 2). It gains a sidebar entry, the
|
|
151
|
+
* `/admin` guard, and the session, and may commit through the same GitHub pipeline. The dispatch
|
|
152
|
+
* route is built in Plan 09; the `load`/`actions`/`component` members are typed loosely here and
|
|
153
|
+
* tightened when the machinery lands.
|
|
154
|
+
*/
|
|
155
|
+
export interface AdminPanel {
|
|
156
|
+
/** Routes under `/admin/<id>`; also the sidebar key. */
|
|
157
|
+
id: string;
|
|
158
|
+
/** Sidebar label. */
|
|
159
|
+
label: string;
|
|
160
|
+
/** Owner-gated, like editor management. */
|
|
161
|
+
owner?: boolean;
|
|
162
|
+
/** Server load, behind the guard. Typed in Plan 09. */
|
|
163
|
+
load?: (event: unknown) => unknown;
|
|
164
|
+
/** Named form actions, which may use the commit pipeline. Typed in Plan 09. */
|
|
165
|
+
actions?: Record<string, (event: unknown) => Promise<unknown>>;
|
|
166
|
+
/** The panel UI, rendered inside the admin shell. Typed as a component in Plan 09. */
|
|
167
|
+
component: unknown;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A custom frontmatter field type contributed by an extension (Mode 2): a renderer plus a validator
|
|
171
|
+
* dispatched alongside the built-in field union. The renderer and validator are typed in Plan 09
|
|
172
|
+
* when the form dispatch becomes a registry; the `type` key reserves the discriminator now.
|
|
173
|
+
*/
|
|
174
|
+
export interface FieldTypeDef {
|
|
175
|
+
/** The field-type discriminator, e.g. "color". */
|
|
176
|
+
type: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* A future build-time extension (seam 2). It folds in the same way the adapter does and
|
|
180
|
+
* contributes the same kinds of things. Reserved and unused in the rebuild; the shape is
|
|
181
|
+
* fixed now so the extension contract is additive later.
|
|
182
|
+
*/
|
|
183
|
+
export interface CairnExtension {
|
|
184
|
+
/** Additional concepts, merged after the adapter's. */
|
|
185
|
+
content?: Record<string, ConceptConfig>;
|
|
186
|
+
/** Site-defined admin panels (Mode 2). Carried onto the runtime now; dispatched in Plan 09. */
|
|
187
|
+
adminPanels?: AdminPanel[];
|
|
188
|
+
/** Custom field types (Mode 2). Carried onto the runtime now; dispatched in Plan 09. */
|
|
189
|
+
fieldTypes?: FieldTypeDef[];
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The composed runtime the engine serves from (seam 2 output). The single aggregation point
|
|
193
|
+
* (`composeRuntime`) folds the adapter and any extensions into this shape.
|
|
194
|
+
*/
|
|
195
|
+
export interface CairnRuntime {
|
|
196
|
+
siteName: string;
|
|
197
|
+
concepts: ConceptDescriptor[];
|
|
198
|
+
backend: BackendConfig;
|
|
199
|
+
sender: SenderConfig;
|
|
200
|
+
renderPreview(md: string): string | Promise<string>;
|
|
201
|
+
registry?: ComponentRegistry;
|
|
202
|
+
navMenu?: NavMenuConfig;
|
|
203
|
+
assets?: AssetConfig;
|
|
204
|
+
/** Admin panels contributed by extensions (Mode 2). Empty until Plan 09 wires the dispatch route. */
|
|
205
|
+
adminPanels?: AdminPanel[];
|
|
206
|
+
/** Field types contributed by extensions (Mode 2). Empty until Plan 09 wires the form dispatch. */
|
|
207
|
+
fieldTypes?: FieldTypeDef[];
|
|
208
|
+
}
|
|
209
|
+
export {};
|
|
210
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/content/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FrontmatterField, ValidationResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate raw frontmatter against a field list. Required text and date fields must be
|
|
4
|
+
* non-empty; required tag fields must be non-empty lists. Booleans coerce to `true`/`false`
|
|
5
|
+
* and tag fields to string arrays. Returns the normalized data, or field-keyed errors when
|
|
6
|
+
* any required field is empty.
|
|
7
|
+
*
|
|
8
|
+
* Frontmatter may arrive from the edit form (all string values) or from `parseMarkdown`,
|
|
9
|
+
* where gray-matter turns an unquoted YAML date into a JS `Date`. The `date` case coerces a
|
|
10
|
+
* `Date` to `YYYY-MM-DD` so a valid parsed date is not mistaken for an empty one.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateFields(fields: FrontmatterField[], frontmatter: Record<string, unknown>): ValidationResult;
|
|
13
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/lib/content/validate.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGrE;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EAAE,EAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,gBAAgB,CA8BlB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { dateInputValue } from './frontmatter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate raw frontmatter against a field list. Required text and date fields must be
|
|
4
|
+
* non-empty; required tag fields must be non-empty lists. Booleans coerce to `true`/`false`
|
|
5
|
+
* and tag fields to string arrays. Returns the normalized data, or field-keyed errors when
|
|
6
|
+
* any required field is empty.
|
|
7
|
+
*
|
|
8
|
+
* Frontmatter may arrive from the edit form (all string values) or from `parseMarkdown`,
|
|
9
|
+
* where gray-matter turns an unquoted YAML date into a JS `Date`. The `date` case coerces a
|
|
10
|
+
* `Date` to `YYYY-MM-DD` so a valid parsed date is not mistaken for an empty one.
|
|
11
|
+
*/
|
|
12
|
+
export function validateFields(fields, frontmatter) {
|
|
13
|
+
const data = {};
|
|
14
|
+
const errors = {};
|
|
15
|
+
for (const field of fields) {
|
|
16
|
+
const value = frontmatter[field.name];
|
|
17
|
+
switch (field.type) {
|
|
18
|
+
case 'boolean':
|
|
19
|
+
data[field.name] = value === true;
|
|
20
|
+
break;
|
|
21
|
+
case 'tags':
|
|
22
|
+
case 'freetags': {
|
|
23
|
+
const list = Array.isArray(value) ? value.map(String) : [];
|
|
24
|
+
if (field.required && list.length === 0)
|
|
25
|
+
errors[field.name] = `${field.label} is required`;
|
|
26
|
+
data[field.name] = list;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'date': {
|
|
30
|
+
const text = value instanceof Date ? dateInputValue(value) : typeof value === 'string' ? value.trim() : '';
|
|
31
|
+
if (field.required && text === '')
|
|
32
|
+
errors[field.name] = `${field.label} is required`;
|
|
33
|
+
data[field.name] = text;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
default: {
|
|
37
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
38
|
+
if (field.required && text === '')
|
|
39
|
+
errors[field.name] = `${field.label} is required`;
|
|
40
|
+
data[field.name] = text;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, data };
|
|
45
|
+
}
|
package/dist/email.d.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
messageId: string;
|
|
11
|
-
}>;
|
|
1
|
+
import type { AuthEnv } from './auth/types.js';
|
|
2
|
+
export type { AuthEnv };
|
|
3
|
+
/** The message a built magic-link email carries. */
|
|
4
|
+
export interface MagicLinkMessage {
|
|
5
|
+
to: string;
|
|
6
|
+
from: string;
|
|
7
|
+
subject: string;
|
|
8
|
+
html: string;
|
|
9
|
+
text: string;
|
|
12
10
|
}
|
|
13
|
-
|
|
11
|
+
/** Per-site identity for the magic-link email, sourced from the adapter. */
|
|
12
|
+
export interface AuthBranding {
|
|
13
|
+
siteName: string;
|
|
14
|
+
from: string;
|
|
15
|
+
replyTo?: string;
|
|
16
|
+
}
|
|
17
|
+
/** The injected send. Production uses `cloudflareSend`; tests pass a sink. */
|
|
18
|
+
export type SendMagicLink = (env: AuthEnv, message: MagicLinkMessage) => Promise<void>;
|
|
19
|
+
/** Build the confirmation email. The link is the only action; the copy stays plain. */
|
|
20
|
+
export declare function buildMagicLinkMessage(input: {
|
|
21
|
+
to: string;
|
|
22
|
+
branding: AuthBranding;
|
|
23
|
+
link: string;
|
|
24
|
+
}): MagicLinkMessage;
|
|
25
|
+
/** The production send: Cloudflare Email Sending through the EMAIL binding. */
|
|
26
|
+
export declare const cloudflareSend: SendMagicLink;
|
|
14
27
|
//# sourceMappingURL=email.d.ts.map
|
package/dist/email.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/lib/email.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/lib/email.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,YAAY,EAAE,OAAO,EAAE,CAAC;AAExB,oDAAoD;AACpD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAYvF,uFAAuF;AACvF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,gBAAgB,CAQnB;AAED,+EAA+E;AAC/E,eAAO,MAAM,cAAc,EAAE,aAG5B,CAAC"}
|
package/dist/email.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const expiry = "This link expires in 10 minutes and works only once. If you didn't request it, ignore this email.";
|
|
10
|
-
try {
|
|
11
|
-
await sender.send({
|
|
12
|
-
to,
|
|
13
|
-
from,
|
|
14
|
-
subject: `Your ${siteName} sign-in link`,
|
|
15
|
-
text: `Sign in to ${siteName}:\n\n${link}\n\n${expiry}`,
|
|
16
|
-
html: `<p>Sign in to ${siteName}:</p><p><a href="${link}">Confirm sign-in</a></p><p style="color:#666;font-size:0.9em">${expiry}</p>`,
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
// H6: Email Sending is beta + the sole auth channel. Surface + audit; a Resend fallback
|
|
21
|
-
// can slot in behind this same signature if Sending proves unreliable.
|
|
22
|
-
console.error(`magic-link email send failed for ${to}:`, err);
|
|
23
|
-
throw err;
|
|
24
|
-
}
|
|
1
|
+
/** Escape the five HTML-significant characters. */
|
|
2
|
+
function escapeHtml(value) {
|
|
3
|
+
return value
|
|
4
|
+
.replaceAll('&', '&')
|
|
5
|
+
.replaceAll('<', '<')
|
|
6
|
+
.replaceAll('>', '>')
|
|
7
|
+
.replaceAll('"', '"')
|
|
8
|
+
.replaceAll("'", ''');
|
|
25
9
|
}
|
|
10
|
+
/** Build the confirmation email. The link is the only action; the copy stays plain. */
|
|
11
|
+
export function buildMagicLinkMessage(input) {
|
|
12
|
+
const { to, branding, link } = input;
|
|
13
|
+
const subject = `Sign in to ${branding.siteName}`;
|
|
14
|
+
const text = `Open this link to sign in to ${branding.siteName}:\n\n${link}\n\nThe link expires in 10 minutes. If you did not request it, ignore this email.`;
|
|
15
|
+
// `link` is engine-built and url-safe; `siteName` is site config, so escape it for HTML.
|
|
16
|
+
const name = escapeHtml(branding.siteName);
|
|
17
|
+
const html = `<p>Open this link to sign in to ${name}:</p><p><a href="${link}">Sign in</a></p><p>The link expires in 10 minutes. If you did not request it, ignore this email.</p>`;
|
|
18
|
+
return { to, from: branding.from, subject, html, text };
|
|
19
|
+
}
|
|
20
|
+
/** The production send: Cloudflare Email Sending through the EMAIL binding. */
|
|
21
|
+
export const cloudflareSend = async (env, message) => {
|
|
22
|
+
if (!env.EMAIL)
|
|
23
|
+
throw new Error('EMAIL binding is not configured');
|
|
24
|
+
await env.EMAIL.send(message);
|
|
25
|
+
};
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the site's public origin from configuration.
|
|
4
|
+
*
|
|
5
|
+
* The origin is always config-derived, never read from a request header, so a
|
|
6
|
+
* forged Host header cannot redirect a magic link (spec 7.1, risk H3).
|
|
7
|
+
*
|
|
8
|
+
* @throws Error when `PUBLIC_ORIGIN` is unset or empty.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requireOrigin(env: {
|
|
11
|
+
PUBLIC_ORIGIN?: string;
|
|
12
|
+
}): string;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the `AUTH_DB` binding, or throws a clear error when a site has not wired it.
|
|
15
|
+
*
|
|
16
|
+
* The handlers read D1 off `event.platform.env`; without this a misconfigured binding
|
|
17
|
+
* surfaces as a raw `TypeError` deep in a store call. This gives the failure a name.
|
|
18
|
+
*
|
|
19
|
+
* @throws Error when `AUTH_DB` is missing.
|
|
20
|
+
*/
|
|
21
|
+
export declare function requireDb(env: {
|
|
22
|
+
AUTH_DB?: D1Database;
|
|
23
|
+
}): D1Database;
|
|
24
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/lib/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMrE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,UAAU,CAKnE"}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the site's public origin from configuration.
|
|
3
|
+
*
|
|
4
|
+
* The origin is always config-derived, never read from a request header, so a
|
|
5
|
+
* forged Host header cannot redirect a magic link (spec 7.1, risk H3).
|
|
6
|
+
*
|
|
7
|
+
* @throws Error when `PUBLIC_ORIGIN` is unset or empty.
|
|
8
|
+
*/
|
|
9
|
+
export function requireOrigin(env) {
|
|
10
|
+
const origin = env.PUBLIC_ORIGIN;
|
|
11
|
+
if (!origin) {
|
|
12
|
+
throw new Error('PUBLIC_ORIGIN is not configured');
|
|
13
|
+
}
|
|
14
|
+
return origin;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns the `AUTH_DB` binding, or throws a clear error when a site has not wired it.
|
|
18
|
+
*
|
|
19
|
+
* The handlers read D1 off `event.platform.env`; without this a misconfigured binding
|
|
20
|
+
* surfaces as a raw `TypeError` deep in a store call. This gives the failure a name.
|
|
21
|
+
*
|
|
22
|
+
* @throws Error when `AUTH_DB` is missing.
|
|
23
|
+
*/
|
|
24
|
+
export function requireDb(env) {
|
|
25
|
+
if (!env.AUTH_DB) {
|
|
26
|
+
throw new Error('AUTH_DB binding is not configured');
|
|
27
|
+
}
|
|
28
|
+
return env.AUTH_DB;
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BackendConfig } from '../content/types.js';
|
|
2
|
+
import type { AppCredentials } from './types.js';
|
|
3
|
+
/** The Worker secret holding the GitHub App private key: base64 of the PEM, single line. */
|
|
4
|
+
export interface GithubKeyEnv {
|
|
5
|
+
GITHUB_APP_PRIVATE_KEY_B64?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Assemble the `AppCredentials` the signer needs from the adapter's `backend` (app id,
|
|
9
|
+
* installation) and the Worker's private-key secret. Throws when the secret is unset.
|
|
10
|
+
*/
|
|
11
|
+
export declare function appCredentials(backend: Pick<BackendConfig, 'appId' | 'installationId'>, env: GithubKeyEnv): AppCredentials;
|
|
12
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/lib/github/credentials.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,gBAAgB,CAAC,EACxD,GAAG,EAAE,YAAY,GAChB,cAAc,CAMhB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble the `AppCredentials` the signer needs from the adapter's `backend` (app id,
|
|
3
|
+
* installation) and the Worker's private-key secret. Throws when the secret is unset.
|
|
4
|
+
*/
|
|
5
|
+
export function appCredentials(backend, env) {
|
|
6
|
+
const privateKeyB64 = env.GITHUB_APP_PRIVATE_KEY_B64;
|
|
7
|
+
if (!privateKeyB64) {
|
|
8
|
+
throw new Error('GITHUB_APP_PRIVATE_KEY_B64 is not configured');
|
|
9
|
+
}
|
|
10
|
+
return { appId: backend.appId, installationId: backend.installationId, privateKeyB64 };
|
|
11
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CommitAuthor, RepoFile, RepoRef } from './types.js';
|
|
2
|
+
/** The recursive Git Trees API URL for the configured branch. */
|
|
3
|
+
export declare function treeUrl(repo: RepoRef): string;
|
|
4
|
+
/** A Git Trees API entry: a full repo path and whether it is a blob or a subtree. */
|
|
5
|
+
interface TreeEntry {
|
|
6
|
+
path: string;
|
|
7
|
+
type: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Markdown files directly in `dir`, newest id first. Tree entries carry full repo paths, so
|
|
11
|
+
* the directory prefix is stripped to a basename before deriving the id. Nested files, non
|
|
12
|
+
* markdown, and other directories are dropped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function markdownFilesIn(dir: string, tree: TreeEntry[]): RepoFile[];
|
|
15
|
+
/**
|
|
16
|
+
* List the markdown files in a concept directory through the Git Trees API. A truncated tree
|
|
17
|
+
* (GitHub caps the recursive listing near 100,000 entries) throws rather than returning a
|
|
18
|
+
* silent partial list; a concept directory sits far below that, and sharding is deferred
|
|
19
|
+
* until one approaches it (spec §7.3).
|
|
20
|
+
*/
|
|
21
|
+
export declare function listMarkdown(repo: RepoRef, dir: string, token?: string): Promise<RepoFile[]>;
|
|
22
|
+
/** The contents-API URL for a repo path, pinned to the configured branch. */
|
|
23
|
+
export declare function contentsUrl(repo: RepoRef, path: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Fetch a file's raw markdown, or null if it does not exist. The contents API caps a raw
|
|
26
|
+
* read at 1 MB; a concept's files sit far below that, and sharding is deferred until one
|
|
27
|
+
* approaches it (spec §7.3).
|
|
28
|
+
*/
|
|
29
|
+
export declare function readRaw(repo: RepoRef, path: string, token?: string): Promise<string | null>;
|
|
30
|
+
/** The current blob sha for a path, or null if the file does not yet exist. */
|
|
31
|
+
export declare function fileSha(repo: RepoRef, path: string, token: string): Promise<string | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Commit `content` to `path` on the configured branch through the contents API. The author is
|
|
34
|
+
* the editor; the committer is omitted, so GitHub attributes it to the App (`cairn-cms[bot]`).
|
|
35
|
+
* Updates the file in place when it exists (passing its sha), creates it otherwise. Returns the
|
|
36
|
+
* commit sha. A stale-sha 409 (someone committed in between) becomes a `CommitConflictError`,
|
|
37
|
+
* so the save fails safe: re-fetch and ask the editor to reapply, never a merge.
|
|
38
|
+
*
|
|
39
|
+
* Caller preconditions this layer cannot enforce, and the save action (Plan 05) must:
|
|
40
|
+
* `path` is confined to the concept's configured directory (the App token can write anywhere
|
|
41
|
+
* in the repo, so an unvalidated path could overwrite CI config or source), and `author` is
|
|
42
|
+
* derived from the verified server-side session, never from request input.
|
|
43
|
+
*/
|
|
44
|
+
export declare function commitFile(repo: RepoRef, path: string, content: string, opts: {
|
|
45
|
+
message: string;
|
|
46
|
+
author: CommitAuthor;
|
|
47
|
+
}, token: string): Promise<string>;
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=repo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/lib/github/repo.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAelE,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAE7C;AAED,qFAAqF;AACrF,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAOD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE,CAW1E;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAMlG;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKjG;AAOD,+EAA+E;AAC/E,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKhG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,EAC/C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAiBjB"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/lib/github/repo.ts
|
|
2
|
+
// cairn-cms: repo reads and the commit, over the GitHub REST API. Listing a concept
|
|
3
|
+
// directory uses the Git Trees API (the contents API silently truncates at 1,000 entries,
|
|
4
|
+
// spec §7.3); a single-file read uses the contents API. An optional token lifts reads to
|
|
5
|
+
// the authenticated rate limit and unlocks private repos; ecnordic's repo is public, 907's
|
|
6
|
+
// is not.
|
|
7
|
+
import { idFromFilename } from '../content/ids.js';
|
|
8
|
+
import { CommitConflictError } from './types.js';
|
|
9
|
+
const API = 'https://api.github.com';
|
|
10
|
+
/** Standard GitHub API headers, with a bearer token when one is supplied. */
|
|
11
|
+
function ghHeaders(accept, token) {
|
|
12
|
+
const headers = {
|
|
13
|
+
Accept: accept,
|
|
14
|
+
'User-Agent': 'cairn-cms',
|
|
15
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
16
|
+
};
|
|
17
|
+
if (token)
|
|
18
|
+
headers.Authorization = `Bearer ${token}`;
|
|
19
|
+
return headers;
|
|
20
|
+
}
|
|
21
|
+
/** The recursive Git Trees API URL for the configured branch. */
|
|
22
|
+
export function treeUrl(repo) {
|
|
23
|
+
return `${API}/repos/${repo.owner}/${repo.repo}/git/trees/${encodeURIComponent(repo.branch)}?recursive=1`;
|
|
24
|
+
}
|
|
25
|
+
/** The basename of a repo path: the segment after the last slash. */
|
|
26
|
+
function basename(path) {
|
|
27
|
+
return path.slice(path.lastIndexOf('/') + 1);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Markdown files directly in `dir`, newest id first. Tree entries carry full repo paths, so
|
|
31
|
+
* the directory prefix is stripped to a basename before deriving the id. Nested files, non
|
|
32
|
+
* markdown, and other directories are dropped.
|
|
33
|
+
*/
|
|
34
|
+
export function markdownFilesIn(dir, tree) {
|
|
35
|
+
const clean = dir.replace(/^\/+|\/+$/g, '');
|
|
36
|
+
const prefix = `${clean}/`;
|
|
37
|
+
return tree
|
|
38
|
+
.filter((entry) => entry.type === 'blob' && entry.path.startsWith(prefix) && entry.path.endsWith('.md'))
|
|
39
|
+
.filter((entry) => !entry.path.slice(prefix.length).includes('/'))
|
|
40
|
+
.map((entry) => {
|
|
41
|
+
const name = basename(entry.path);
|
|
42
|
+
return { id: idFromFilename(name), name, path: entry.path };
|
|
43
|
+
})
|
|
44
|
+
.sort((a, b) => b.id.localeCompare(a.id));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* List the markdown files in a concept directory through the Git Trees API. A truncated tree
|
|
48
|
+
* (GitHub caps the recursive listing near 100,000 entries) throws rather than returning a
|
|
49
|
+
* silent partial list; a concept directory sits far below that, and sharding is deferred
|
|
50
|
+
* until one approaches it (spec §7.3).
|
|
51
|
+
*/
|
|
52
|
+
export async function listMarkdown(repo, dir, token) {
|
|
53
|
+
const res = await fetch(treeUrl(repo), { headers: ghHeaders('application/vnd.github+json', token) });
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw new Error(`GitHub tree ${repo.branch} failed: ${res.status}`);
|
|
56
|
+
const body = (await res.json());
|
|
57
|
+
if (body.truncated)
|
|
58
|
+
throw new Error(`GitHub tree ${repo.branch} is truncated; ${dir} exceeds the listing cap`);
|
|
59
|
+
return markdownFilesIn(dir, body.tree);
|
|
60
|
+
}
|
|
61
|
+
/** The contents-API URL for a repo path, pinned to the configured branch. */
|
|
62
|
+
export function contentsUrl(repo, path) {
|
|
63
|
+
const clean = path.replace(/^\/+|\/+$/g, '');
|
|
64
|
+
return `${API}/repos/${repo.owner}/${repo.repo}/contents/${clean}?ref=${encodeURIComponent(repo.branch)}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Fetch a file's raw markdown, or null if it does not exist. The contents API caps a raw
|
|
68
|
+
* read at 1 MB; a concept's files sit far below that, and sharding is deferred until one
|
|
69
|
+
* approaches it (spec §7.3).
|
|
70
|
+
*/
|
|
71
|
+
export async function readRaw(repo, path, token) {
|
|
72
|
+
const res = await fetch(contentsUrl(repo, path), { headers: ghHeaders('application/vnd.github.raw', token) });
|
|
73
|
+
if (res.status === 404)
|
|
74
|
+
return null;
|
|
75
|
+
if (!res.ok)
|
|
76
|
+
throw new Error(`GitHub read ${path} failed: ${res.status}`);
|
|
77
|
+
return res.text();
|
|
78
|
+
}
|
|
79
|
+
/** Standard (padded) base64 of UTF-8 text, the form the contents API expects. */
|
|
80
|
+
function toBase64(text) {
|
|
81
|
+
return btoa(Array.from(new TextEncoder().encode(text), (b) => String.fromCharCode(b)).join(''));
|
|
82
|
+
}
|
|
83
|
+
/** The current blob sha for a path, or null if the file does not yet exist. */
|
|
84
|
+
export async function fileSha(repo, path, token) {
|
|
85
|
+
const res = await fetch(contentsUrl(repo, path), { headers: ghHeaders('application/vnd.github+json', token) });
|
|
86
|
+
if (res.status === 404)
|
|
87
|
+
return null;
|
|
88
|
+
if (!res.ok)
|
|
89
|
+
throw new Error(`GitHub stat ${path} failed: ${res.status}`);
|
|
90
|
+
return (await res.json()).sha;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Commit `content` to `path` on the configured branch through the contents API. The author is
|
|
94
|
+
* the editor; the committer is omitted, so GitHub attributes it to the App (`cairn-cms[bot]`).
|
|
95
|
+
* Updates the file in place when it exists (passing its sha), creates it otherwise. Returns the
|
|
96
|
+
* commit sha. A stale-sha 409 (someone committed in between) becomes a `CommitConflictError`,
|
|
97
|
+
* so the save fails safe: re-fetch and ask the editor to reapply, never a merge.
|
|
98
|
+
*
|
|
99
|
+
* Caller preconditions this layer cannot enforce, and the save action (Plan 05) must:
|
|
100
|
+
* `path` is confined to the concept's configured directory (the App token can write anywhere
|
|
101
|
+
* in the repo, so an unvalidated path could overwrite CI config or source), and `author` is
|
|
102
|
+
* derived from the verified server-side session, never from request input.
|
|
103
|
+
*/
|
|
104
|
+
export async function commitFile(repo, path, content, opts, token) {
|
|
105
|
+
const sha = await fileSha(repo, path, token);
|
|
106
|
+
const url = `${API}/repos/${repo.owner}/${repo.repo}/contents/${path.replace(/^\/+|\/+$/g, '')}`;
|
|
107
|
+
const res = await fetch(url, {
|
|
108
|
+
method: 'PUT',
|
|
109
|
+
headers: { ...ghHeaders('application/vnd.github+json', token), 'Content-Type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
message: opts.message,
|
|
112
|
+
content: toBase64(content),
|
|
113
|
+
branch: repo.branch,
|
|
114
|
+
author: opts.author,
|
|
115
|
+
...(sha ? { sha } : {}),
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
if (res.status === 409)
|
|
119
|
+
throw new CommitConflictError(path);
|
|
120
|
+
if (!res.ok)
|
|
121
|
+
throw new Error(`GitHub commit ${path} failed: ${res.status} ${await res.text()}`);
|
|
122
|
+
return (await res.json()).commit.sha;
|
|
123
|
+
}
|