@rimelight/cms 0.0.3 → 0.0.5
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/bylines-Csggkl-g.d.mts +405 -0
- package/dist/index.d.mts +724 -0
- package/dist/index.mjs +2382 -0
- package/dist/integration.d.mts +33 -0
- package/dist/integration.mjs +161 -0
- package/dist/schema/index.d.mts +2063 -0
- package/dist/schema/index.mjs +176 -0
- package/dist/schema/sqlite.d.mts +2132 -0
- package/dist/schema/sqlite.mjs +137 -0
- package/dist/storage/index.d.mts +59 -0
- package/dist/storage/index.mjs +133 -0
- package/package.json +35 -14
- package/src/admin/api/media-file.ts +1 -1
- package/src/admin/api/media.ts +28 -3
- package/src/admin/layouts/CMSDashboardLayout.astro +16 -3
- package/src/admin/pages/assets.astro +11 -11
- package/src/admin/pages/pages-edit.astro +3 -3
- package/src/admin/pages/pages-index.astro +2 -2
- package/src/admin/pages/pages-preview.astro +3 -3
- package/src/admin/pages/pages-review.astro +2 -2
- package/src/admin/pages/templates-edit.astro +1 -1
- package/src/admin/pages/templates-index.astro +1 -1
- package/src/astro/BlockRenderer.astro +1 -1
- package/src/astro/PageRenderer.astro +4 -4
- package/src/astro/blocks/CalloutBlock.astro +1 -1
- package/src/astro/blocks/OrderedListBlock.astro +2 -2
- package/src/astro/blocks/ParagraphBlock.astro +1 -1
- package/src/astro/blocks/TabsBlock.astro +5 -5
- package/src/astro/blocks/UnorderedListBlock.astro +2 -2
- package/src/auth/editor-guards.ts +1 -1
- package/src/cache/purge.ts +2 -2
- package/src/client/components/RimelightBlock.tsx +1 -1
- package/src/client/components/RimelightBlockPicker.tsx +1 -1
- package/src/client/components/RimelightPreview.tsx +27 -27
- package/src/client/components/RimelightPropertiesPanel.tsx +4 -4
- package/src/client/components/RimelightTemplateEditor.tsx +9 -5
- package/src/client/components/editors/CalloutEditor.tsx +10 -10
- package/src/client/components/editors/CardEditor.tsx +5 -5
- package/src/client/components/editors/CardsEditor.tsx +2 -2
- package/src/client/components/editors/CodeEditor.tsx +5 -5
- package/src/client/components/editors/DialogueEditor.tsx +6 -6
- package/src/client/components/editors/FileTreeEditor.tsx +1 -1
- package/src/client/components/editors/ImageEditor.tsx +9 -9
- package/src/client/components/editors/ParagraphEditor.tsx +1 -1
- package/src/client/components/editors/SceneEditor.tsx +9 -9
- package/src/client/components/editors/ScriptEditor.tsx +8 -8
- package/src/client/components/editors/SectionEditor.tsx +3 -3
- package/src/client/components/editors/StepItemEditor.tsx +6 -5
- package/src/client/components/editors/StepsEditor.tsx +1 -1
- package/src/client/components/editors/TabItemEditor.tsx +3 -3
- package/src/client/components/editors/TableEditor.tsx +4 -4
- package/src/client/components/editors/TabsEditor.tsx +1 -1
- package/src/client/state/editor-store.ts +23 -23
- package/src/client/utils/sortable.ts +13 -12
- package/src/client/utils/tree-sanitizer.ts +1 -1
- package/src/core/excerpt.ts +16 -16
- package/src/core/page-definitions.ts +2 -2
- package/src/docs/agent.ts +1 -1
- package/src/docs/routing.ts +2 -2
- package/src/env.d.ts +6 -0
- package/src/index.ts +17 -4
- package/src/integration.ts +54 -0
- package/src/loader/live.ts +1 -1
- package/src/markdown/serializer.ts +26 -26
- package/src/schema/sqlite.ts +344 -0
- package/src/storage/index.ts +4 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2382 @@
|
|
|
1
|
+
import { bylines, contentSearchIndex, pageDraftLocks, pageDrafts, pageTaxonomyTerms, pageTemplates, pageVersionApprovals, pageVersionComments, pageVersions, pages, siteSettings, taxonomyTerms } from "./schema/index.mjs";
|
|
2
|
+
import { rimelightCms } from "./integration.mjs";
|
|
3
|
+
import { deleteR2File, getR2Bucket, listR2Files, r2, uploadR2File } from "./storage/index.mjs";
|
|
4
|
+
import { auth0Auth, cfAccessAuth, evaluateAccess, hasPermission, hasRole, mockAuth } from "@rimelight/auth";
|
|
5
|
+
import { and, asc, desc, eq, isNotNull, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
6
|
+
import { addToast, clearToasts, removeToast, showErrorToast, showSuccessToast, toast, toasts } from "@rimelight/ui/components/toast/store.ts";
|
|
7
|
+
//#region src/core/types/blocks.ts
|
|
8
|
+
const MIN_SECTION_HEADING_LEVEL = 2;
|
|
9
|
+
const MAX_SECTION_HEADING_LEVEL = 6;
|
|
10
|
+
const MAX_SECTION_DEPTH = 4;
|
|
11
|
+
function calculateHeadingLevel(sectionDepth) {
|
|
12
|
+
return Math.min(6, Math.max(0, Math.floor(sectionDepth || 0)) + 2);
|
|
13
|
+
}
|
|
14
|
+
const ALLOWED_CHILDREN_MAP = {
|
|
15
|
+
Root: [
|
|
16
|
+
"SectionBlock",
|
|
17
|
+
"ParagraphBlock",
|
|
18
|
+
"CalloutBlock",
|
|
19
|
+
"ImageBlock",
|
|
20
|
+
"CodeBlock",
|
|
21
|
+
"ScriptBlock",
|
|
22
|
+
"SceneBlock",
|
|
23
|
+
"DialogueBlock",
|
|
24
|
+
"TableBlock",
|
|
25
|
+
"TabsBlock",
|
|
26
|
+
"StepsBlock",
|
|
27
|
+
"CardsBlock",
|
|
28
|
+
"FileTreeBlock",
|
|
29
|
+
"ComponentShowcaseBlock",
|
|
30
|
+
"ApiDocumentationBlock",
|
|
31
|
+
"LivePreviewBlock"
|
|
32
|
+
],
|
|
33
|
+
SectionBlock: [
|
|
34
|
+
"SectionBlock",
|
|
35
|
+
"ParagraphBlock",
|
|
36
|
+
"CalloutBlock",
|
|
37
|
+
"ImageBlock",
|
|
38
|
+
"CodeBlock",
|
|
39
|
+
"ScriptBlock",
|
|
40
|
+
"TableBlock",
|
|
41
|
+
"TabsBlock",
|
|
42
|
+
"StepsBlock",
|
|
43
|
+
"CardsBlock",
|
|
44
|
+
"FileTreeBlock",
|
|
45
|
+
"ComponentShowcaseBlock",
|
|
46
|
+
"ApiDocumentationBlock",
|
|
47
|
+
"LivePreviewBlock"
|
|
48
|
+
],
|
|
49
|
+
CalloutBlock: [
|
|
50
|
+
"ParagraphBlock",
|
|
51
|
+
"CalloutBlock",
|
|
52
|
+
"ImageBlock",
|
|
53
|
+
"CodeBlock",
|
|
54
|
+
"TableBlock",
|
|
55
|
+
"TabsBlock",
|
|
56
|
+
"StepsBlock",
|
|
57
|
+
"CardsBlock",
|
|
58
|
+
"FileTreeBlock",
|
|
59
|
+
"ComponentShowcaseBlock",
|
|
60
|
+
"ApiDocumentationBlock",
|
|
61
|
+
"LivePreviewBlock"
|
|
62
|
+
],
|
|
63
|
+
TabsBlock: ["TabItemBlock"],
|
|
64
|
+
TabItemBlock: [
|
|
65
|
+
"ParagraphBlock",
|
|
66
|
+
"CalloutBlock",
|
|
67
|
+
"ImageBlock",
|
|
68
|
+
"CodeBlock",
|
|
69
|
+
"TableBlock",
|
|
70
|
+
"StepsBlock",
|
|
71
|
+
"CardsBlock",
|
|
72
|
+
"FileTreeBlock",
|
|
73
|
+
"ComponentShowcaseBlock",
|
|
74
|
+
"ApiDocumentationBlock",
|
|
75
|
+
"LivePreviewBlock"
|
|
76
|
+
],
|
|
77
|
+
StepsBlock: ["StepItemBlock"],
|
|
78
|
+
StepItemBlock: [
|
|
79
|
+
"ParagraphBlock",
|
|
80
|
+
"CalloutBlock",
|
|
81
|
+
"ImageBlock",
|
|
82
|
+
"CodeBlock",
|
|
83
|
+
"TableBlock",
|
|
84
|
+
"TabsBlock",
|
|
85
|
+
"CardsBlock",
|
|
86
|
+
"FileTreeBlock",
|
|
87
|
+
"ComponentShowcaseBlock",
|
|
88
|
+
"ApiDocumentationBlock",
|
|
89
|
+
"LivePreviewBlock"
|
|
90
|
+
],
|
|
91
|
+
CardsBlock: ["CardBlock"],
|
|
92
|
+
ScriptBlock: ["SceneBlock"],
|
|
93
|
+
SceneBlock: [
|
|
94
|
+
"ParagraphBlock",
|
|
95
|
+
"CalloutBlock",
|
|
96
|
+
"DialogueBlock"
|
|
97
|
+
]
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/core/page-definitions.ts
|
|
101
|
+
function uuid() {
|
|
102
|
+
return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 11);
|
|
103
|
+
}
|
|
104
|
+
function definePageDefinition(def) {
|
|
105
|
+
return def;
|
|
106
|
+
}
|
|
107
|
+
const DOCUMENT_DEFINITION = definePageDefinition({
|
|
108
|
+
typeLabelKey: "page.type.document",
|
|
109
|
+
properties: {}
|
|
110
|
+
});
|
|
111
|
+
const BLOG_POST_DEFINITION = definePageDefinition({
|
|
112
|
+
typeLabelKey: "page.type.blogPost",
|
|
113
|
+
properties: { meta: {
|
|
114
|
+
label: { en: "Post Metadata" },
|
|
115
|
+
defaultOpen: true,
|
|
116
|
+
fields: {
|
|
117
|
+
category: {
|
|
118
|
+
type: "enum",
|
|
119
|
+
label: { en: "Category" },
|
|
120
|
+
defaultValue: { en: "Company News" },
|
|
121
|
+
options: [
|
|
122
|
+
{ en: "Company News" },
|
|
123
|
+
{ en: "Development Log" },
|
|
124
|
+
{ en: "New Release" }
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
publishedAt: {
|
|
128
|
+
type: "text",
|
|
129
|
+
label: { en: "Published At" },
|
|
130
|
+
defaultValue: { en: "" }
|
|
131
|
+
},
|
|
132
|
+
author: {
|
|
133
|
+
type: "text",
|
|
134
|
+
label: { en: "Author" },
|
|
135
|
+
defaultValue: { en: "" }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} }
|
|
139
|
+
});
|
|
140
|
+
const PATCH_NOTE_DEFINITION = definePageDefinition({
|
|
141
|
+
typeLabelKey: "page.type.patchNote",
|
|
142
|
+
properties: { version: {
|
|
143
|
+
label: { en: "Version Info" },
|
|
144
|
+
defaultOpen: true,
|
|
145
|
+
fields: {
|
|
146
|
+
versionNumber: {
|
|
147
|
+
type: "text",
|
|
148
|
+
label: { en: "Version Number" },
|
|
149
|
+
defaultValue: "1.0.0"
|
|
150
|
+
},
|
|
151
|
+
releaseDate: {
|
|
152
|
+
type: "text",
|
|
153
|
+
label: { en: "Release Date" },
|
|
154
|
+
defaultValue: { en: "" }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} }
|
|
158
|
+
});
|
|
159
|
+
const LOCATION_DEFINITION = definePageDefinition({
|
|
160
|
+
typeLabelKey: "page.type.location",
|
|
161
|
+
properties: { geography: {
|
|
162
|
+
label: { en: "Geography" },
|
|
163
|
+
defaultOpen: true,
|
|
164
|
+
fields: {
|
|
165
|
+
region: {
|
|
166
|
+
type: "text",
|
|
167
|
+
label: { en: "Region" },
|
|
168
|
+
defaultValue: { en: "" }
|
|
169
|
+
},
|
|
170
|
+
climate: {
|
|
171
|
+
type: "text",
|
|
172
|
+
label: { en: "Climate" },
|
|
173
|
+
defaultValue: "Temperate"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} }
|
|
177
|
+
});
|
|
178
|
+
const SPECIES_DEFINITION = definePageDefinition({
|
|
179
|
+
typeLabelKey: "page.type.species",
|
|
180
|
+
properties: { biology: {
|
|
181
|
+
label: { en: "Biology" },
|
|
182
|
+
defaultOpen: true,
|
|
183
|
+
fields: {
|
|
184
|
+
lifespan: {
|
|
185
|
+
type: "text",
|
|
186
|
+
label: { en: "Average Lifespan" },
|
|
187
|
+
defaultValue: { en: "" }
|
|
188
|
+
},
|
|
189
|
+
homeworld: {
|
|
190
|
+
type: "page",
|
|
191
|
+
label: { en: "Homeworld" },
|
|
192
|
+
defaultValue: "",
|
|
193
|
+
allowedPageTypes: ["Location"]
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} }
|
|
197
|
+
});
|
|
198
|
+
const CHARACTER_DEFINITION = definePageDefinition({
|
|
199
|
+
typeLabelKey: "page.type.character",
|
|
200
|
+
properties: {
|
|
201
|
+
identity: {
|
|
202
|
+
label: { en: "Identity" },
|
|
203
|
+
defaultOpen: true,
|
|
204
|
+
fields: {
|
|
205
|
+
name: {
|
|
206
|
+
type: "text",
|
|
207
|
+
label: { en: "Name" },
|
|
208
|
+
defaultValue: { en: "" }
|
|
209
|
+
},
|
|
210
|
+
title: {
|
|
211
|
+
type: "text",
|
|
212
|
+
label: { en: "Title" },
|
|
213
|
+
defaultValue: { en: "" }
|
|
214
|
+
},
|
|
215
|
+
aliases: {
|
|
216
|
+
type: "text-array",
|
|
217
|
+
label: { en: "Aliases" },
|
|
218
|
+
defaultValue: []
|
|
219
|
+
},
|
|
220
|
+
pronouns: {
|
|
221
|
+
type: "enum",
|
|
222
|
+
label: { en: "Pronouns" },
|
|
223
|
+
defaultValue: { en: "Unknown" },
|
|
224
|
+
options: [
|
|
225
|
+
{ en: "He/Him" },
|
|
226
|
+
{ en: "She/Her" },
|
|
227
|
+
{ en: "They/Them" },
|
|
228
|
+
{ en: "He/They" },
|
|
229
|
+
{ en: "She/They" },
|
|
230
|
+
{ en: "Other" },
|
|
231
|
+
{ en: "Unknown" }
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
flavourText: {
|
|
235
|
+
type: "text",
|
|
236
|
+
label: { en: "Flavour Text" },
|
|
237
|
+
defaultValue: { en: "" }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
characteristics: {
|
|
242
|
+
label: { en: "Characteristics" },
|
|
243
|
+
defaultOpen: true,
|
|
244
|
+
fields: {
|
|
245
|
+
species: {
|
|
246
|
+
type: "page",
|
|
247
|
+
label: { en: "Species" },
|
|
248
|
+
defaultValue: "",
|
|
249
|
+
allowedPageTypes: ["Species"]
|
|
250
|
+
},
|
|
251
|
+
sex: {
|
|
252
|
+
type: "enum",
|
|
253
|
+
label: { en: "Sex" },
|
|
254
|
+
defaultValue: { en: "Unknown" },
|
|
255
|
+
options: [
|
|
256
|
+
{ en: "Male" },
|
|
257
|
+
{ en: "Female" },
|
|
258
|
+
{ en: "Other" },
|
|
259
|
+
{ en: "Unknown" }
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
height: {
|
|
263
|
+
type: "number",
|
|
264
|
+
label: { en: "Height" },
|
|
265
|
+
defaultValue: 0
|
|
266
|
+
},
|
|
267
|
+
weight: {
|
|
268
|
+
type: "number",
|
|
269
|
+
label: { en: "Weight" },
|
|
270
|
+
defaultValue: 0
|
|
271
|
+
},
|
|
272
|
+
dateOfBirth: {
|
|
273
|
+
type: "text",
|
|
274
|
+
label: { en: "Date of Birth" },
|
|
275
|
+
defaultValue: { en: "" }
|
|
276
|
+
},
|
|
277
|
+
placeOfBirth: {
|
|
278
|
+
type: "text",
|
|
279
|
+
label: { en: "Place of Birth" },
|
|
280
|
+
defaultValue: { en: "" }
|
|
281
|
+
},
|
|
282
|
+
dateOfDeath: {
|
|
283
|
+
type: "text",
|
|
284
|
+
label: { en: "Date of Death" },
|
|
285
|
+
defaultValue: { en: "" }
|
|
286
|
+
},
|
|
287
|
+
placeOfDeath: {
|
|
288
|
+
type: "text",
|
|
289
|
+
label: { en: "Place of Death" },
|
|
290
|
+
defaultValue: { en: "" }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
inventory: {
|
|
295
|
+
label: { en: "Inventory & Assets" },
|
|
296
|
+
defaultOpen: false,
|
|
297
|
+
fields: {
|
|
298
|
+
equipment: {
|
|
299
|
+
type: "page-array",
|
|
300
|
+
label: { en: "Equipment" },
|
|
301
|
+
defaultValue: [],
|
|
302
|
+
allowedPageTypes: ["Object"]
|
|
303
|
+
},
|
|
304
|
+
pets: {
|
|
305
|
+
type: "page-array",
|
|
306
|
+
label: { en: "Pets" },
|
|
307
|
+
defaultValue: [],
|
|
308
|
+
allowedPageTypes: ["Character"]
|
|
309
|
+
},
|
|
310
|
+
mounts: {
|
|
311
|
+
type: "page-array",
|
|
312
|
+
label: { en: "Mounts" },
|
|
313
|
+
defaultValue: [],
|
|
314
|
+
allowedPageTypes: ["Character"]
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
affiliations: {
|
|
319
|
+
label: { en: "Affiliations" },
|
|
320
|
+
defaultOpen: false,
|
|
321
|
+
fields: {
|
|
322
|
+
currentAffiliations: {
|
|
323
|
+
type: "page-array",
|
|
324
|
+
label: { en: "Current Affiliations" },
|
|
325
|
+
defaultValue: [],
|
|
326
|
+
allowedPageTypes: ["Group"]
|
|
327
|
+
},
|
|
328
|
+
formerAffiliations: {
|
|
329
|
+
type: "page-array",
|
|
330
|
+
label: { en: "Former Affiliations" },
|
|
331
|
+
defaultValue: [],
|
|
332
|
+
allowedPageTypes: ["Group"]
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
other: {
|
|
337
|
+
label: { en: "Other" },
|
|
338
|
+
defaultOpen: false,
|
|
339
|
+
fields: { favouriteFood: {
|
|
340
|
+
type: "page",
|
|
341
|
+
label: { en: "Favourite Food" },
|
|
342
|
+
defaultValue: "",
|
|
343
|
+
allowedPageTypes: ["Object"]
|
|
344
|
+
} }
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
initialBlocks: () => [
|
|
348
|
+
{
|
|
349
|
+
id: uuid(),
|
|
350
|
+
type: "SectionBlock",
|
|
351
|
+
props: {
|
|
352
|
+
level: 2,
|
|
353
|
+
title: "Appearance",
|
|
354
|
+
children: []
|
|
355
|
+
},
|
|
356
|
+
isTemplated: true
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: uuid(),
|
|
360
|
+
type: "SectionBlock",
|
|
361
|
+
props: {
|
|
362
|
+
level: 2,
|
|
363
|
+
title: "Abilities",
|
|
364
|
+
children: []
|
|
365
|
+
},
|
|
366
|
+
isTemplated: true
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: uuid(),
|
|
370
|
+
type: "SectionBlock",
|
|
371
|
+
props: {
|
|
372
|
+
level: 2,
|
|
373
|
+
title: "History",
|
|
374
|
+
children: []
|
|
375
|
+
},
|
|
376
|
+
isTemplated: true
|
|
377
|
+
}
|
|
378
|
+
]
|
|
379
|
+
});
|
|
380
|
+
const SKILL_DEFINITION = definePageDefinition({
|
|
381
|
+
typeLabelKey: "page.type.skill",
|
|
382
|
+
properties: { mechanics: {
|
|
383
|
+
label: { en: "Mechanics" },
|
|
384
|
+
defaultOpen: true,
|
|
385
|
+
fields: {
|
|
386
|
+
cooldown: {
|
|
387
|
+
type: "number",
|
|
388
|
+
label: { en: "Cooldown (sec)" },
|
|
389
|
+
defaultValue: 10
|
|
390
|
+
},
|
|
391
|
+
manaCost: {
|
|
392
|
+
type: "number",
|
|
393
|
+
label: { en: "Mana Cost" },
|
|
394
|
+
defaultValue: 50
|
|
395
|
+
},
|
|
396
|
+
damageType: {
|
|
397
|
+
type: "enum",
|
|
398
|
+
label: { en: "Damage Type" },
|
|
399
|
+
defaultValue: { en: "Physical" },
|
|
400
|
+
options: [
|
|
401
|
+
{ en: "Physical" },
|
|
402
|
+
{ en: "Magic" },
|
|
403
|
+
{ en: "True" },
|
|
404
|
+
{ en: "None" }
|
|
405
|
+
]
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} }
|
|
409
|
+
});
|
|
410
|
+
const ITEM_DEFINITION = definePageDefinition({
|
|
411
|
+
typeLabelKey: "page.type.item",
|
|
412
|
+
properties: { details: {
|
|
413
|
+
label: { en: "Item Details" },
|
|
414
|
+
defaultOpen: true,
|
|
415
|
+
fields: {
|
|
416
|
+
rarity: {
|
|
417
|
+
type: "enum",
|
|
418
|
+
label: { en: "Rarity" },
|
|
419
|
+
defaultValue: { en: "Common" },
|
|
420
|
+
options: [
|
|
421
|
+
{ en: "Common" },
|
|
422
|
+
{ en: "Uncommon" },
|
|
423
|
+
{ en: "Rare" },
|
|
424
|
+
{ en: "Epic" },
|
|
425
|
+
{ en: "Legendary" }
|
|
426
|
+
]
|
|
427
|
+
},
|
|
428
|
+
price: {
|
|
429
|
+
type: "number",
|
|
430
|
+
label: { en: "Gold Price" },
|
|
431
|
+
defaultValue: 100
|
|
432
|
+
},
|
|
433
|
+
isQuestItem: {
|
|
434
|
+
type: "enum",
|
|
435
|
+
label: { en: "Quest Item" },
|
|
436
|
+
defaultValue: { en: "No" },
|
|
437
|
+
options: [{ en: "Yes" }, { en: "No" }]
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} }
|
|
441
|
+
});
|
|
442
|
+
const OBJECT_DEFINITION = definePageDefinition({
|
|
443
|
+
typeLabelKey: "page.type.object",
|
|
444
|
+
properties: { details: {
|
|
445
|
+
label: { en: "Object Details" },
|
|
446
|
+
defaultOpen: true,
|
|
447
|
+
fields: { category: {
|
|
448
|
+
type: "text",
|
|
449
|
+
label: { en: "Category" },
|
|
450
|
+
defaultValue: { en: "Miscellaneous" }
|
|
451
|
+
} }
|
|
452
|
+
} }
|
|
453
|
+
});
|
|
454
|
+
const GROUP_DEFINITION = definePageDefinition({
|
|
455
|
+
typeLabelKey: "page.type.group",
|
|
456
|
+
properties: { details: {
|
|
457
|
+
label: { en: "Group Details" },
|
|
458
|
+
defaultOpen: true,
|
|
459
|
+
fields: {
|
|
460
|
+
leader: {
|
|
461
|
+
type: "page",
|
|
462
|
+
label: { en: "Leader" },
|
|
463
|
+
defaultValue: "",
|
|
464
|
+
allowedPageTypes: ["Character"]
|
|
465
|
+
},
|
|
466
|
+
headquarters: {
|
|
467
|
+
type: "page",
|
|
468
|
+
label: { en: "Headquarters" },
|
|
469
|
+
defaultValue: "",
|
|
470
|
+
allowedPageTypes: ["Location"]
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} }
|
|
474
|
+
});
|
|
475
|
+
const HERO_DEFINITION = definePageDefinition({
|
|
476
|
+
typeLabelKey: "page.type.hero",
|
|
477
|
+
properties: {
|
|
478
|
+
combat: {
|
|
479
|
+
label: { en: "Combat Stats" },
|
|
480
|
+
defaultOpen: true,
|
|
481
|
+
fields: {
|
|
482
|
+
class: {
|
|
483
|
+
type: "enum",
|
|
484
|
+
label: { en: "Class" },
|
|
485
|
+
defaultValue: { en: "Warrior" },
|
|
486
|
+
options: [
|
|
487
|
+
{ en: "Warrior" },
|
|
488
|
+
{ en: "Mage" },
|
|
489
|
+
{ en: "Rogue" },
|
|
490
|
+
{ en: "Paladin" }
|
|
491
|
+
]
|
|
492
|
+
},
|
|
493
|
+
difficulty: {
|
|
494
|
+
type: "number",
|
|
495
|
+
label: { en: "Difficulty" },
|
|
496
|
+
defaultValue: 1
|
|
497
|
+
},
|
|
498
|
+
primaryRole: {
|
|
499
|
+
type: "enum",
|
|
500
|
+
label: { en: "Primary Role" },
|
|
501
|
+
defaultValue: { en: "Tank" },
|
|
502
|
+
options: [
|
|
503
|
+
{ en: "Tank" },
|
|
504
|
+
{ en: "DPS" },
|
|
505
|
+
{ en: "Support" }
|
|
506
|
+
]
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
progression: {
|
|
511
|
+
label: { en: "Progression" },
|
|
512
|
+
defaultOpen: true,
|
|
513
|
+
fields: {
|
|
514
|
+
baseHp: {
|
|
515
|
+
type: "number",
|
|
516
|
+
label: { en: "Base HP" },
|
|
517
|
+
defaultValue: 500
|
|
518
|
+
},
|
|
519
|
+
baseMana: {
|
|
520
|
+
type: "number",
|
|
521
|
+
label: { en: "Base Mana" },
|
|
522
|
+
defaultValue: 100
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
initialBlocks: () => [{
|
|
528
|
+
id: uuid(),
|
|
529
|
+
type: "SectionBlock",
|
|
530
|
+
props: {
|
|
531
|
+
level: 2,
|
|
532
|
+
title: "Playstyle",
|
|
533
|
+
children: []
|
|
534
|
+
},
|
|
535
|
+
isTemplated: true
|
|
536
|
+
}, {
|
|
537
|
+
id: uuid(),
|
|
538
|
+
type: "SectionBlock",
|
|
539
|
+
props: {
|
|
540
|
+
level: 2,
|
|
541
|
+
title: "Background Lore",
|
|
542
|
+
children: []
|
|
543
|
+
},
|
|
544
|
+
isTemplated: true
|
|
545
|
+
}]
|
|
546
|
+
});
|
|
547
|
+
const CARD_DEFINITION = definePageDefinition({
|
|
548
|
+
typeLabelKey: "page.type.card",
|
|
549
|
+
properties: {},
|
|
550
|
+
initialBlocks: () => []
|
|
551
|
+
});
|
|
552
|
+
const SERIES_DEFINITION = definePageDefinition({
|
|
553
|
+
typeLabelKey: "page.type.series",
|
|
554
|
+
properties: { details: {
|
|
555
|
+
label: { en: "Details" },
|
|
556
|
+
defaultOpen: true,
|
|
557
|
+
fields: {
|
|
558
|
+
genre: {
|
|
559
|
+
type: "text",
|
|
560
|
+
label: { en: "Genre" },
|
|
561
|
+
defaultValue: { en: "Fantasy" }
|
|
562
|
+
},
|
|
563
|
+
status: {
|
|
564
|
+
type: "enum",
|
|
565
|
+
label: { en: "Status" },
|
|
566
|
+
defaultValue: { en: "In Development" },
|
|
567
|
+
options: [
|
|
568
|
+
{ en: "In Development" },
|
|
569
|
+
{ en: "Ongoing" },
|
|
570
|
+
{ en: "Completed" },
|
|
571
|
+
{ en: "Cancelled" }
|
|
572
|
+
]
|
|
573
|
+
},
|
|
574
|
+
seasons: {
|
|
575
|
+
type: "number",
|
|
576
|
+
label: { en: "Seasons" },
|
|
577
|
+
defaultValue: 1
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} },
|
|
581
|
+
initialBlocks: () => [{
|
|
582
|
+
id: uuid(),
|
|
583
|
+
type: "SectionBlock",
|
|
584
|
+
props: {
|
|
585
|
+
level: 2,
|
|
586
|
+
title: "Overview",
|
|
587
|
+
children: []
|
|
588
|
+
},
|
|
589
|
+
isTemplated: true
|
|
590
|
+
}, {
|
|
591
|
+
id: uuid(),
|
|
592
|
+
type: "SectionBlock",
|
|
593
|
+
props: {
|
|
594
|
+
level: 2,
|
|
595
|
+
title: "Seasons",
|
|
596
|
+
children: []
|
|
597
|
+
},
|
|
598
|
+
isTemplated: true
|
|
599
|
+
}]
|
|
600
|
+
});
|
|
601
|
+
const PAGE_MAP = {
|
|
602
|
+
Document: DOCUMENT_DEFINITION,
|
|
603
|
+
BlogPost: BLOG_POST_DEFINITION,
|
|
604
|
+
PatchNote: PATCH_NOTE_DEFINITION,
|
|
605
|
+
Location: LOCATION_DEFINITION,
|
|
606
|
+
Species: SPECIES_DEFINITION,
|
|
607
|
+
Character: CHARACTER_DEFINITION,
|
|
608
|
+
Skill: SKILL_DEFINITION,
|
|
609
|
+
Item: ITEM_DEFINITION,
|
|
610
|
+
Object: OBJECT_DEFINITION,
|
|
611
|
+
Group: GROUP_DEFINITION,
|
|
612
|
+
Card: CARD_DEFINITION,
|
|
613
|
+
Hero: HERO_DEFINITION,
|
|
614
|
+
Series: SERIES_DEFINITION,
|
|
615
|
+
Tale: definePageDefinition({
|
|
616
|
+
typeLabelKey: "page.type.tale",
|
|
617
|
+
properties: {}
|
|
618
|
+
}),
|
|
619
|
+
Episode: definePageDefinition({
|
|
620
|
+
typeLabelKey: "page.type.episode",
|
|
621
|
+
properties: {}
|
|
622
|
+
}),
|
|
623
|
+
Default: definePageDefinition({
|
|
624
|
+
typeLabelKey: "page.type.default",
|
|
625
|
+
properties: {}
|
|
626
|
+
})
|
|
627
|
+
};
|
|
628
|
+
function getPageDefinition(type) {
|
|
629
|
+
if (!type) return PAGE_MAP["Default"];
|
|
630
|
+
if (PAGE_MAP[type]) return PAGE_MAP[type];
|
|
631
|
+
const normalized = type.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
632
|
+
const ALIAS_MAP = {
|
|
633
|
+
blog: BLOG_POST_DEFINITION,
|
|
634
|
+
blogpost: BLOG_POST_DEFINITION,
|
|
635
|
+
article: BLOG_POST_DEFINITION,
|
|
636
|
+
doc: DOCUMENT_DEFINITION,
|
|
637
|
+
document: DOCUMENT_DEFINITION,
|
|
638
|
+
patchnote: PATCH_NOTE_DEFINITION,
|
|
639
|
+
location: LOCATION_DEFINITION,
|
|
640
|
+
species: SPECIES_DEFINITION,
|
|
641
|
+
character: CHARACTER_DEFINITION,
|
|
642
|
+
skill: SKILL_DEFINITION,
|
|
643
|
+
item: ITEM_DEFINITION,
|
|
644
|
+
object: OBJECT_DEFINITION,
|
|
645
|
+
group: GROUP_DEFINITION,
|
|
646
|
+
card: CARD_DEFINITION,
|
|
647
|
+
hero: HERO_DEFINITION,
|
|
648
|
+
series: SERIES_DEFINITION
|
|
649
|
+
};
|
|
650
|
+
if (ALIAS_MAP[normalized]) return ALIAS_MAP[normalized];
|
|
651
|
+
for (const [key, def] of Object.entries(PAGE_MAP)) if (key.toLowerCase() === normalized) return def;
|
|
652
|
+
return PAGE_MAP["Default"];
|
|
653
|
+
}
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/markdown/serializer.ts
|
|
656
|
+
function inlinesToMarkdown(inlines) {
|
|
657
|
+
if (!inlines) return "";
|
|
658
|
+
if (typeof inlines === "string") return inlines;
|
|
659
|
+
if (!Array.isArray(inlines)) return inlines.en || "";
|
|
660
|
+
return inlines.map((node) => {
|
|
661
|
+
if (node.type === "text") {
|
|
662
|
+
let text = node.text || "";
|
|
663
|
+
const marks = node.marks || [];
|
|
664
|
+
if (marks.includes("code")) text = `\`${text}\``;
|
|
665
|
+
if (marks.includes("bold")) text = `**${text}**`;
|
|
666
|
+
if (marks.includes("italic")) text = `*${text}*`;
|
|
667
|
+
if (marks.includes("strikethrough")) text = `~~${text}~~`;
|
|
668
|
+
return text;
|
|
669
|
+
}
|
|
670
|
+
if (node.type === "link") {
|
|
671
|
+
const link = node;
|
|
672
|
+
return `[${(link.children || []).map((c) => c.text).join("") || link.url}](${link.url})`;
|
|
673
|
+
}
|
|
674
|
+
if (node.type === "page_mention") return `[${node.displayTitle || node.pageSlug || "Page"}](/${node.pageSlug || ""})`;
|
|
675
|
+
return "";
|
|
676
|
+
}).join("");
|
|
677
|
+
}
|
|
678
|
+
function blocksToMarkdown(blocks = [], options = {}) {
|
|
679
|
+
if (!Array.isArray(blocks) || blocks.length === 0) return "";
|
|
680
|
+
const baseLevel = options.sectionLevel ?? 2;
|
|
681
|
+
const lines = [];
|
|
682
|
+
for (const block of blocks) {
|
|
683
|
+
if (!block) continue;
|
|
684
|
+
const children = block.children || block.props?.["children"] || [];
|
|
685
|
+
switch (block.type) {
|
|
686
|
+
case "SectionBlock": {
|
|
687
|
+
const level = block.props?.["level"] || baseLevel;
|
|
688
|
+
const hashes = "#".repeat(Math.min(6, Math.max(1, level)));
|
|
689
|
+
const title = block.props?.["title"] || "";
|
|
690
|
+
if (title) lines.push(`${hashes} ${title}`);
|
|
691
|
+
if (block.props?.["description"]) lines.push(`_${block.props["description"]}_`);
|
|
692
|
+
if (children.length > 0) lines.push(blocksToMarkdown(children, { sectionLevel: Math.min(6, level + 1) }));
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
case "ParagraphBlock": {
|
|
696
|
+
const text = inlinesToMarkdown(block.props?.["text"]);
|
|
697
|
+
if (text) lines.push(text);
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
case "CalloutBlock": {
|
|
701
|
+
const variant = (block.props?.["variant"] || "info").toUpperCase();
|
|
702
|
+
const childMd = blocksToMarkdown(children, options);
|
|
703
|
+
const calloutHeader = `> [!${variant}]`;
|
|
704
|
+
if (childMd) {
|
|
705
|
+
const indented = childMd.split("\n").map((l) => l.trim() ? `> ${l}` : `>`).join("\n");
|
|
706
|
+
lines.push(`${calloutHeader}\n${indented}`);
|
|
707
|
+
} else lines.push(calloutHeader);
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
case "CodeBlock": {
|
|
711
|
+
const lang = block.props?.["language"] || "";
|
|
712
|
+
const code = block.props?.["code"] || "";
|
|
713
|
+
const caption = block.props?.["caption"] ? `\n_${block.props["caption"]}_` : "";
|
|
714
|
+
lines.push(`\`\`\`${lang}\n${code}\n\`\`\`${caption}`);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case "TableBlock": {
|
|
718
|
+
const columns = block.props?.["columns"] || [];
|
|
719
|
+
const rows = block.props?.["rows"] || [];
|
|
720
|
+
if (columns.length > 0) {
|
|
721
|
+
const tableMd = [
|
|
722
|
+
`| ${columns.map((c) => c.header || c.key).join(" | ")} |`,
|
|
723
|
+
`| ${columns.map((c) => {
|
|
724
|
+
if (c.align === "center") return ":---:";
|
|
725
|
+
if (c.align === "right") return "---:";
|
|
726
|
+
return "---";
|
|
727
|
+
}).join(" | ")} |`,
|
|
728
|
+
...rows.map((r) => `| ${columns.map((c) => r[c.key] || "").join(" | ")} |`)
|
|
729
|
+
].join("\n");
|
|
730
|
+
const caption = block.props?.["caption"] ? `\n_${block.props["caption"]}_` : "";
|
|
731
|
+
lines.push(`${tableMd}${caption}`);
|
|
732
|
+
}
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
case "ImageBlock": {
|
|
736
|
+
const alt = block.props?.["alt"] || "";
|
|
737
|
+
const src = block.props?.["src"] || "";
|
|
738
|
+
const caption = block.props?.["caption"] ? `\n_${block.props["caption"]}_` : "";
|
|
739
|
+
lines.push(`${caption}`);
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case "TabsBlock":
|
|
743
|
+
for (const tab of children) {
|
|
744
|
+
const tabLabel = tab.props?.label || "Tab";
|
|
745
|
+
const tabChildren = tab.children || tab.props?.children || [];
|
|
746
|
+
lines.push(`#### ${tabLabel}\n\n${blocksToMarkdown(tabChildren, options)}`);
|
|
747
|
+
}
|
|
748
|
+
break;
|
|
749
|
+
case "TabItemBlock": {
|
|
750
|
+
const label = block.props?.["label"] || "Tab";
|
|
751
|
+
lines.push(`#### ${label}\n\n${blocksToMarkdown(children, options)}`);
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "StepsBlock": {
|
|
755
|
+
let stepIndex = 1;
|
|
756
|
+
for (const step of children) {
|
|
757
|
+
const title = step.props?.title || `Step ${stepIndex}`;
|
|
758
|
+
const desc = step.props?.description ? ` - ${step.props.description}` : "";
|
|
759
|
+
const stepChildren = step.children || step.props?.children || [];
|
|
760
|
+
lines.push(`${stepIndex}. **${title}**${desc}`);
|
|
761
|
+
if (stepChildren.length > 0) lines.push(blocksToMarkdown(stepChildren, options));
|
|
762
|
+
stepIndex++;
|
|
763
|
+
}
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
case "StepItemBlock": {
|
|
767
|
+
const title = block.props?.["title"] || "Step";
|
|
768
|
+
const desc = block.props?.["description"] ? ` - ${block.props["description"]}` : "";
|
|
769
|
+
lines.push(`- **${title}**${desc}`);
|
|
770
|
+
if (children.length > 0) lines.push(blocksToMarkdown(children, options));
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
case "CardsBlock":
|
|
774
|
+
for (const card of children) {
|
|
775
|
+
const title = card.props?.title || "";
|
|
776
|
+
const href = card.props?.href || "#";
|
|
777
|
+
const desc = card.props?.description ? `: ${card.props.description}` : "";
|
|
778
|
+
lines.push(`- [${title}](${href})${desc}`);
|
|
779
|
+
}
|
|
780
|
+
break;
|
|
781
|
+
case "CardBlock": {
|
|
782
|
+
const title = block.props?.["title"] || "";
|
|
783
|
+
const href = block.props?.["href"] || "#";
|
|
784
|
+
const desc = block.props?.["description"] ? `: ${block.props["description"]}` : "";
|
|
785
|
+
lines.push(`- [${title}](${href})${desc}`);
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
case "FileTreeBlock": {
|
|
789
|
+
const renderTree = (nodes, prefix = "") => {
|
|
790
|
+
const treeLines = [];
|
|
791
|
+
for (const node of nodes) {
|
|
792
|
+
const icon = node.isDir ? "📁 " : "📄 ";
|
|
793
|
+
const comment = node.comment ? ` # ${node.comment}` : "";
|
|
794
|
+
treeLines.push(`${prefix}${icon}${node.name}${comment}`);
|
|
795
|
+
if (node.children && node.children.length > 0) treeLines.push(...renderTree(node.children, `${prefix} `));
|
|
796
|
+
}
|
|
797
|
+
return treeLines;
|
|
798
|
+
};
|
|
799
|
+
const treeNodes = block.props?.["tree"] || [];
|
|
800
|
+
lines.push(`\`\`\`text\n${renderTree(treeNodes).join("\n")}\n\`\`\``);
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
case "DialogueBlock":
|
|
804
|
+
lines.push(`**${block.props?.["character"]}:** "${block.props?.["line"] || ""}"`);
|
|
805
|
+
break;
|
|
806
|
+
default: if (block.props?.["text"]) lines.push(inlinesToMarkdown(block.props["text"]));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return lines.filter(Boolean).join("\n\n");
|
|
810
|
+
}
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/core/registry.ts
|
|
813
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
814
|
+
const BlockRegistry = {
|
|
815
|
+
register(def) {
|
|
816
|
+
definitions.set(def.type, def);
|
|
817
|
+
},
|
|
818
|
+
get(type) {
|
|
819
|
+
return definitions.get(type);
|
|
820
|
+
},
|
|
821
|
+
getAll() {
|
|
822
|
+
return Array.from(definitions.values());
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
BlockRegistry.register({
|
|
826
|
+
type: "SectionBlock",
|
|
827
|
+
label: "Section",
|
|
828
|
+
description: "Container block for grouping nested content",
|
|
829
|
+
defaultProps: { title: "" },
|
|
830
|
+
allowChildren: true
|
|
831
|
+
});
|
|
832
|
+
BlockRegistry.register({
|
|
833
|
+
type: "ParagraphBlock",
|
|
834
|
+
label: "Paragraph",
|
|
835
|
+
description: "Rich text paragraph block",
|
|
836
|
+
defaultProps: { text: { en: "" } }
|
|
837
|
+
});
|
|
838
|
+
BlockRegistry.register({
|
|
839
|
+
type: "CalloutBlock",
|
|
840
|
+
label: "Callout",
|
|
841
|
+
description: "Highlighted callout box (info, warning, note)",
|
|
842
|
+
defaultProps: {
|
|
843
|
+
variant: "info",
|
|
844
|
+
text: { en: "" }
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
BlockRegistry.register({
|
|
848
|
+
type: "ImageBlock",
|
|
849
|
+
label: "Image",
|
|
850
|
+
description: "Image component with caption and alt text",
|
|
851
|
+
defaultProps: {
|
|
852
|
+
url: "",
|
|
853
|
+
alt: "",
|
|
854
|
+
caption: ""
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
BlockRegistry.register({
|
|
858
|
+
type: "CodeBlock",
|
|
859
|
+
label: "Code",
|
|
860
|
+
description: "Code block with syntax highlighting",
|
|
861
|
+
defaultProps: {
|
|
862
|
+
code: "",
|
|
863
|
+
language: "typescript"
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region src/core/validator.ts
|
|
868
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
869
|
+
function isValidUUID(uuid) {
|
|
870
|
+
return UUID_REGEX.test(uuid);
|
|
871
|
+
}
|
|
872
|
+
function validateBlockAST(blocks) {
|
|
873
|
+
const errors = [];
|
|
874
|
+
function checkBlocks(nodes) {
|
|
875
|
+
for (const b of nodes) {
|
|
876
|
+
if (!b.id || typeof b.id !== "string" || !isValidUUID(b.id)) errors.push(`Block of type '${b.type}' has invalid UUID identifier: '${b.id}'`);
|
|
877
|
+
if (!b.type || typeof b.type !== "string") errors.push(`Block missing valid 'type' field`);
|
|
878
|
+
if (b.children && Array.isArray(b.children)) checkBlocks(b.children);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
checkBlocks(blocks);
|
|
882
|
+
return {
|
|
883
|
+
valid: errors.length === 0,
|
|
884
|
+
errors
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/auth/permissions.ts
|
|
889
|
+
const cmsStatements = {
|
|
890
|
+
page: [
|
|
891
|
+
"create",
|
|
892
|
+
"read",
|
|
893
|
+
"update",
|
|
894
|
+
"delete",
|
|
895
|
+
"publish",
|
|
896
|
+
"approve"
|
|
897
|
+
],
|
|
898
|
+
template: [
|
|
899
|
+
"create",
|
|
900
|
+
"read",
|
|
901
|
+
"update",
|
|
902
|
+
"delete",
|
|
903
|
+
"instantiate"
|
|
904
|
+
],
|
|
905
|
+
draft: [
|
|
906
|
+
"checkout",
|
|
907
|
+
"edit",
|
|
908
|
+
"release",
|
|
909
|
+
"force_unlock"
|
|
910
|
+
],
|
|
911
|
+
version: [
|
|
912
|
+
"create",
|
|
913
|
+
"review",
|
|
914
|
+
"approve",
|
|
915
|
+
"publish"
|
|
916
|
+
]
|
|
917
|
+
};
|
|
918
|
+
const cmsAc = {
|
|
919
|
+
page: {
|
|
920
|
+
read: ["admin", "editor"],
|
|
921
|
+
update: ["admin", "editor"],
|
|
922
|
+
delete: ["admin"],
|
|
923
|
+
publish: ["admin"],
|
|
924
|
+
approve: ["admin"]
|
|
925
|
+
},
|
|
926
|
+
template: {
|
|
927
|
+
read: ["admin", "editor"],
|
|
928
|
+
update: ["admin"],
|
|
929
|
+
delete: ["admin"],
|
|
930
|
+
instantiate: ["admin", "editor"]
|
|
931
|
+
},
|
|
932
|
+
draft: {
|
|
933
|
+
checkout: ["admin", "editor"],
|
|
934
|
+
edit: ["admin", "editor"],
|
|
935
|
+
release: ["admin", "editor"],
|
|
936
|
+
force_unlock: ["admin"]
|
|
937
|
+
},
|
|
938
|
+
version: {
|
|
939
|
+
create: ["admin", "editor"],
|
|
940
|
+
review: ["admin", "editor"],
|
|
941
|
+
approve: ["admin"],
|
|
942
|
+
publish: ["admin"]
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/auth/filter-blocks.ts
|
|
947
|
+
/**
|
|
948
|
+
* Pure immutable function for Astro SSR Reader. Returns new array; never mutates input AST
|
|
949
|
+
*/
|
|
950
|
+
function filterBlocksForReader(blocks, session) {
|
|
951
|
+
const filtered = [];
|
|
952
|
+
for (const block of blocks) {
|
|
953
|
+
if (block.allowedRoles && block.allowedRoles.length > 0) {
|
|
954
|
+
if (!session || !session.roles.some((r) => block.allowedRoles.includes(r))) continue;
|
|
955
|
+
}
|
|
956
|
+
if (block.requiredPermission) {
|
|
957
|
+
if (!session || !session.permissions.includes(block.requiredPermission)) continue;
|
|
958
|
+
}
|
|
959
|
+
const filteredChildren = block.children?.length ? filterBlocksForReader(block.children, session) : block.children;
|
|
960
|
+
const newBlock = {
|
|
961
|
+
id: block.id,
|
|
962
|
+
type: block.type,
|
|
963
|
+
props: block.props
|
|
964
|
+
};
|
|
965
|
+
if (filteredChildren !== void 0) newBlock.children = filteredChildren;
|
|
966
|
+
if (block.isTemplated !== void 0) newBlock.isTemplated = block.isTemplated;
|
|
967
|
+
if (block.templatedProps !== void 0) newBlock.templatedProps = block.templatedProps;
|
|
968
|
+
if (block.allowedRoles !== void 0) newBlock.allowedRoles = block.allowedRoles;
|
|
969
|
+
if (block.requiredPermission !== void 0) newBlock.requiredPermission = block.requiredPermission;
|
|
970
|
+
filtered.push(newBlock);
|
|
971
|
+
}
|
|
972
|
+
return filtered;
|
|
973
|
+
}
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/auth/editor-guards.ts
|
|
976
|
+
/**
|
|
977
|
+
* Checks if a user has edit rights over a specific block
|
|
978
|
+
*/
|
|
979
|
+
function canUserEditBlock(block, session) {
|
|
980
|
+
if (block.allowedRoles && block.allowedRoles.length > 0) {
|
|
981
|
+
if (!session || !session.roles.some((r) => block.allowedRoles.includes(r))) return false;
|
|
982
|
+
}
|
|
983
|
+
if (block.requiredPermission) {
|
|
984
|
+
if (!session || !session.permissions.includes(block.requiredPermission)) return false;
|
|
985
|
+
}
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Recursively checks if a block or ANY of its descendants are locked or un-editable
|
|
990
|
+
*/
|
|
991
|
+
function hasLockedDescendants(block, session) {
|
|
992
|
+
if (!canUserEditBlock(block, session)) return true;
|
|
993
|
+
if (block.isTemplated) return true;
|
|
994
|
+
if (block.children?.length) return block.children.some((child) => hasLockedDescendants(child, session));
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Server-Side Guard: Validates that an incoming edited block tree obeys templated & authorization
|
|
999
|
+
* invariants
|
|
1000
|
+
*/
|
|
1001
|
+
function validateServerBlockPayload(existingTree, incomingTree, session) {
|
|
1002
|
+
const existingTemplated = extractTemplatedBlocks(existingTree);
|
|
1003
|
+
const incomingTemplated = extractTemplatedBlocks(incomingTree);
|
|
1004
|
+
if (existingTemplated.length !== incomingTemplated.length) return {
|
|
1005
|
+
valid: false,
|
|
1006
|
+
reason: "Forbidden: One or more templated blocks were removed."
|
|
1007
|
+
};
|
|
1008
|
+
for (let i = 0; i < existingTemplated.length; i++) {
|
|
1009
|
+
const existing = existingTemplated[i];
|
|
1010
|
+
const incoming = incomingTemplated[i];
|
|
1011
|
+
if (!existing || !incoming || existing.id !== incoming.id || existing.type !== incoming.type) return {
|
|
1012
|
+
valid: false,
|
|
1013
|
+
reason: "Forbidden: Reordering of templated blocks is forbidden."
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
for (const existingBlock of existingTemplated) {
|
|
1017
|
+
const incomingBlock = incomingTemplated.find((b) => b.type === existingBlock.type && b.id === existingBlock.id);
|
|
1018
|
+
if (!incomingBlock) return {
|
|
1019
|
+
valid: false,
|
|
1020
|
+
reason: `Forbidden: Templated block ${existingBlock.type} (ID: ${existingBlock.id}) missing in payload.`
|
|
1021
|
+
};
|
|
1022
|
+
if (existingBlock.templatedProps === true) {
|
|
1023
|
+
if (JSON.stringify(existingBlock.props) !== JSON.stringify(incomingBlock.props)) return {
|
|
1024
|
+
valid: false,
|
|
1025
|
+
reason: `Forbidden: Cannot modify locked properties on ${existingBlock.type}.`
|
|
1026
|
+
};
|
|
1027
|
+
} else if (Array.isArray(existingBlock.templatedProps)) {
|
|
1028
|
+
for (const key of existingBlock.templatedProps) if (JSON.stringify(existingBlock.props[key]) !== JSON.stringify(incomingBlock.props[key])) return {
|
|
1029
|
+
valid: false,
|
|
1030
|
+
reason: `Forbidden: Cannot modify locked property '${key}' on ${existingBlock.type}.`
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (session) {
|
|
1035
|
+
const existingProtected = extractProtectedBlocks(existingTree);
|
|
1036
|
+
const incomingMap = new Map(flattenBlocks(incomingTree).map((b) => [b.id, b]));
|
|
1037
|
+
for (const existingBlock of existingProtected) {
|
|
1038
|
+
const incomingBlock = incomingMap.get(existingBlock.id);
|
|
1039
|
+
if (!canUserEditBlock(existingBlock, session)) {
|
|
1040
|
+
if (!incomingBlock) return {
|
|
1041
|
+
valid: false,
|
|
1042
|
+
reason: `Forbidden: Cannot delete protected block ${existingBlock.type} (ID: ${existingBlock.id}).`
|
|
1043
|
+
};
|
|
1044
|
+
if (JSON.stringify(existingBlock.props) !== JSON.stringify(incomingBlock.props)) return {
|
|
1045
|
+
valid: false,
|
|
1046
|
+
reason: `Forbidden: Cannot modify protected block ${existingBlock.type} (ID: ${existingBlock.id}).`
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return { valid: true };
|
|
1052
|
+
}
|
|
1053
|
+
function getChildren(b) {
|
|
1054
|
+
if (!b) return [];
|
|
1055
|
+
if (Array.isArray(b.props?.["children"])) return b.props["children"];
|
|
1056
|
+
if (Array.isArray(b.children)) return b.children;
|
|
1057
|
+
return [];
|
|
1058
|
+
}
|
|
1059
|
+
function flattenBlocks(blocks) {
|
|
1060
|
+
let result = [];
|
|
1061
|
+
for (const b of blocks) {
|
|
1062
|
+
if (!b) continue;
|
|
1063
|
+
result.push(b);
|
|
1064
|
+
const children = getChildren(b);
|
|
1065
|
+
if (children.length) result = result.concat(flattenBlocks(children));
|
|
1066
|
+
}
|
|
1067
|
+
return result;
|
|
1068
|
+
}
|
|
1069
|
+
function extractTemplatedBlocks(blocks) {
|
|
1070
|
+
let result = [];
|
|
1071
|
+
for (const b of blocks) {
|
|
1072
|
+
if (!b) continue;
|
|
1073
|
+
if (b.isTemplated) result.push(b);
|
|
1074
|
+
const children = getChildren(b);
|
|
1075
|
+
if (children.length) result = result.concat(extractTemplatedBlocks(children));
|
|
1076
|
+
}
|
|
1077
|
+
return result;
|
|
1078
|
+
}
|
|
1079
|
+
function extractProtectedBlocks(blocks) {
|
|
1080
|
+
return flattenBlocks(blocks).filter((b) => b.allowedRoles && b.allowedRoles.length > 0 || !!b.requiredPermission);
|
|
1081
|
+
}
|
|
1082
|
+
//#endregion
|
|
1083
|
+
//#region src/auth/filter-templates.ts
|
|
1084
|
+
function canUserCreateFromTemplate(template, session) {
|
|
1085
|
+
const allowedCreate = template.rolePermissions?.whoCanCreate?.length ? template.rolePermissions.whoCanCreate : template.allowedRoles;
|
|
1086
|
+
if (allowedCreate && allowedCreate.length > 0) {
|
|
1087
|
+
if (!session || !session.roles.some((r) => allowedCreate.includes(r))) return false;
|
|
1088
|
+
}
|
|
1089
|
+
if (template.requiredPermission) {
|
|
1090
|
+
if (!session || !session.permissions.includes(template.requiredPermission)) return false;
|
|
1091
|
+
}
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
function canUserEditFromTemplate(template, session) {
|
|
1095
|
+
const allowedEdit = template.rolePermissions?.whoCanEdit;
|
|
1096
|
+
if (allowedEdit && allowedEdit.length > 0) {
|
|
1097
|
+
if (!session || !session.roles.some((r) => allowedEdit.includes(r))) return false;
|
|
1098
|
+
}
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
function canUserReviewFromTemplate(template, session) {
|
|
1102
|
+
const allowedReview = template.rolePermissions?.whoCanReview?.length ? template.rolePermissions.whoCanReview : template.approvalRules?.requiredRolesForApproval;
|
|
1103
|
+
if (allowedReview && allowedReview.length > 0) {
|
|
1104
|
+
if (!session || !session.roles.some((r) => allowedReview.includes(r))) return false;
|
|
1105
|
+
}
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
function canUserViewFromTemplate(template, session) {
|
|
1109
|
+
const allowedView = template.rolePermissions?.whoCanView;
|
|
1110
|
+
if (allowedView && allowedView.length > 0) {
|
|
1111
|
+
if (!session || !session.roles.some((r) => allowedView.includes(r))) return false;
|
|
1112
|
+
}
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
function canUserInstantiateTemplate(template, session) {
|
|
1116
|
+
return canUserCreateFromTemplate(template, session);
|
|
1117
|
+
}
|
|
1118
|
+
function filterTemplatesForUser(templates, session) {
|
|
1119
|
+
return templates.filter((t) => canUserInstantiateTemplate(t, session));
|
|
1120
|
+
}
|
|
1121
|
+
//#endregion
|
|
1122
|
+
//#region src/cache/purge.ts
|
|
1123
|
+
async function invalidatePageCache(options) {
|
|
1124
|
+
const { slug, locales, domain = "rimelight.com", basePath = "", isEnterpriseCloudflare } = options;
|
|
1125
|
+
const zoneId = process.env["CLOUDFLARE_ZONE_ID"];
|
|
1126
|
+
const apiToken = process.env["CLOUDFLARE_API_TOKEN"];
|
|
1127
|
+
if (!zoneId || !apiToken) return false;
|
|
1128
|
+
const cleanPath = basePath ? `/${basePath.replace(/^\/+|\/+$/g, "")}` : "";
|
|
1129
|
+
const body = isEnterpriseCloudflare ? { tags: [`page-${domain}-${slug}`] } : { files: locales.map((lang) => `https://${domain}/${lang}${cleanPath}/${slug}`) };
|
|
1130
|
+
try {
|
|
1131
|
+
return (await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, {
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
headers: {
|
|
1134
|
+
"Authorization": `Bearer ${apiToken}`,
|
|
1135
|
+
"Content-Type": "application/json"
|
|
1136
|
+
},
|
|
1137
|
+
body: JSON.stringify(body)
|
|
1138
|
+
})).ok;
|
|
1139
|
+
} catch {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/search/query.ts
|
|
1145
|
+
const POSTGRES_SEARCH_CONFIGS = {
|
|
1146
|
+
en: "english",
|
|
1147
|
+
es: "spanish",
|
|
1148
|
+
de: "german",
|
|
1149
|
+
fr: "french"
|
|
1150
|
+
};
|
|
1151
|
+
function getPostgresSearchConfig(locale) {
|
|
1152
|
+
return POSTGRES_SEARCH_CONFIGS[locale] || "simple";
|
|
1153
|
+
}
|
|
1154
|
+
//#endregion
|
|
1155
|
+
//#region src/core/excerpt.ts
|
|
1156
|
+
function extractTextFromInlineNodes(nodes) {
|
|
1157
|
+
if (!nodes || !Array.isArray(nodes)) return "";
|
|
1158
|
+
return nodes.map((node) => {
|
|
1159
|
+
if (node.type === "text") return node.text || "";
|
|
1160
|
+
if (node.type === "page_mention") return node.displayTitle || node.pageSlug || "";
|
|
1161
|
+
if (node.type === "link") return extractTextFromInlineNodes(node.children);
|
|
1162
|
+
return "";
|
|
1163
|
+
}).join("");
|
|
1164
|
+
}
|
|
1165
|
+
function extractTextFromProperty(prop, locale = "en") {
|
|
1166
|
+
if (!prop) return "";
|
|
1167
|
+
if (typeof prop === "string") return prop;
|
|
1168
|
+
if (typeof prop === "object" && prop !== null) {
|
|
1169
|
+
if (Array.isArray(prop)) return extractTextFromInlineNodes(prop);
|
|
1170
|
+
const record = prop;
|
|
1171
|
+
if (typeof record[locale] === "string") return record[locale];
|
|
1172
|
+
if (typeof record["en"] === "string") return record["en"];
|
|
1173
|
+
const firstVal = Object.values(record)[0];
|
|
1174
|
+
if (typeof firstVal === "string") return firstVal;
|
|
1175
|
+
}
|
|
1176
|
+
return "";
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Recursively extracts plain text from an array of CMS blocks.
|
|
1180
|
+
*/
|
|
1181
|
+
function extractTextFromBlocks(blocks, locale = "en") {
|
|
1182
|
+
if (!blocks || !Array.isArray(blocks)) return "";
|
|
1183
|
+
const chunks = [];
|
|
1184
|
+
for (const block of blocks) {
|
|
1185
|
+
if (!block) continue;
|
|
1186
|
+
const props = block.props || {};
|
|
1187
|
+
switch (block.type) {
|
|
1188
|
+
case "ParagraphBlock": {
|
|
1189
|
+
const text = extractTextFromProperty(props["text"], locale);
|
|
1190
|
+
if (text) chunks.push(text);
|
|
1191
|
+
break;
|
|
1192
|
+
}
|
|
1193
|
+
case "SectionBlock":
|
|
1194
|
+
if (props["title"]) chunks.push(String(props["title"]));
|
|
1195
|
+
if (props["description"]) chunks.push(String(props["description"]));
|
|
1196
|
+
if (Array.isArray(props["children"])) {
|
|
1197
|
+
const childText = extractTextFromBlocks(props["children"], locale);
|
|
1198
|
+
if (childText) chunks.push(childText);
|
|
1199
|
+
}
|
|
1200
|
+
break;
|
|
1201
|
+
case "CardBlock":
|
|
1202
|
+
if (props["title"]) chunks.push(String(props["title"]));
|
|
1203
|
+
if (props["description"]) chunks.push(String(props["description"]));
|
|
1204
|
+
break;
|
|
1205
|
+
case "DialogueBlock":
|
|
1206
|
+
if (props["character"] && props["line"]) chunks.push(`${props["character"]}: ${props["line"]}`);
|
|
1207
|
+
else if (props["line"]) chunks.push(String(props["line"]));
|
|
1208
|
+
break;
|
|
1209
|
+
case "StepItemBlock":
|
|
1210
|
+
if (props["title"]) chunks.push(String(props["title"]));
|
|
1211
|
+
if (props["description"]) chunks.push(String(props["description"]));
|
|
1212
|
+
}
|
|
1213
|
+
if (Array.isArray(block.children) && block.children.length > 0) {
|
|
1214
|
+
const childText = extractTextFromBlocks(block.children, locale);
|
|
1215
|
+
if (childText) chunks.push(childText);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return chunks.join(" ").replace(/\s+/g, " ").trim();
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Generates an SEO / RSS safe plain text excerpt from a CMS page or block payload.
|
|
1222
|
+
*/
|
|
1223
|
+
function createPageExcerpt(page, options = {}) {
|
|
1224
|
+
const { maxLength = 160, locale = "en", fallback = "" } = options;
|
|
1225
|
+
if (!page) return fallback;
|
|
1226
|
+
const explicitDesc = extractTextFromProperty(page.description, locale);
|
|
1227
|
+
if (explicitDesc) return truncateExcerpt(explicitDesc, maxLength);
|
|
1228
|
+
let contentObj = page.content;
|
|
1229
|
+
if (typeof contentObj === "string") try {
|
|
1230
|
+
contentObj = JSON.parse(contentObj);
|
|
1231
|
+
} catch {
|
|
1232
|
+
contentObj = null;
|
|
1233
|
+
}
|
|
1234
|
+
if (contentObj && typeof contentObj === "object") {
|
|
1235
|
+
const props = contentObj.properties;
|
|
1236
|
+
if (props && props["description"]) {
|
|
1237
|
+
const descText = extractTextFromProperty(props["description"], locale);
|
|
1238
|
+
if (descText) return truncateExcerpt(descText, maxLength);
|
|
1239
|
+
}
|
|
1240
|
+
const blocks = contentObj.blocks;
|
|
1241
|
+
if (Array.isArray(blocks) && blocks.length > 0) {
|
|
1242
|
+
const extracted = extractTextFromBlocks(blocks, locale);
|
|
1243
|
+
if (extracted) return truncateExcerpt(extracted, maxLength);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return fallback;
|
|
1247
|
+
}
|
|
1248
|
+
function truncateExcerpt(text, maxLength) {
|
|
1249
|
+
const clean = text.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
|
|
1250
|
+
if (clean.length <= maxLength) return clean;
|
|
1251
|
+
return `${clean.slice(0, maxLength - 1).trimEnd()}…`;
|
|
1252
|
+
}
|
|
1253
|
+
//#endregion
|
|
1254
|
+
//#region src/services/site-settings.ts
|
|
1255
|
+
const DEFAULT_SITE_SETTINGS = {
|
|
1256
|
+
id: "default",
|
|
1257
|
+
name: "Rimelight",
|
|
1258
|
+
description: "Built with Rimelight CMS",
|
|
1259
|
+
url: "https://rimelight.com",
|
|
1260
|
+
ogImage: "https://cdn.rimelight.com/Images/og-placeholder.webp",
|
|
1261
|
+
author: "Rimelight",
|
|
1262
|
+
email: "",
|
|
1263
|
+
branding: {
|
|
1264
|
+
logo: { alt: "Rimelight" },
|
|
1265
|
+
favicon: { svg: "/favicon.svg" },
|
|
1266
|
+
colors: {
|
|
1267
|
+
themeColor: "#ffffff",
|
|
1268
|
+
backgroundColor: "#ffffff"
|
|
1269
|
+
}
|
|
1270
|
+
},
|
|
1271
|
+
seo: {
|
|
1272
|
+
titleTemplate: "%s | Rimelight",
|
|
1273
|
+
ogImageFallback: "https://cdn.rimelight.com/Images/og-placeholder.webp",
|
|
1274
|
+
maxDescriptionLength: 160
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
/**
|
|
1278
|
+
* Retrieves the dynamic site settings from the database. If no settings are found, seeds the
|
|
1279
|
+
* default record and returns it.
|
|
1280
|
+
*/
|
|
1281
|
+
async function getSiteSettings(db, fallbackDefaults) {
|
|
1282
|
+
if (!db) return {
|
|
1283
|
+
...DEFAULT_SITE_SETTINGS,
|
|
1284
|
+
...fallbackDefaults,
|
|
1285
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1286
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1287
|
+
};
|
|
1288
|
+
try {
|
|
1289
|
+
const records = await db.select().from(siteSettings).where(eq(siteSettings.id, "default")).limit(1);
|
|
1290
|
+
if (records.length > 0) return records[0];
|
|
1291
|
+
const initial = {
|
|
1292
|
+
...DEFAULT_SITE_SETTINGS,
|
|
1293
|
+
...fallbackDefaults,
|
|
1294
|
+
id: "default"
|
|
1295
|
+
};
|
|
1296
|
+
return (await db.insert(siteSettings).values(initial).returning())[0];
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
console.warn("Failed to fetch site settings from DB, using fallback:", error);
|
|
1299
|
+
return {
|
|
1300
|
+
...DEFAULT_SITE_SETTINGS,
|
|
1301
|
+
...fallbackDefaults,
|
|
1302
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1303
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Updates dynamic site settings in the database.
|
|
1309
|
+
*/
|
|
1310
|
+
async function updateSiteSettings(db, updates, id = "default") {
|
|
1311
|
+
const current = await getSiteSettings(db);
|
|
1312
|
+
const updatedValues = {
|
|
1313
|
+
...updates,
|
|
1314
|
+
branding: updates.branding ? {
|
|
1315
|
+
...current.branding,
|
|
1316
|
+
...updates.branding
|
|
1317
|
+
} : current.branding,
|
|
1318
|
+
seo: updates.seo ? {
|
|
1319
|
+
...current.seo,
|
|
1320
|
+
...updates.seo
|
|
1321
|
+
} : current.seo,
|
|
1322
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1323
|
+
};
|
|
1324
|
+
return (await db.update(siteSettings).set(updatedValues).where(eq(siteSettings.id, id)).returning())[0];
|
|
1325
|
+
}
|
|
1326
|
+
//#endregion
|
|
1327
|
+
//#region src/loader/live.ts
|
|
1328
|
+
function resolveLocalized$1(val, locale = "en") {
|
|
1329
|
+
if (!val) return "";
|
|
1330
|
+
if (typeof val === "string") return val;
|
|
1331
|
+
if (typeof val === "object" && val !== null) {
|
|
1332
|
+
const rec = val;
|
|
1333
|
+
return rec[locale] || rec["en"] || Object.values(rec)[0] || "";
|
|
1334
|
+
}
|
|
1335
|
+
return typeof val === "number" ? String(val) : "";
|
|
1336
|
+
}
|
|
1337
|
+
function transformPageToEntry(rawPage, locale = "en") {
|
|
1338
|
+
let contentObj = rawPage.content;
|
|
1339
|
+
if (typeof contentObj === "string") try {
|
|
1340
|
+
contentObj = JSON.parse(contentObj);
|
|
1341
|
+
} catch {
|
|
1342
|
+
contentObj = {
|
|
1343
|
+
blocks: [],
|
|
1344
|
+
properties: {}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
else if (!contentObj || typeof contentObj !== "object") contentObj = {
|
|
1348
|
+
blocks: [],
|
|
1349
|
+
properties: {}
|
|
1350
|
+
};
|
|
1351
|
+
const blocks = Array.isArray(contentObj.blocks) ? contentObj.blocks : [];
|
|
1352
|
+
const properties = contentObj.properties || {};
|
|
1353
|
+
const resolvedTitle = resolveLocalized$1(rawPage.title, locale);
|
|
1354
|
+
const resolvedDescription = resolveLocalized$1(rawPage.description, locale);
|
|
1355
|
+
const excerpt = createPageExcerpt(rawPage, {
|
|
1356
|
+
locale,
|
|
1357
|
+
maxLength: 160
|
|
1358
|
+
});
|
|
1359
|
+
return {
|
|
1360
|
+
id: rawPage.slug || rawPage.id,
|
|
1361
|
+
data: {
|
|
1362
|
+
id: rawPage.id,
|
|
1363
|
+
slug: rawPage.slug,
|
|
1364
|
+
type: rawPage.type,
|
|
1365
|
+
title: rawPage.title,
|
|
1366
|
+
resolvedTitle,
|
|
1367
|
+
description: rawPage.description,
|
|
1368
|
+
resolvedDescription,
|
|
1369
|
+
excerpt,
|
|
1370
|
+
tags: rawPage.tags || [],
|
|
1371
|
+
authorIds: rawPage.authorIds || [],
|
|
1372
|
+
blocks,
|
|
1373
|
+
properties,
|
|
1374
|
+
publishedVersionId: rawPage.publishedVersionId,
|
|
1375
|
+
postedAt: rawPage.postedAt ? new Date(rawPage.postedAt) : null,
|
|
1376
|
+
createdAt: new Date(rawPage.createdAt),
|
|
1377
|
+
updatedAt: rawPage.updatedAt ? new Date(rawPage.updatedAt) : null
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Direct querying helper for CMS collections.
|
|
1383
|
+
*/
|
|
1384
|
+
async function getCmsCollection(db, options = {}) {
|
|
1385
|
+
const { type, locale = "en", includeDrafts = false, includeScheduled = false, limit, offset, where: customWhere } = options;
|
|
1386
|
+
if (!db) return {
|
|
1387
|
+
entries: [],
|
|
1388
|
+
cacheHint: { tags: ["cms"] }
|
|
1389
|
+
};
|
|
1390
|
+
const conditions = [isNull(pages.deletedAt)];
|
|
1391
|
+
if (type) conditions.push(eq(pages.type, type));
|
|
1392
|
+
if (!includeDrafts) conditions.push(isNotNull(pages.publishedVersionId));
|
|
1393
|
+
if (!includeScheduled) conditions.push(lte(pages.postedAt, /* @__PURE__ */ new Date()));
|
|
1394
|
+
if (customWhere) conditions.push(customWhere);
|
|
1395
|
+
if (options.taxonomy) {
|
|
1396
|
+
for (const [taxName, termSlug] of Object.entries(options.taxonomy)) if (termSlug) conditions.push(sql`${pages.id} IN (
|
|
1397
|
+
SELECT ${pageTaxonomyTerms.pageId} FROM ${pageTaxonomyTerms}
|
|
1398
|
+
INNER JOIN ${taxonomyTerms} ON ${taxonomyTerms.id} = ${pageTaxonomyTerms.termId}
|
|
1399
|
+
WHERE ${taxonomyTerms.taxonomy} = ${taxName} AND ${taxonomyTerms.slug} = ${termSlug}
|
|
1400
|
+
)`);
|
|
1401
|
+
}
|
|
1402
|
+
let query = db.select().from(pages).where(and(...conditions)).orderBy(desc(pages.postedAt));
|
|
1403
|
+
if (typeof limit === "number") query = query.limit(limit);
|
|
1404
|
+
if (typeof offset === "number") query = query.offset(offset);
|
|
1405
|
+
const entries = (await query).map((r) => transformPageToEntry(r, locale));
|
|
1406
|
+
let lastModified = /* @__PURE__ */ new Date(0);
|
|
1407
|
+
for (const entry of entries) if (entry.data.updatedAt && entry.data.updatedAt > lastModified) lastModified = entry.data.updatedAt;
|
|
1408
|
+
const tags = ["cms"];
|
|
1409
|
+
if (type) tags.push(`cms:${type}`);
|
|
1410
|
+
return {
|
|
1411
|
+
entries,
|
|
1412
|
+
cacheHint: {
|
|
1413
|
+
tags,
|
|
1414
|
+
lastModified: lastModified.getTime() > 0 ? lastModified : /* @__PURE__ */ new Date()
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Direct querying helper for a single CMS entry by slug or id.
|
|
1420
|
+
*/
|
|
1421
|
+
async function getCmsEntry(db, slugOrId, options = {}) {
|
|
1422
|
+
const { entries, cacheHint } = await getCmsCollection(db, {
|
|
1423
|
+
...options,
|
|
1424
|
+
limit: 1,
|
|
1425
|
+
where: eq(pages.slug, slugOrId)
|
|
1426
|
+
});
|
|
1427
|
+
const entry = entries[0] || null;
|
|
1428
|
+
if (entry) cacheHint.tags?.push(`page:${entry.data.slug}`);
|
|
1429
|
+
return {
|
|
1430
|
+
entry,
|
|
1431
|
+
cacheHint
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Astro Live Loader for CMS collections. Use inside src/live.config.ts with defineLiveCollection({
|
|
1436
|
+
* loader: rimelightCmsLoader({ type: 'blog' }) })
|
|
1437
|
+
*/
|
|
1438
|
+
function rimelightCmsLoader(loaderOptions = {}) {
|
|
1439
|
+
return {
|
|
1440
|
+
name: "rimelight-cms-loader",
|
|
1441
|
+
loadCollection: async (context) => {
|
|
1442
|
+
const db = loaderOptions.db || globalThis.db;
|
|
1443
|
+
const limit = typeof context.filter?.limit === "number" ? context.filter.limit : void 0;
|
|
1444
|
+
const offset = typeof context.filter?.offset === "number" ? context.filter.offset : void 0;
|
|
1445
|
+
return getCmsCollection(db, {
|
|
1446
|
+
...loaderOptions,
|
|
1447
|
+
limit,
|
|
1448
|
+
offset
|
|
1449
|
+
});
|
|
1450
|
+
},
|
|
1451
|
+
loadEntry: async (context) => {
|
|
1452
|
+
const { entry, cacheHint } = await getCmsEntry(loaderOptions.db || globalThis.db, context.filter.id, loaderOptions);
|
|
1453
|
+
if (!entry) return void 0;
|
|
1454
|
+
return {
|
|
1455
|
+
id: entry.id,
|
|
1456
|
+
data: entry.data,
|
|
1457
|
+
cacheHint
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
//#endregion
|
|
1463
|
+
//#region src/cron/scheduled.ts
|
|
1464
|
+
/**
|
|
1465
|
+
* Handles the periodic Cloudflare Worker cron trigger (e.g. "* * * * *").
|
|
1466
|
+
*
|
|
1467
|
+
* - Cleans up expired page draft locks
|
|
1468
|
+
* - Checks for active scheduled pages
|
|
1469
|
+
*/
|
|
1470
|
+
async function handleCmsCron(_event, _env, _ctx, db) {
|
|
1471
|
+
const result = {
|
|
1472
|
+
expiredLocksCleaned: 0,
|
|
1473
|
+
scheduledPagesActive: 0,
|
|
1474
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1475
|
+
};
|
|
1476
|
+
if (!db) return result;
|
|
1477
|
+
try {
|
|
1478
|
+
const now = /* @__PURE__ */ new Date();
|
|
1479
|
+
result.expiredLocksCleaned = (await db.delete(pageDraftLocks).where(lt(pageDraftLocks.expiresAt, now)).returning({ pageId: pageDraftLocks.pageId })).length;
|
|
1480
|
+
result.scheduledPagesActive = (await db.select({
|
|
1481
|
+
id: pages.id,
|
|
1482
|
+
slug: pages.slug
|
|
1483
|
+
}).from(pages).where(and(isNull(pages.deletedAt), isNotNull(pages.publishedVersionId), lte(pages.postedAt, now))).limit(10)).length;
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
console.error("[CmsCron] Error executing scheduled tasks:", error);
|
|
1486
|
+
}
|
|
1487
|
+
return result;
|
|
1488
|
+
}
|
|
1489
|
+
//#endregion
|
|
1490
|
+
//#region src/mcp/index.ts
|
|
1491
|
+
const CMS_MCP_TOOLS = [
|
|
1492
|
+
{
|
|
1493
|
+
name: "cms_list_pages",
|
|
1494
|
+
description: "List CMS pages with optional filters for type, draft status, and pagination.",
|
|
1495
|
+
inputSchema: {
|
|
1496
|
+
type: "object",
|
|
1497
|
+
properties: {
|
|
1498
|
+
type: {
|
|
1499
|
+
type: "string",
|
|
1500
|
+
description: "Filter by page type (e.g. 'blog', 'docs', 'landing')"
|
|
1501
|
+
},
|
|
1502
|
+
includeDrafts: {
|
|
1503
|
+
type: "boolean",
|
|
1504
|
+
description: "Whether to include unpublished drafts"
|
|
1505
|
+
},
|
|
1506
|
+
limit: {
|
|
1507
|
+
type: "number",
|
|
1508
|
+
description: "Maximum number of pages to return (default 20)"
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
name: "cms_get_page",
|
|
1515
|
+
description: "Get full details, block tree, and properties for a specific CMS page by slug or id.",
|
|
1516
|
+
inputSchema: {
|
|
1517
|
+
type: "object",
|
|
1518
|
+
properties: { slugOrId: {
|
|
1519
|
+
type: "string",
|
|
1520
|
+
description: "Page slug or UUID"
|
|
1521
|
+
} },
|
|
1522
|
+
required: ["slugOrId"]
|
|
1523
|
+
}
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
name: "cms_create_draft",
|
|
1527
|
+
description: "Create or update a draft version for a CMS page.",
|
|
1528
|
+
inputSchema: {
|
|
1529
|
+
type: "object",
|
|
1530
|
+
properties: {
|
|
1531
|
+
pageId: {
|
|
1532
|
+
type: "string",
|
|
1533
|
+
description: "Page UUID"
|
|
1534
|
+
},
|
|
1535
|
+
userId: {
|
|
1536
|
+
type: "string",
|
|
1537
|
+
description: "User ID creating the draft"
|
|
1538
|
+
},
|
|
1539
|
+
blocks: {
|
|
1540
|
+
type: "array",
|
|
1541
|
+
description: "Array of structured CMS blocks"
|
|
1542
|
+
},
|
|
1543
|
+
properties: {
|
|
1544
|
+
type: "object",
|
|
1545
|
+
description: "Page properties object"
|
|
1546
|
+
}
|
|
1547
|
+
},
|
|
1548
|
+
required: [
|
|
1549
|
+
"pageId",
|
|
1550
|
+
"userId",
|
|
1551
|
+
"blocks"
|
|
1552
|
+
]
|
|
1553
|
+
}
|
|
1554
|
+
},
|
|
1555
|
+
{
|
|
1556
|
+
name: "cms_get_site_settings",
|
|
1557
|
+
description: "Retrieve current dynamic site settings including branding and SEO defaults.",
|
|
1558
|
+
inputSchema: {
|
|
1559
|
+
type: "object",
|
|
1560
|
+
properties: {}
|
|
1561
|
+
}
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
name: "cms_update_site_settings",
|
|
1565
|
+
description: "Update dynamic site settings (name, description, branding, SEO).",
|
|
1566
|
+
inputSchema: {
|
|
1567
|
+
type: "object",
|
|
1568
|
+
properties: {
|
|
1569
|
+
name: { type: "string" },
|
|
1570
|
+
description: { type: "string" },
|
|
1571
|
+
author: { type: "string" },
|
|
1572
|
+
email: { type: "string" },
|
|
1573
|
+
branding: { type: "object" },
|
|
1574
|
+
seo: { type: "object" }
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
];
|
|
1579
|
+
/**
|
|
1580
|
+
* Handles Model Context Protocol (MCP) JSON-RPC requests for CMS tools.
|
|
1581
|
+
*/
|
|
1582
|
+
async function handleCmsMcpRequest(body, db) {
|
|
1583
|
+
const { id = 1, method, params = {} } = body;
|
|
1584
|
+
if (method === "tools/list") return {
|
|
1585
|
+
jsonrpc: "2.0",
|
|
1586
|
+
id,
|
|
1587
|
+
result: { tools: CMS_MCP_TOOLS }
|
|
1588
|
+
};
|
|
1589
|
+
if (method === "tools/call") {
|
|
1590
|
+
const { name, arguments: args = {} } = params;
|
|
1591
|
+
switch (name) {
|
|
1592
|
+
case "cms_list_pages": {
|
|
1593
|
+
const conditions = [isNull(pages.deletedAt)];
|
|
1594
|
+
if (args.type) conditions.push(eq(pages.type, args.type));
|
|
1595
|
+
if (!args.includeDrafts) conditions.push(isNotNull(pages.publishedVersionId));
|
|
1596
|
+
const queryLimit = typeof args.limit === "number" ? args.limit : 20;
|
|
1597
|
+
const rows = await db.select({
|
|
1598
|
+
id: pages.id,
|
|
1599
|
+
slug: pages.slug,
|
|
1600
|
+
type: pages.type,
|
|
1601
|
+
title: pages.title,
|
|
1602
|
+
postedAt: pages.postedAt,
|
|
1603
|
+
publishedVersionId: pages.publishedVersionId
|
|
1604
|
+
}).from(pages).where(and(...conditions)).orderBy(desc(pages.postedAt)).limit(queryLimit);
|
|
1605
|
+
return {
|
|
1606
|
+
jsonrpc: "2.0",
|
|
1607
|
+
id,
|
|
1608
|
+
result: { content: [{
|
|
1609
|
+
type: "text",
|
|
1610
|
+
text: JSON.stringify(rows, null, 2)
|
|
1611
|
+
}] }
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
case "cms_get_page": {
|
|
1615
|
+
const page = (await db.select().from(pages).where(and(isNull(pages.deletedAt), eq(pages.slug, args.slugOrId))).limit(1))[0] || null;
|
|
1616
|
+
return {
|
|
1617
|
+
jsonrpc: "2.0",
|
|
1618
|
+
id,
|
|
1619
|
+
result: { content: [{
|
|
1620
|
+
type: "text",
|
|
1621
|
+
text: JSON.stringify(page, null, 2)
|
|
1622
|
+
}] }
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
case "cms_create_draft": {
|
|
1626
|
+
const contentPayload = {
|
|
1627
|
+
blocks: args.blocks,
|
|
1628
|
+
properties: args.properties || {}
|
|
1629
|
+
};
|
|
1630
|
+
const draft = await db.insert(pageDrafts).values({
|
|
1631
|
+
pageId: args.pageId,
|
|
1632
|
+
updatedBy: args.userId,
|
|
1633
|
+
content: contentPayload
|
|
1634
|
+
}).onConflictDoUpdate({
|
|
1635
|
+
target: [pageDrafts.pageId],
|
|
1636
|
+
set: {
|
|
1637
|
+
content: contentPayload,
|
|
1638
|
+
updatedBy: args.userId,
|
|
1639
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1640
|
+
}
|
|
1641
|
+
}).returning();
|
|
1642
|
+
return {
|
|
1643
|
+
jsonrpc: "2.0",
|
|
1644
|
+
id,
|
|
1645
|
+
result: { content: [{
|
|
1646
|
+
type: "text",
|
|
1647
|
+
text: JSON.stringify(draft[0], null, 2)
|
|
1648
|
+
}] }
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
case "cms_get_site_settings": {
|
|
1652
|
+
const settings = await getSiteSettings(db);
|
|
1653
|
+
return {
|
|
1654
|
+
jsonrpc: "2.0",
|
|
1655
|
+
id,
|
|
1656
|
+
result: { content: [{
|
|
1657
|
+
type: "text",
|
|
1658
|
+
text: JSON.stringify(settings, null, 2)
|
|
1659
|
+
}] }
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
case "cms_update_site_settings": {
|
|
1663
|
+
const updated = await updateSiteSettings(db, args);
|
|
1664
|
+
return {
|
|
1665
|
+
jsonrpc: "2.0",
|
|
1666
|
+
id,
|
|
1667
|
+
result: { content: [{
|
|
1668
|
+
type: "text",
|
|
1669
|
+
text: JSON.stringify(updated, null, 2)
|
|
1670
|
+
}] }
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
default: return {
|
|
1674
|
+
jsonrpc: "2.0",
|
|
1675
|
+
id,
|
|
1676
|
+
error: {
|
|
1677
|
+
code: -32601,
|
|
1678
|
+
message: `Method or tool '${name}' not found`
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return {
|
|
1684
|
+
jsonrpc: "2.0",
|
|
1685
|
+
id,
|
|
1686
|
+
error: {
|
|
1687
|
+
code: -32601,
|
|
1688
|
+
message: `Unknown method: ${method}`
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
//#endregion
|
|
1693
|
+
//#region src/services/bylines.ts
|
|
1694
|
+
/**
|
|
1695
|
+
* Returns all active (non-deleted) bylines.
|
|
1696
|
+
*/
|
|
1697
|
+
async function getBylines(db) {
|
|
1698
|
+
if (!db) return [];
|
|
1699
|
+
return db.select().from(bylines).where(isNull(bylines.deletedAt)).orderBy(bylines.name);
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Retrieves a single byline by ID or slug.
|
|
1703
|
+
*/
|
|
1704
|
+
async function getByline(db, idOrSlug) {
|
|
1705
|
+
if (!db || !idOrSlug) return null;
|
|
1706
|
+
return (await db.select().from(bylines).where(and(isNull(bylines.deletedAt), or(eq(bylines.id, idOrSlug), eq(bylines.slug, idOrSlug)))).limit(1))[0] || null;
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Creates a new byline.
|
|
1710
|
+
*/
|
|
1711
|
+
async function createByline(db, data) {
|
|
1712
|
+
const [created] = await db.insert(bylines).values({
|
|
1713
|
+
...data,
|
|
1714
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1715
|
+
}).returning();
|
|
1716
|
+
return created;
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Updates an existing byline.
|
|
1720
|
+
*/
|
|
1721
|
+
async function updateByline(db, id, data) {
|
|
1722
|
+
const [updated] = await db.update(bylines).set({
|
|
1723
|
+
...data,
|
|
1724
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1725
|
+
}).where(and(eq(bylines.id, id), isNull(bylines.deletedAt))).returning();
|
|
1726
|
+
return updated || null;
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Soft deletes a byline.
|
|
1730
|
+
*/
|
|
1731
|
+
async function deleteByline(db, id) {
|
|
1732
|
+
const [deleted] = await db.update(bylines).set({
|
|
1733
|
+
deletedAt: /* @__PURE__ */ new Date(),
|
|
1734
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1735
|
+
}).where(and(eq(bylines.id, id), isNull(bylines.deletedAt))).returning();
|
|
1736
|
+
return !!deleted;
|
|
1737
|
+
}
|
|
1738
|
+
//#endregion
|
|
1739
|
+
//#region src/core/preview.ts
|
|
1740
|
+
/**
|
|
1741
|
+
* Generates and verifies HMAC-signed stateless preview tokens for draft pages.
|
|
1742
|
+
*/
|
|
1743
|
+
async function getCryptoKey(secret) {
|
|
1744
|
+
const enc = new TextEncoder();
|
|
1745
|
+
return crypto.subtle.importKey("raw", enc.encode(secret), {
|
|
1746
|
+
name: "HMAC",
|
|
1747
|
+
hash: "SHA-256"
|
|
1748
|
+
}, false, ["sign", "verify"]);
|
|
1749
|
+
}
|
|
1750
|
+
function hexEncode(buffer) {
|
|
1751
|
+
const bytes = new Uint8Array(buffer);
|
|
1752
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1753
|
+
}
|
|
1754
|
+
function hexDecode(hex) {
|
|
1755
|
+
const matches = hex.match(/.{1,2}/g) || [];
|
|
1756
|
+
return new Uint8Array(matches.map((byte) => parseInt(byte, 16)));
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Generates a signed token for a page ID valid for ttlSeconds (default 24h).
|
|
1760
|
+
*/
|
|
1761
|
+
async function generatePreviewToken(pageId, secret, ttlSeconds = 86400) {
|
|
1762
|
+
const expiresAt = Math.floor(Date.now() / 1e3) + ttlSeconds;
|
|
1763
|
+
const payload = `${pageId}:${expiresAt}`;
|
|
1764
|
+
const key = await getCryptoKey(secret);
|
|
1765
|
+
const enc = new TextEncoder();
|
|
1766
|
+
const signature = hexEncode(await crypto.subtle.sign("HMAC", key, enc.encode(payload)));
|
|
1767
|
+
return {
|
|
1768
|
+
token: `${btoa(payload)}.${signature}`,
|
|
1769
|
+
expiresAt
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Verifies a signed preview token against a pageId and secret.
|
|
1774
|
+
*/
|
|
1775
|
+
async function verifyPreviewToken(pageId, token, secret) {
|
|
1776
|
+
try {
|
|
1777
|
+
const parts = token.split(".");
|
|
1778
|
+
if (parts.length !== 2) return false;
|
|
1779
|
+
const [b64Payload, signatureHex] = parts;
|
|
1780
|
+
if (!b64Payload || !signatureHex) return false;
|
|
1781
|
+
const payload = atob(b64Payload);
|
|
1782
|
+
const [tokenPageId, tokenExpStr] = payload.split(":");
|
|
1783
|
+
if (!tokenPageId || !tokenExpStr) return false;
|
|
1784
|
+
if (tokenPageId !== pageId) return false;
|
|
1785
|
+
const expiresAt = parseInt(tokenExpStr, 10);
|
|
1786
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1787
|
+
if (isNaN(expiresAt) || now > expiresAt) return false;
|
|
1788
|
+
const key = await getCryptoKey(secret);
|
|
1789
|
+
const enc = new TextEncoder();
|
|
1790
|
+
const signatureBytes = hexDecode(signatureHex);
|
|
1791
|
+
return await crypto.subtle.verify("HMAC", key, signatureBytes, enc.encode(payload));
|
|
1792
|
+
} catch {
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
//#endregion
|
|
1797
|
+
//#region src/core/diff.ts
|
|
1798
|
+
/**
|
|
1799
|
+
* Calculates a detailed diff between an old list of blocks and a new list of blocks.
|
|
1800
|
+
*/
|
|
1801
|
+
function diffBlocks(oldBlocks = [], newBlocks = []) {
|
|
1802
|
+
const diffs = [];
|
|
1803
|
+
const oldMap = /* @__PURE__ */ new Map();
|
|
1804
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1805
|
+
oldBlocks.forEach((block, index) => {
|
|
1806
|
+
oldMap.set(block.id, {
|
|
1807
|
+
block,
|
|
1808
|
+
index
|
|
1809
|
+
});
|
|
1810
|
+
});
|
|
1811
|
+
newBlocks.forEach((block, index) => {
|
|
1812
|
+
newMap.set(block.id, {
|
|
1813
|
+
block,
|
|
1814
|
+
index
|
|
1815
|
+
});
|
|
1816
|
+
});
|
|
1817
|
+
for (const newBlock of newBlocks) {
|
|
1818
|
+
const oldEntry = oldMap.get(newBlock.id);
|
|
1819
|
+
if (!oldEntry) diffs.push({
|
|
1820
|
+
blockId: newBlock.id,
|
|
1821
|
+
type: newBlock.type,
|
|
1822
|
+
status: "added",
|
|
1823
|
+
newBlock
|
|
1824
|
+
});
|
|
1825
|
+
else {
|
|
1826
|
+
const oldBlock = oldEntry.block;
|
|
1827
|
+
const fieldChanges = [];
|
|
1828
|
+
const oldProps = oldBlock.props || oldBlock.properties || {};
|
|
1829
|
+
const newProps = newBlock.props || newBlock.properties || {};
|
|
1830
|
+
const allPropKeys = /* @__PURE__ */ new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
|
|
1831
|
+
for (const key of allPropKeys) {
|
|
1832
|
+
const oldVal = oldProps[key];
|
|
1833
|
+
const newVal = newProps[key];
|
|
1834
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) fieldChanges.push({
|
|
1835
|
+
field: `props.${key}`,
|
|
1836
|
+
oldValue: oldVal,
|
|
1837
|
+
newValue: newVal
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
if (fieldChanges.length > 0) diffs.push({
|
|
1841
|
+
blockId: newBlock.id,
|
|
1842
|
+
type: newBlock.type,
|
|
1843
|
+
status: "modified",
|
|
1844
|
+
oldBlock,
|
|
1845
|
+
newBlock,
|
|
1846
|
+
fieldChanges
|
|
1847
|
+
});
|
|
1848
|
+
else diffs.push({
|
|
1849
|
+
blockId: newBlock.id,
|
|
1850
|
+
type: newBlock.type,
|
|
1851
|
+
status: "unchanged",
|
|
1852
|
+
oldBlock,
|
|
1853
|
+
newBlock
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
for (const oldBlock of oldBlocks) if (!newMap.has(oldBlock.id)) diffs.push({
|
|
1858
|
+
blockId: oldBlock.id,
|
|
1859
|
+
type: oldBlock.type,
|
|
1860
|
+
status: "removed",
|
|
1861
|
+
oldBlock
|
|
1862
|
+
});
|
|
1863
|
+
return diffs;
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Compares two page snapshots (metadata + content blocks).
|
|
1867
|
+
*/
|
|
1868
|
+
function diffPageSnapshots(oldSnapshot, newSnapshot) {
|
|
1869
|
+
const blockDiffs = diffBlocks(oldSnapshot?.content?.blocks || [], newSnapshot?.content?.blocks || []);
|
|
1870
|
+
const metaFields = [
|
|
1871
|
+
"title",
|
|
1872
|
+
"slug",
|
|
1873
|
+
"description",
|
|
1874
|
+
"tags",
|
|
1875
|
+
"type",
|
|
1876
|
+
"authorIds",
|
|
1877
|
+
"bylines"
|
|
1878
|
+
];
|
|
1879
|
+
const metaChanges = [];
|
|
1880
|
+
for (const field of metaFields) {
|
|
1881
|
+
const oldVal = oldSnapshot?.[field];
|
|
1882
|
+
const newVal = newSnapshot?.[field];
|
|
1883
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) metaChanges.push({
|
|
1884
|
+
field,
|
|
1885
|
+
oldValue: oldVal,
|
|
1886
|
+
newValue: newVal
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
return {
|
|
1890
|
+
blocks: blockDiffs,
|
|
1891
|
+
metaChanges,
|
|
1892
|
+
summary: {
|
|
1893
|
+
addedCount: blockDiffs.filter((d) => d.status === "added").length,
|
|
1894
|
+
removedCount: blockDiffs.filter((d) => d.status === "removed").length,
|
|
1895
|
+
modifiedCount: blockDiffs.filter((d) => d.status === "modified").length,
|
|
1896
|
+
unchangedCount: blockDiffs.filter((d) => d.status === "unchanged").length
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
//#endregion
|
|
1901
|
+
//#region src/services/search-indexer.ts
|
|
1902
|
+
async function indexPageForSearch(db, page, locales = ["en", "pt"]) {
|
|
1903
|
+
if (!db) return;
|
|
1904
|
+
const parseJson = (val) => typeof val === "string" ? JSON.parse(val) : val;
|
|
1905
|
+
const titleObj = parseJson(page.title) || {};
|
|
1906
|
+
const blocks = (parseJson(page.content) || {}).blocks || [];
|
|
1907
|
+
for (const locale of locales) {
|
|
1908
|
+
const titleText = typeof titleObj === "object" && titleObj !== null ? titleObj[locale] || titleObj.en || Object.values(titleObj)[0] || "" : String(titleObj || "");
|
|
1909
|
+
const contentText = extractTextFromBlocks(blocks, locale);
|
|
1910
|
+
if (!titleText && !contentText) continue;
|
|
1911
|
+
try {
|
|
1912
|
+
const existing = await db.select().from(contentSearchIndex).where(and(eq(contentSearchIndex.pageId, page.id), eq(contentSearchIndex.locale, locale))).limit(1);
|
|
1913
|
+
if (existing.length > 0) await db.update(contentSearchIndex).set({
|
|
1914
|
+
titleText,
|
|
1915
|
+
contentText,
|
|
1916
|
+
searchVector: sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`
|
|
1917
|
+
}).where(eq(contentSearchIndex.id, existing[0].id));
|
|
1918
|
+
else await db.insert(contentSearchIndex).values({
|
|
1919
|
+
pageId: page.id,
|
|
1920
|
+
locale,
|
|
1921
|
+
titleText,
|
|
1922
|
+
contentText,
|
|
1923
|
+
searchVector: sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`
|
|
1924
|
+
});
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
console.error(`[Search Indexer] Failed to index page ${page.id} for locale ${locale}:`, err);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
//#endregion
|
|
1931
|
+
//#region src/services/taxonomies.ts
|
|
1932
|
+
/**
|
|
1933
|
+
* Retrieves all terms for a given taxonomy. If hierarchical, returns a tree structure with
|
|
1934
|
+
* `children: TaxonomyTerm[]`.
|
|
1935
|
+
*/
|
|
1936
|
+
async function getTaxonomyTerms(db, taxonomy, options = {}) {
|
|
1937
|
+
if (!db) return [];
|
|
1938
|
+
const includeCounts = options.includeCounts ?? true;
|
|
1939
|
+
const rawTerms = await db.select().from(taxonomyTerms).where(eq(taxonomyTerms.taxonomy, taxonomy)).orderBy(asc(taxonomyTerms.displayOrder), asc(taxonomyTerms.createdAt));
|
|
1940
|
+
if (!rawTerms || rawTerms.length === 0) return [];
|
|
1941
|
+
const countsMap = /* @__PURE__ */ new Map();
|
|
1942
|
+
if (includeCounts) try {
|
|
1943
|
+
const counts = await db.select({
|
|
1944
|
+
termId: pageTaxonomyTerms.termId,
|
|
1945
|
+
count: sql`count(${pageTaxonomyTerms.pageId})::int`
|
|
1946
|
+
}).from(pageTaxonomyTerms).innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId)).where(isNull(pages.deletedAt)).groupBy(pageTaxonomyTerms.termId);
|
|
1947
|
+
for (const row of counts) countsMap.set(row.termId, Number(row.count));
|
|
1948
|
+
} catch {}
|
|
1949
|
+
const termsWithCounts = rawTerms.map((t) => ({
|
|
1950
|
+
id: t.id,
|
|
1951
|
+
taxonomy: t.taxonomy,
|
|
1952
|
+
slug: t.slug,
|
|
1953
|
+
label: t.label,
|
|
1954
|
+
description: t.description,
|
|
1955
|
+
parentId: t.parentId,
|
|
1956
|
+
displayOrder: t.displayOrder,
|
|
1957
|
+
count: countsMap.get(t.id) ?? 0,
|
|
1958
|
+
children: [],
|
|
1959
|
+
createdAt: t.createdAt,
|
|
1960
|
+
updatedAt: t.updatedAt
|
|
1961
|
+
}));
|
|
1962
|
+
if (!termsWithCounts.some((t) => !!t.parentId)) return termsWithCounts;
|
|
1963
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1964
|
+
const rootTerms = [];
|
|
1965
|
+
termsWithCounts.forEach((term) => {
|
|
1966
|
+
idMap.set(term.id, term);
|
|
1967
|
+
});
|
|
1968
|
+
termsWithCounts.forEach((term) => {
|
|
1969
|
+
if (term.parentId && idMap.has(term.parentId)) {
|
|
1970
|
+
const parent = idMap.get(term.parentId);
|
|
1971
|
+
parent.children = parent.children || [];
|
|
1972
|
+
parent.children.push(term);
|
|
1973
|
+
} else rootTerms.push(term);
|
|
1974
|
+
});
|
|
1975
|
+
return rootTerms;
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Retrieves a single term by taxonomy and slug.
|
|
1979
|
+
*/
|
|
1980
|
+
async function getTerm(db, taxonomy, slug) {
|
|
1981
|
+
if (!db) return null;
|
|
1982
|
+
const results = await db.select().from(taxonomyTerms).where(and(eq(taxonomyTerms.taxonomy, taxonomy), eq(taxonomyTerms.slug, slug))).limit(1);
|
|
1983
|
+
if (!results[0]) return null;
|
|
1984
|
+
let count = 0;
|
|
1985
|
+
try {
|
|
1986
|
+
const countResult = await db.select({ count: sql`count(${pageTaxonomyTerms.pageId})::int` }).from(pageTaxonomyTerms).innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId)).where(and(eq(pageTaxonomyTerms.termId, results[0].id), isNull(pages.deletedAt)));
|
|
1987
|
+
count = Number(countResult[0]?.count ?? 0);
|
|
1988
|
+
} catch {
|
|
1989
|
+
count = 0;
|
|
1990
|
+
}
|
|
1991
|
+
return {
|
|
1992
|
+
...results[0],
|
|
1993
|
+
count,
|
|
1994
|
+
children: []
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Retrieves all terms assigned to a specific page entry.
|
|
1999
|
+
*/
|
|
2000
|
+
async function getEntryTerms(db, pageId, taxonomy) {
|
|
2001
|
+
if (!db || !pageId) return [];
|
|
2002
|
+
const conditions = [eq(pageTaxonomyTerms.pageId, pageId)];
|
|
2003
|
+
if (taxonomy) conditions.push(eq(taxonomyTerms.taxonomy, taxonomy));
|
|
2004
|
+
return await db.select({
|
|
2005
|
+
id: taxonomyTerms.id,
|
|
2006
|
+
taxonomy: taxonomyTerms.taxonomy,
|
|
2007
|
+
slug: taxonomyTerms.slug,
|
|
2008
|
+
label: taxonomyTerms.label,
|
|
2009
|
+
description: taxonomyTerms.description,
|
|
2010
|
+
parentId: taxonomyTerms.parentId,
|
|
2011
|
+
displayOrder: taxonomyTerms.displayOrder
|
|
2012
|
+
}).from(pageTaxonomyTerms).innerJoin(taxonomyTerms, eq(taxonomyTerms.id, pageTaxonomyTerms.termId)).where(and(...conditions)).orderBy(asc(taxonomyTerms.displayOrder));
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Retrieves pages tagged with a given taxonomy term.
|
|
2016
|
+
*/
|
|
2017
|
+
async function getEntriesByTerm(db, taxonomy, slug, options = {}) {
|
|
2018
|
+
if (!db) return {
|
|
2019
|
+
entries: [],
|
|
2020
|
+
total: 0
|
|
2021
|
+
};
|
|
2022
|
+
const term = await getTerm(db, taxonomy, slug);
|
|
2023
|
+
if (!term) return {
|
|
2024
|
+
entries: [],
|
|
2025
|
+
total: 0
|
|
2026
|
+
};
|
|
2027
|
+
const conditions = [eq(pageTaxonomyTerms.termId, term.id), isNull(pages.deletedAt)];
|
|
2028
|
+
if (options.type) conditions.push(eq(pages.type, options.type));
|
|
2029
|
+
let query = db.select({ page: pages }).from(pageTaxonomyTerms).innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId)).where(and(...conditions)).orderBy(asc(pages.createdAt));
|
|
2030
|
+
if (options.limit) query = query.limit(options.limit);
|
|
2031
|
+
if (options.offset) query = query.offset(options.offset);
|
|
2032
|
+
const entries = (await query).map((r) => r.page);
|
|
2033
|
+
return {
|
|
2034
|
+
entries,
|
|
2035
|
+
total: term.count ?? entries.length,
|
|
2036
|
+
term
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Sets/syncs the assigned terms for a page within a specific taxonomy.
|
|
2041
|
+
*/
|
|
2042
|
+
async function assignEntryTerms(db, pageId, taxonomy, termIds) {
|
|
2043
|
+
if (!db || !pageId) return;
|
|
2044
|
+
const currentAssigned = await getEntryTerms(db, pageId, taxonomy);
|
|
2045
|
+
const currentTermIds = new Set(currentAssigned.map((t) => t.id));
|
|
2046
|
+
const newTermIds = new Set(termIds);
|
|
2047
|
+
for (const term of currentAssigned) if (!newTermIds.has(term.id)) await db.delete(pageTaxonomyTerms).where(and(eq(pageTaxonomyTerms.pageId, pageId), eq(pageTaxonomyTerms.termId, term.id)));
|
|
2048
|
+
for (const termId of termIds) if (!currentTermIds.has(termId)) await db.insert(pageTaxonomyTerms).values({
|
|
2049
|
+
pageId,
|
|
2050
|
+
termId
|
|
2051
|
+
}).onConflictDoNothing();
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Creates a new taxonomy term.
|
|
2055
|
+
*/
|
|
2056
|
+
async function createTerm(db, data) {
|
|
2057
|
+
return (await db.insert(taxonomyTerms).values(data).returning())[0];
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Updates an existing taxonomy term.
|
|
2061
|
+
*/
|
|
2062
|
+
async function updateTerm(db, id, updates) {
|
|
2063
|
+
return (await db.update(taxonomyTerms).set({
|
|
2064
|
+
...updates,
|
|
2065
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2066
|
+
}).where(eq(taxonomyTerms.id, id)).returning())[0];
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Deletes a taxonomy term.
|
|
2070
|
+
*/
|
|
2071
|
+
async function deleteTerm(db, id) {
|
|
2072
|
+
await db.delete(taxonomyTerms).where(eq(taxonomyTerms.id, id));
|
|
2073
|
+
return { success: true };
|
|
2074
|
+
}
|
|
2075
|
+
//#endregion
|
|
2076
|
+
//#region src/docs/config.ts
|
|
2077
|
+
const defineDocsConfig = (config) => config;
|
|
2078
|
+
//#endregion
|
|
2079
|
+
//#region src/docs/sidebar.ts
|
|
2080
|
+
function formatDocsUrl$1(href, locale, activeVersion, includeVersionInUrl) {
|
|
2081
|
+
let url = href.replace("{locale}", locale);
|
|
2082
|
+
if (activeVersion && includeVersionInUrl) {
|
|
2083
|
+
if (url.includes(`/${activeVersion}/`) || url.endsWith(`/${activeVersion}`)) return url;
|
|
2084
|
+
const docsPattern = new RegExp(`(/${locale}/docs)(/|$)`);
|
|
2085
|
+
const companyDocsPattern = new RegExp(`(/${locale}/company/docs)(/|$)`);
|
|
2086
|
+
if (companyDocsPattern.test(url)) url = url.replace(companyDocsPattern, `$1/${activeVersion}$2`);
|
|
2087
|
+
else if (docsPattern.test(url)) url = url.replace(docsPattern, `$1/${activeVersion}$2`);
|
|
2088
|
+
url = url.replace(/\/{2,}/g, "/");
|
|
2089
|
+
}
|
|
2090
|
+
return url;
|
|
2091
|
+
}
|
|
2092
|
+
function findFirstHref(items, locale, activeVersion, includeVersionInUrl) {
|
|
2093
|
+
for (const item of items) {
|
|
2094
|
+
if (item.href) return formatDocsUrl$1(item.href, locale, activeVersion, includeVersionInUrl);
|
|
2095
|
+
const childItems = item.children || item.items;
|
|
2096
|
+
if (childItems) {
|
|
2097
|
+
const found = findFirstHref(childItems, locale, activeVersion, includeVersionInUrl);
|
|
2098
|
+
if (found) return found;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
function getScopePrefix(items, locale, activeVersion, includeVersionInUrl) {
|
|
2103
|
+
const allHrefs = [];
|
|
2104
|
+
function collect(item) {
|
|
2105
|
+
if (item.href) allHrefs.push(formatDocsUrl$1(item.href, locale, activeVersion, includeVersionInUrl));
|
|
2106
|
+
const childItems = item.children || item.items;
|
|
2107
|
+
if (childItems) childItems.forEach(collect);
|
|
2108
|
+
}
|
|
2109
|
+
items.forEach(collect);
|
|
2110
|
+
if (allHrefs.length === 0) return "";
|
|
2111
|
+
const parts = allHrefs.map((h) => h.split("/").filter(Boolean));
|
|
2112
|
+
let commonLen = 0;
|
|
2113
|
+
const firstPart = parts[0];
|
|
2114
|
+
if (!firstPart) return "";
|
|
2115
|
+
for (let i = 0; i < firstPart.length; i++) if (parts.every((p) => p[i] === firstPart[i])) commonLen++;
|
|
2116
|
+
else break;
|
|
2117
|
+
return "/" + firstPart.slice(0, commonLen).join("/") + "/";
|
|
2118
|
+
}
|
|
2119
|
+
function hasPath(items, locale, normPath, activeVersion, includeVersionInUrl) {
|
|
2120
|
+
for (const item of items) {
|
|
2121
|
+
if (item.href && formatDocsUrl$1(item.href, locale, activeVersion, includeVersionInUrl).replace(/\/$/, "").toLowerCase() === normPath) return true;
|
|
2122
|
+
const childItems = item.children || item.items;
|
|
2123
|
+
if (childItems && hasPath(childItems, locale, normPath, activeVersion, includeVersionInUrl)) return true;
|
|
2124
|
+
}
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
2127
|
+
async function getDocsSidebar(config, locale, currentPath, activeVersion, includeVersionInUrl) {
|
|
2128
|
+
const normPath = currentPath.replace(/\/$/, "").toLowerCase();
|
|
2129
|
+
const sidebarScopes = Array.isArray(config.sidebar) ? config.sidebar.filter((item) => "id" in item || "scopes" in item) : config.sidebar.scopes || [];
|
|
2130
|
+
const scopes = sidebarScopes.map((scope) => {
|
|
2131
|
+
const id = scope.id || scope.label;
|
|
2132
|
+
let href = scope.href || "";
|
|
2133
|
+
if (!href && scope.items && scope.items.length > 0) href = findFirstHref(scope.items, locale, activeVersion, includeVersionInUrl) || "";
|
|
2134
|
+
return {
|
|
2135
|
+
id,
|
|
2136
|
+
label: scope.label,
|
|
2137
|
+
href: formatDocsUrl$1(href, locale, activeVersion, includeVersionInUrl),
|
|
2138
|
+
icon: scope.icon
|
|
2139
|
+
};
|
|
2140
|
+
});
|
|
2141
|
+
const scopePrefixes = sidebarScopes.map((scope) => scope.items ? getScopePrefix(scope.items, locale, activeVersion, includeVersionInUrl) : "");
|
|
2142
|
+
let activeScope = scopes.find((s) => normPath === s.href.replace(/\/$/, "").toLowerCase());
|
|
2143
|
+
if (!activeScope) for (let i = 0; i < sidebarScopes.length; i++) {
|
|
2144
|
+
const scope = sidebarScopes[i];
|
|
2145
|
+
if (scope && scope.items && hasPath(scope.items, locale, normPath, activeVersion, includeVersionInUrl)) {
|
|
2146
|
+
activeScope = scopes[i];
|
|
2147
|
+
break;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
if (!activeScope) {
|
|
2151
|
+
const sorted = [...scopePrefixes.entries()].filter(([, p]) => p).toSorted(([, a], [, b]) => b.length - a.length);
|
|
2152
|
+
for (const [i] of sorted) {
|
|
2153
|
+
const prefix = scopePrefixes[i];
|
|
2154
|
+
if (prefix && normPath.startsWith(prefix.toLowerCase())) {
|
|
2155
|
+
activeScope = scopes[i];
|
|
2156
|
+
break;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (!activeScope && scopes.length > 0) activeScope = scopes[0];
|
|
2161
|
+
if (activeScope) activeScope.active = true;
|
|
2162
|
+
const items = sidebarScopes.find((item) => activeScope && (item.id === activeScope.id || item.label === activeScope.id))?.items || [];
|
|
2163
|
+
async function resolveItem(item) {
|
|
2164
|
+
const resolved = {
|
|
2165
|
+
label: item.label || "",
|
|
2166
|
+
href: item.href ? formatDocsUrl$1(item.href, locale, activeVersion, includeVersionInUrl) : void 0,
|
|
2167
|
+
icon: item.icon,
|
|
2168
|
+
badge: item.badge,
|
|
2169
|
+
collapsed: item.collapsed
|
|
2170
|
+
};
|
|
2171
|
+
if (resolved.href) resolved.active = normPath === resolved.href.replace(/\/$/, "").toLowerCase();
|
|
2172
|
+
const childItems = item.children || item.items;
|
|
2173
|
+
if (childItems) {
|
|
2174
|
+
const children = [];
|
|
2175
|
+
const resolvedChildren = await Promise.all(childItems.map((child) => resolveItem(child)));
|
|
2176
|
+
for (const res of resolvedChildren) children.push(...res);
|
|
2177
|
+
resolved.children = children;
|
|
2178
|
+
if (!resolved.active) resolved.active = children.some((c) => c.active);
|
|
2179
|
+
}
|
|
2180
|
+
return [resolved];
|
|
2181
|
+
}
|
|
2182
|
+
const resolvedItems = [];
|
|
2183
|
+
const resolvedAllItems = await Promise.all(items.map((item) => resolveItem(item)));
|
|
2184
|
+
for (const res of resolvedAllItems) resolvedItems.push(...res);
|
|
2185
|
+
return {
|
|
2186
|
+
items: resolvedItems,
|
|
2187
|
+
scopes,
|
|
2188
|
+
activeScope
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
function getSurroundItems(sidebar, currentPath) {
|
|
2192
|
+
const flatItems = [];
|
|
2193
|
+
function flatten(items) {
|
|
2194
|
+
for (const item of items) {
|
|
2195
|
+
if (item.href) flatItems.push({
|
|
2196
|
+
label: item.label,
|
|
2197
|
+
href: item.href
|
|
2198
|
+
});
|
|
2199
|
+
if (item.children) flatten(item.children);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
flatten(sidebar);
|
|
2203
|
+
const normPath = currentPath.replace(/\/$/, "").toLowerCase();
|
|
2204
|
+
const currentIndex = flatItems.findIndex((item) => item.href.replace(/\/$/, "").toLowerCase() === normPath);
|
|
2205
|
+
return {
|
|
2206
|
+
prev: currentIndex > 0 ? flatItems[currentIndex - 1] : null,
|
|
2207
|
+
next: currentIndex >= 0 && currentIndex < flatItems.length - 1 ? flatItems[currentIndex + 1] : null
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
//#endregion
|
|
2211
|
+
//#region src/docs/routing.ts
|
|
2212
|
+
function getDocsVersions(entries, locale) {
|
|
2213
|
+
const versions = /* @__PURE__ */ new Set();
|
|
2214
|
+
for (const entry of entries) {
|
|
2215
|
+
if (typeof entry === "string") {
|
|
2216
|
+
const parts = entry.replace(/\\/g, "/").split("/");
|
|
2217
|
+
if (parts[0]?.startsWith("v")) versions.add(parts[0]);
|
|
2218
|
+
continue;
|
|
2219
|
+
}
|
|
2220
|
+
if (entry.version && entry.version.startsWith("v")) {
|
|
2221
|
+
versions.add(entry.version);
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
if (entry.properties?.["version"] && String(entry.properties["version"]).startsWith("v")) {
|
|
2225
|
+
versions.add(String(entry.properties["version"]));
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
const parts = (entry.slug || entry.id || "").replace(/\\/g, "/").split("/");
|
|
2229
|
+
if (locale && parts[1] === locale && parts[0]?.startsWith("v")) versions.add(parts[0]);
|
|
2230
|
+
else if (parts[0]?.startsWith("v")) versions.add(parts[0]);
|
|
2231
|
+
}
|
|
2232
|
+
return Array.from(versions).toSorted((a, b) => {
|
|
2233
|
+
return b.localeCompare(a, void 0, {
|
|
2234
|
+
numeric: true,
|
|
2235
|
+
sensitivity: "base"
|
|
2236
|
+
});
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
function parseDocsSlug(slug, versions) {
|
|
2240
|
+
if (versions.length === 0) return {
|
|
2241
|
+
activeVersion: "",
|
|
2242
|
+
contentPath: slug || "index",
|
|
2243
|
+
includeVersionInUrl: false
|
|
2244
|
+
};
|
|
2245
|
+
const defaultVersion = versions[0];
|
|
2246
|
+
const isMultiVersion = versions.length > 1;
|
|
2247
|
+
if (!slug) return {
|
|
2248
|
+
activeVersion: defaultVersion,
|
|
2249
|
+
contentPath: "index",
|
|
2250
|
+
includeVersionInUrl: isMultiVersion
|
|
2251
|
+
};
|
|
2252
|
+
const parts = slug.split("/");
|
|
2253
|
+
const firstPart = parts[0];
|
|
2254
|
+
if (firstPart && versions.includes(firstPart)) return {
|
|
2255
|
+
activeVersion: firstPart,
|
|
2256
|
+
contentPath: parts.slice(1).join("/") || "index",
|
|
2257
|
+
includeVersionInUrl: isMultiVersion
|
|
2258
|
+
};
|
|
2259
|
+
return {
|
|
2260
|
+
activeVersion: defaultVersion,
|
|
2261
|
+
contentPath: slug || "index",
|
|
2262
|
+
includeVersionInUrl: isMultiVersion
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
function formatDocsUrl(href, locale, activeVersion, includeVersionInUrl) {
|
|
2266
|
+
let url = href.replace("{locale}", locale);
|
|
2267
|
+
if (activeVersion && includeVersionInUrl) {
|
|
2268
|
+
if (url.includes(`/${activeVersion}/`) || url.endsWith(`/${activeVersion}`)) return url;
|
|
2269
|
+
const docsPattern = new RegExp(`(/${locale}/docs)(/|$)`);
|
|
2270
|
+
const companyDocsPattern = new RegExp(`(/${locale}/company/docs)(/|$)`);
|
|
2271
|
+
if (companyDocsPattern.test(url)) url = url.replace(companyDocsPattern, `$1/${activeVersion}$2`);
|
|
2272
|
+
else if (docsPattern.test(url)) url = url.replace(docsPattern, `$1/${activeVersion}$2`);
|
|
2273
|
+
url = url.replace(/\/{2,}/g, "/");
|
|
2274
|
+
}
|
|
2275
|
+
return url;
|
|
2276
|
+
}
|
|
2277
|
+
//#endregion
|
|
2278
|
+
//#region src/docs/toc.ts
|
|
2279
|
+
function slugifyHeading(text) {
|
|
2280
|
+
return text.toString().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/-{2,}/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
2281
|
+
}
|
|
2282
|
+
function extractHeadingsFromBlocks(blocks = [], currentDepth = 2) {
|
|
2283
|
+
if (!Array.isArray(blocks)) return [];
|
|
2284
|
+
const headings = [];
|
|
2285
|
+
for (const block of blocks) {
|
|
2286
|
+
if (!block) continue;
|
|
2287
|
+
if (block.type === "SectionBlock") {
|
|
2288
|
+
const title = block.props?.title || "";
|
|
2289
|
+
const level = Number(block.props?.level) || currentDepth;
|
|
2290
|
+
if (title) headings.push({
|
|
2291
|
+
depth: level,
|
|
2292
|
+
slug: slugifyHeading(title),
|
|
2293
|
+
text: title
|
|
2294
|
+
});
|
|
2295
|
+
const children = block.children || block.props?.children || [];
|
|
2296
|
+
if (Array.isArray(children) && children.length > 0) headings.push(...extractHeadingsFromBlocks(children, Math.min(6, level + 1)));
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
return headings;
|
|
2300
|
+
}
|
|
2301
|
+
//#endregion
|
|
2302
|
+
//#region src/docs/agent.ts
|
|
2303
|
+
function resolveLocalized(val, locale = "en") {
|
|
2304
|
+
if (!val) return "";
|
|
2305
|
+
if (typeof val === "string") return val;
|
|
2306
|
+
if (typeof val === "object" && val !== null) {
|
|
2307
|
+
const rec = val;
|
|
2308
|
+
return rec[locale] || rec["en"] || Object.values(rec)[0] || "";
|
|
2309
|
+
}
|
|
2310
|
+
return typeof val === "number" ? String(val) : "";
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Renders a CMS page record into clean, agent-ready Markdown with YAML frontmatter.
|
|
2314
|
+
*/
|
|
2315
|
+
function renderPageAsMarkdown(page, locale = "en", options = {}) {
|
|
2316
|
+
const { siteUrl = "https://rimelight.com", llmsUrl = "/llms.txt", sourceUrl } = options;
|
|
2317
|
+
let contentObj = page.content;
|
|
2318
|
+
if (typeof contentObj === "string") try {
|
|
2319
|
+
contentObj = JSON.parse(contentObj);
|
|
2320
|
+
} catch {
|
|
2321
|
+
contentObj = {
|
|
2322
|
+
blocks: [],
|
|
2323
|
+
properties: {}
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
else if (!contentObj || typeof contentObj !== "object") contentObj = {
|
|
2327
|
+
blocks: [],
|
|
2328
|
+
properties: {}
|
|
2329
|
+
};
|
|
2330
|
+
const blocks = Array.isArray(contentObj.blocks) ? contentObj.blocks : [];
|
|
2331
|
+
const properties = contentObj.properties || {};
|
|
2332
|
+
const title = resolveLocalized(page.title, locale) || properties.title || "Untitled";
|
|
2333
|
+
const description = resolveLocalized(page.description, locale) || properties.description || "";
|
|
2334
|
+
const version = properties.version || page.templateVersion;
|
|
2335
|
+
const markdownBody = blocksToMarkdown(blocks);
|
|
2336
|
+
const frontmatter = [
|
|
2337
|
+
"---",
|
|
2338
|
+
`title: ${JSON.stringify(title)}`,
|
|
2339
|
+
...description ? [`description: ${JSON.stringify(description)}`] : [],
|
|
2340
|
+
...version ? [`version: ${JSON.stringify(version)}`] : [],
|
|
2341
|
+
...page.type ? [`type: ${JSON.stringify(page.type)}`] : [],
|
|
2342
|
+
"---",
|
|
2343
|
+
"",
|
|
2344
|
+
"> Documentation Index",
|
|
2345
|
+
`> Fetch the complete documentation index at: ${new URL(llmsUrl, siteUrl).href}`,
|
|
2346
|
+
"> Use this file to discover all available pages and sections.",
|
|
2347
|
+
"",
|
|
2348
|
+
`# ${title}`,
|
|
2349
|
+
"",
|
|
2350
|
+
markdownBody,
|
|
2351
|
+
""
|
|
2352
|
+
];
|
|
2353
|
+
if (sourceUrl) frontmatter.push(`Source: ${new URL(sourceUrl, siteUrl).href}`, "");
|
|
2354
|
+
return frontmatter.join("\n");
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Collate all published pages of a given type into a single corpus markdown document.
|
|
2358
|
+
*/
|
|
2359
|
+
async function renderCorpusMarkdown(db, options = {}) {
|
|
2360
|
+
const { type = "doc", locale = "en", siteUrl = "https://rimelight.com", title = "Rimelight Documentation Corpus", description = "Complete single-corpus documentation for AI agents." } = options;
|
|
2361
|
+
if (!db) return `# ${title}\n\n${description}`;
|
|
2362
|
+
const conditions = [isNull(pages.deletedAt), isNotNull(pages.publishedVersionId)];
|
|
2363
|
+
if (type) conditions.push(eq(pages.type, type));
|
|
2364
|
+
const rows = await db.select().from(pages).where(and(...conditions));
|
|
2365
|
+
const parts = [
|
|
2366
|
+
`# ${title}`,
|
|
2367
|
+
"",
|
|
2368
|
+
description,
|
|
2369
|
+
"",
|
|
2370
|
+
`Generated for site: ${siteUrl}`,
|
|
2371
|
+
"",
|
|
2372
|
+
"---",
|
|
2373
|
+
""
|
|
2374
|
+
];
|
|
2375
|
+
for (const page of rows) {
|
|
2376
|
+
const pageMd = renderPageAsMarkdown(page, locale, { siteUrl });
|
|
2377
|
+
parts.push(pageMd, "", "---", "");
|
|
2378
|
+
}
|
|
2379
|
+
return parts.join("\n");
|
|
2380
|
+
}
|
|
2381
|
+
//#endregion
|
|
2382
|
+
export { ALLOWED_CHILDREN_MAP, BLOG_POST_DEFINITION, BlockRegistry, CARD_DEFINITION, CHARACTER_DEFINITION, CMS_MCP_TOOLS, DEFAULT_SITE_SETTINGS, DOCUMENT_DEFINITION, GROUP_DEFINITION, HERO_DEFINITION, ITEM_DEFINITION, LOCATION_DEFINITION, MAX_SECTION_DEPTH, MAX_SECTION_HEADING_LEVEL, MIN_SECTION_HEADING_LEVEL, OBJECT_DEFINITION, PAGE_MAP, PATCH_NOTE_DEFINITION, POSTGRES_SEARCH_CONFIGS, SERIES_DEFINITION, SKILL_DEFINITION, SPECIES_DEFINITION, addToast, assignEntryTerms, auth0Auth, blocksToMarkdown, bylines, calculateHeadingLevel, canUserCreateFromTemplate, canUserEditBlock, canUserEditFromTemplate, canUserInstantiateTemplate, canUserReviewFromTemplate, canUserViewFromTemplate, cfAccessAuth, clearToasts, cmsAc, cmsStatements, contentSearchIndex, createByline, createPageExcerpt, createTerm, defineDocsConfig, definePageDefinition, deleteByline, deleteR2File, deleteTerm, diffBlocks, diffPageSnapshots, evaluateAccess, extractHeadingsFromBlocks, extractTextFromBlocks, filterBlocksForReader, filterTemplatesForUser, formatDocsUrl, generatePreviewToken, getByline, getBylines, getCmsCollection, getCmsEntry, getDocsSidebar, getDocsVersions, getEntriesByTerm, getEntryTerms, getPageDefinition, getPostgresSearchConfig, getR2Bucket, getSiteSettings, getSurroundItems, getTaxonomyTerms, getTerm, handleCmsCron, handleCmsMcpRequest, hasLockedDescendants, hasPermission, hasRole, indexPageForSearch, inlinesToMarkdown, invalidatePageCache, isValidUUID, listR2Files, mockAuth, pageDraftLocks, pageDrafts, pageTaxonomyTerms, pageTemplates, pageVersionApprovals, pageVersionComments, pageVersions, pages, parseDocsSlug, r2, removeToast, renderCorpusMarkdown, renderPageAsMarkdown, rimelightCms, rimelightCmsLoader, showErrorToast, showSuccessToast, siteSettings, slugifyHeading, taxonomyTerms, toast, toasts, updateByline, updateSiteSettings, updateTerm, uploadR2File, validateBlockAST, validateServerBlockPayload, verifyPreviewToken };
|