@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.
Files changed (66) hide show
  1. package/dist/bylines-Csggkl-g.d.mts +405 -0
  2. package/dist/index.d.mts +724 -0
  3. package/dist/index.mjs +2382 -0
  4. package/dist/integration.d.mts +33 -0
  5. package/dist/integration.mjs +161 -0
  6. package/dist/schema/index.d.mts +2063 -0
  7. package/dist/schema/index.mjs +176 -0
  8. package/dist/schema/sqlite.d.mts +2132 -0
  9. package/dist/schema/sqlite.mjs +137 -0
  10. package/dist/storage/index.d.mts +59 -0
  11. package/dist/storage/index.mjs +133 -0
  12. package/package.json +35 -14
  13. package/src/admin/api/media-file.ts +1 -1
  14. package/src/admin/api/media.ts +28 -3
  15. package/src/admin/layouts/CMSDashboardLayout.astro +16 -3
  16. package/src/admin/pages/assets.astro +11 -11
  17. package/src/admin/pages/pages-edit.astro +3 -3
  18. package/src/admin/pages/pages-index.astro +2 -2
  19. package/src/admin/pages/pages-preview.astro +3 -3
  20. package/src/admin/pages/pages-review.astro +2 -2
  21. package/src/admin/pages/templates-edit.astro +1 -1
  22. package/src/admin/pages/templates-index.astro +1 -1
  23. package/src/astro/BlockRenderer.astro +1 -1
  24. package/src/astro/PageRenderer.astro +4 -4
  25. package/src/astro/blocks/CalloutBlock.astro +1 -1
  26. package/src/astro/blocks/OrderedListBlock.astro +2 -2
  27. package/src/astro/blocks/ParagraphBlock.astro +1 -1
  28. package/src/astro/blocks/TabsBlock.astro +5 -5
  29. package/src/astro/blocks/UnorderedListBlock.astro +2 -2
  30. package/src/auth/editor-guards.ts +1 -1
  31. package/src/cache/purge.ts +2 -2
  32. package/src/client/components/RimelightBlock.tsx +1 -1
  33. package/src/client/components/RimelightBlockPicker.tsx +1 -1
  34. package/src/client/components/RimelightPreview.tsx +27 -27
  35. package/src/client/components/RimelightPropertiesPanel.tsx +4 -4
  36. package/src/client/components/RimelightTemplateEditor.tsx +9 -5
  37. package/src/client/components/editors/CalloutEditor.tsx +10 -10
  38. package/src/client/components/editors/CardEditor.tsx +5 -5
  39. package/src/client/components/editors/CardsEditor.tsx +2 -2
  40. package/src/client/components/editors/CodeEditor.tsx +5 -5
  41. package/src/client/components/editors/DialogueEditor.tsx +6 -6
  42. package/src/client/components/editors/FileTreeEditor.tsx +1 -1
  43. package/src/client/components/editors/ImageEditor.tsx +9 -9
  44. package/src/client/components/editors/ParagraphEditor.tsx +1 -1
  45. package/src/client/components/editors/SceneEditor.tsx +9 -9
  46. package/src/client/components/editors/ScriptEditor.tsx +8 -8
  47. package/src/client/components/editors/SectionEditor.tsx +3 -3
  48. package/src/client/components/editors/StepItemEditor.tsx +6 -5
  49. package/src/client/components/editors/StepsEditor.tsx +1 -1
  50. package/src/client/components/editors/TabItemEditor.tsx +3 -3
  51. package/src/client/components/editors/TableEditor.tsx +4 -4
  52. package/src/client/components/editors/TabsEditor.tsx +1 -1
  53. package/src/client/state/editor-store.ts +23 -23
  54. package/src/client/utils/sortable.ts +13 -12
  55. package/src/client/utils/tree-sanitizer.ts +1 -1
  56. package/src/core/excerpt.ts +16 -16
  57. package/src/core/page-definitions.ts +2 -2
  58. package/src/docs/agent.ts +1 -1
  59. package/src/docs/routing.ts +2 -2
  60. package/src/env.d.ts +6 -0
  61. package/src/index.ts +17 -4
  62. package/src/integration.ts +54 -0
  63. package/src/loader/live.ts +1 -1
  64. package/src/markdown/serializer.ts +26 -26
  65. package/src/schema/sqlite.ts +344 -0
  66. 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(`![${alt}](${src})${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 };