@pramen/cms 0.0.48 → 0.0.49

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/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { HandlerContext, Policy, FileRef, BootstrapFn } from "@pramen/server";
1
+ import type { HandlerContext, Policy, FileRef, BootstrapFn, JsonValue } from "@pramen/server";
2
+ import type { EnvBag } from "@pramen/server";
2
3
  /** A field in a block type's (or content type's) field schema. Recursive: a `repeater`
3
4
  * or `group` nests `fields`. Mirrors WollyCMS's FieldDefinition. */
4
5
  export interface FieldDefinition {
@@ -69,7 +70,7 @@ export interface RegionDefinition {
69
70
  export interface DefaultBlockDefinition {
70
71
  region: string;
71
72
  blockTypeSlug: string;
72
- fields?: Record<string, unknown>;
73
+ fields?: FieldValues;
73
74
  }
74
75
  /** A rich-text value — a serialized editor document (or a plain string). */
75
76
  export type RichText = string | {
@@ -548,7 +549,7 @@ export interface ValidateOpts {
548
549
  export declare function validateFields(schema: FieldDefinition[] | undefined | null, values: unknown, path?: string, opts?: ValidateOpts): void;
549
550
  /** Deep-sanitize the richtext fields in a values object against a field schema (recursing
550
551
  * into group/repeater). Returns a sanitized copy; non-richtext fields pass through. */
551
- export declare function sanitizeFields(schema: FieldDefinition[] | undefined | null, values: Record<string, unknown>): Record<string, unknown>;
552
+ export declare function sanitizeFields(schema: FieldDefinition[] | undefined | null, values: FieldValues): FieldValues;
552
553
  export interface RenderedBlock {
553
554
  /** The placement id (cms_page_blocks) — stable per position; used for reorder/remove. */
554
555
  id: string;
@@ -556,7 +557,7 @@ export interface RenderedBlock {
556
557
  block_id: string;
557
558
  block_type: string;
558
559
  title: string | null;
559
- fields: Record<string, unknown>;
560
+ fields: FieldValues;
560
561
  is_shared: boolean;
561
562
  }
562
563
  export interface PageTranslation {
@@ -586,7 +587,7 @@ export interface AssembledPage {
586
587
  translationGroupId: string | null;
587
588
  /** Published sibling locales of this page (for hreflang alternates). */
588
589
  translations: PageTranslation[];
589
- fields: Record<string, unknown> | null;
590
+ fields: FieldValues | null;
590
591
  /** Back-compat: mirrors seo.metaTitle/metaDescription. */
591
592
  metaTitle: string | null;
592
593
  metaDescription: string | null;
@@ -594,6 +595,14 @@ export interface AssembledPage {
594
595
  };
595
596
  regions: Record<string, RenderedBlock[]>;
596
597
  }
598
+ /** One authored field value inside a block / collection / page `fields` bag. Stored
599
+ * as JSON; a `"media"` field is resolved from its stored id to a `ResolvedMedia` at
600
+ * assemble time, and `group`/`repeater` fields nest further bags. */
601
+ export type FieldValue = JsonValue | ResolvedMedia | FieldValues | FieldValue[];
602
+ /** A block / collection / page `fields` bag — field name -> authored value. */
603
+ export interface FieldValues {
604
+ [field: string]: FieldValue;
605
+ }
597
606
  /** A `"media"` block field, resolved from a stored media id to a servable shape at
598
607
  * assemble time. `url` is the raw (full-size) serving path; pass `key` to `imageUrl()`
599
608
  * for on-the-fly transforms. `null` when the referenced media was deleted. */
@@ -737,7 +746,7 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
737
746
  title?: string;
738
747
  slug?: string;
739
748
  locale?: string;
740
- fields?: Record<string, unknown>;
749
+ fields?: FieldValues;
741
750
  }, {
742
751
  ok: boolean;
743
752
  page: Record<string, unknown>;
@@ -748,7 +757,7 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
748
757
  title: string;
749
758
  slug: string;
750
759
  locale?: string;
751
- fields?: Record<string, unknown>;
760
+ fields?: FieldValues;
752
761
  }, Record<string, unknown>>;
753
762
  /** Create a translation of an existing page: a new page in `locale` sharing the
754
763
  * source's translationGroupId (and content type). Content starts empty — the editor
@@ -778,7 +787,7 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
778
787
  pageId: string;
779
788
  blockTypeSlug: string;
780
789
  region: string;
781
- fields?: Record<string, unknown>;
790
+ fields?: FieldValues;
782
791
  title?: string;
783
792
  position?: number;
784
793
  isReusable?: boolean;
@@ -797,7 +806,7 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
797
806
  blockId: string;
798
807
  region: string;
799
808
  position?: number;
800
- overrides?: Record<string, unknown>;
809
+ overrides?: FieldValues;
801
810
  }, Record<string, unknown>>;
802
811
  /** Fetch a block's RAW content (media fields as ids, not resolved) — for editing. */
803
812
  getBlock: import("@pramen/server").Handler<{
@@ -806,7 +815,7 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
806
815
  /** Update a block's content (re-validated against its type's field schema). */
807
816
  updateBlock: import("@pramen/server").Handler<{
808
817
  blockId: string;
809
- fields?: Record<string, unknown>;
818
+ fields?: FieldValues;
810
819
  title?: string;
811
820
  }, Record<string, unknown> | undefined>;
812
821
  /** Reorder a region: `order` is the page_block ids in their new order. It must cover
@@ -1007,7 +1016,7 @@ export declare const cmsHandlers: {
1007
1016
  title?: string;
1008
1017
  slug?: string;
1009
1018
  locale?: string;
1010
- fields?: Record<string, unknown>;
1019
+ fields?: FieldValues;
1011
1020
  }, {
1012
1021
  ok: boolean;
1013
1022
  page: Record<string, unknown>;
@@ -1018,7 +1027,7 @@ export declare const cmsHandlers: {
1018
1027
  title: string;
1019
1028
  slug: string;
1020
1029
  locale?: string;
1021
- fields?: Record<string, unknown>;
1030
+ fields?: FieldValues;
1022
1031
  }, Record<string, unknown>>;
1023
1032
  /** Create a translation of an existing page: a new page in `locale` sharing the
1024
1033
  * source's translationGroupId (and content type). Content starts empty — the editor
@@ -1048,7 +1057,7 @@ export declare const cmsHandlers: {
1048
1057
  pageId: string;
1049
1058
  blockTypeSlug: string;
1050
1059
  region: string;
1051
- fields?: Record<string, unknown>;
1060
+ fields?: FieldValues;
1052
1061
  title?: string;
1053
1062
  position?: number;
1054
1063
  isReusable?: boolean;
@@ -1067,7 +1076,7 @@ export declare const cmsHandlers: {
1067
1076
  blockId: string;
1068
1077
  region: string;
1069
1078
  position?: number;
1070
- overrides?: Record<string, unknown>;
1079
+ overrides?: FieldValues;
1071
1080
  }, Record<string, unknown>>;
1072
1081
  /** Fetch a block's RAW content (media fields as ids, not resolved) — for editing. */
1073
1082
  getBlock: import("@pramen/server").Handler<{
@@ -1076,7 +1085,7 @@ export declare const cmsHandlers: {
1076
1085
  /** Update a block's content (re-validated against its type's field schema). */
1077
1086
  updateBlock: import("@pramen/server").Handler<{
1078
1087
  blockId: string;
1079
- fields?: Record<string, unknown>;
1088
+ fields?: FieldValues;
1080
1089
  title?: string;
1081
1090
  }, Record<string, unknown> | undefined>;
1082
1091
  /** Reorder a region: `order` is the page_block ids in their new order. It must cover
@@ -1336,7 +1345,7 @@ export declare function robotsTxt(opts: {
1336
1345
  interface RouteCtx {
1337
1346
  callPrivileged: (opts: {
1338
1347
  name: string;
1339
- input?: unknown;
1348
+ input?: JsonValue;
1340
1349
  tenant?: string;
1341
1350
  roles?: string[];
1342
1351
  }) => Promise<Response>;
@@ -1344,7 +1353,7 @@ interface RouteCtx {
1344
1353
  interface CmsRoute {
1345
1354
  method: string;
1346
1355
  path: string;
1347
- handler: (request: Request, env: Readonly<Record<string, unknown>>, ctx: RouteCtx) => Promise<Response>;
1356
+ handler: (request: Request, env: EnvBag, ctx: RouteCtx) => Promise<Response>;
1348
1357
  }
1349
1358
  /** Turnkey public routes for `GET /sitemap.xml` and `GET /robots.txt`. Spread into
1350
1359
  * `app.routes`. The sitemap pulls published pages via `callPrivileged(listPublishedPages)`.
package/dist/index.js CHANGED
@@ -504,7 +504,9 @@ async function assembleLive(db, page) {
504
504
  // fields (id → ResolvedMedia) in one batched lookup across the whole page.
505
505
  const merged = placements.map((p) => {
506
506
  const block = asObj(p.block);
507
- const fields = { ...asObj(block.fields), ...(p.isShared ? asObj(p.overrides) : {}) };
507
+ const fields = p.isShared
508
+ ? { ...asObj(block.fields), ...asObj(p.overrides) }
509
+ : { ...asObj(block.fields) };
508
510
  return { p, block, fields, schema: typeById.get(String(block.typeId))?.fieldsSchema };
509
511
  });
510
512
  const mediaIds = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pramen/cms",
3
- "version": "0.0.48",
3
+ "version": "0.0.49",
4
4
  "description": "Optional block/page builder for pramen — Drupal-Paragraphs-style typed blocks in named regions, reusable blocks, scheduled publishing, built entirely from pramen primitives.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@pramen/server": "0.0.48",
44
+ "@pramen/server": "0.0.49",
45
45
  "xss": "^1.0.15"
46
46
  },
47
47
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -42,8 +42,9 @@ import {
42
42
  Forbidden,
43
43
  PramenError,
44
44
  } from "@pramen/server";
45
- import type { HandlerContext, Policy, FileRef, BootstrapFn } from "@pramen/server";
45
+ import type { HandlerContext, Policy, FileRef, BootstrapFn, JsonValue } from "@pramen/server";
46
46
  import { filterXSS } from "xss";
47
+ import type { EnvBag } from "@pramen/server";
47
48
 
48
49
  // --- field schema DSL (the block-editor field language) ---------------------
49
50
 
@@ -131,7 +132,7 @@ export interface RegionDefinition {
131
132
  export interface DefaultBlockDefinition {
132
133
  region: string;
133
134
  blockTypeSlug: string;
134
- fields?: Record<string, unknown>;
135
+ fields?: FieldValues;
135
136
  }
136
137
 
137
138
  // --- hybrid typed blocks: compile-time inference over a const FieldDefinition[] --------
@@ -529,7 +530,7 @@ function isDateTimeString(v: string): boolean {
529
530
  export function validateFields(schema: FieldDefinition[] | undefined | null, values: unknown, path = "", opts: ValidateOpts = {}): void {
530
531
  const requireRequired = opts.requireRequired !== false;
531
532
  const defs = Array.isArray(schema) ? schema : [];
532
- const obj = (values ?? {}) as Record<string, unknown>;
533
+ const obj = (values ?? {}) as FieldValues;
533
534
  if (typeof obj !== "object" || Array.isArray(obj)) throw new BadRequest(`${path || "fields"} must be an object`);
534
535
  for (const def of defs) {
535
536
  const at = path ? `${path}.${def.name}` : def.name;
@@ -616,15 +617,15 @@ function sanitizeRichText(html: string): string {
616
617
 
617
618
  /** Deep-sanitize the richtext fields in a values object against a field schema (recursing
618
619
  * into group/repeater). Returns a sanitized copy; non-richtext fields pass through. */
619
- export function sanitizeFields(schema: FieldDefinition[] | undefined | null, values: Record<string, unknown>): Record<string, unknown> {
620
+ export function sanitizeFields(schema: FieldDefinition[] | undefined | null, values: FieldValues): FieldValues {
620
621
  const defs = Array.isArray(schema) ? schema : [];
621
- const out: Record<string, unknown> = { ...values };
622
+ const out: FieldValues = { ...values };
622
623
  for (const def of defs) {
623
624
  const v = out[def.name];
624
625
  if (v == null) continue;
625
626
  if (def.type === "richtext" && typeof v === "string") out[def.name] = sanitizeRichText(v);
626
- else if (def.type === "group" && typeof v === "object" && !Array.isArray(v)) out[def.name] = sanitizeFields(def.fields, v as Record<string, unknown>);
627
- else if (def.type === "repeater" && Array.isArray(v)) out[def.name] = v.map((it) => (it && typeof it === "object" ? sanitizeFields(def.fields, it as Record<string, unknown>) : it));
627
+ else if (def.type === "group" && typeof v === "object" && !Array.isArray(v)) out[def.name] = sanitizeFields(def.fields, v as FieldValues);
628
+ else if (def.type === "repeater" && Array.isArray(v)) out[def.name] = v.map((it) => (it && typeof it === "object" ? sanitizeFields(def.fields, it as FieldValues) : it));
628
629
  }
629
630
  return out;
630
631
  }
@@ -638,7 +639,7 @@ export interface RenderedBlock {
638
639
  block_id: string;
639
640
  block_type: string;
640
641
  title: string | null;
641
- fields: Record<string, unknown>;
642
+ fields: FieldValues;
642
643
  is_shared: boolean;
643
644
  }
644
645
 
@@ -671,7 +672,7 @@ export interface AssembledPage {
671
672
  translationGroupId: string | null;
672
673
  /** Published sibling locales of this page (for hreflang alternates). */
673
674
  translations: PageTranslation[];
674
- fields: Record<string, unknown> | null;
675
+ fields: FieldValues | null;
675
676
  /** Back-compat: mirrors seo.metaTitle/metaDescription. */
676
677
  metaTitle: string | null;
677
678
  metaDescription: string | null;
@@ -682,6 +683,16 @@ export interface AssembledPage {
682
683
 
683
684
  // --- media -------------------------------------------------------------------
684
685
 
686
+ /** One authored field value inside a block / collection / page `fields` bag. Stored
687
+ * as JSON; a `"media"` field is resolved from its stored id to a `ResolvedMedia` at
688
+ * assemble time, and `group`/`repeater` fields nest further bags. */
689
+ export type FieldValue = JsonValue | ResolvedMedia | FieldValues | FieldValue[];
690
+
691
+ /** A block / collection / page `fields` bag — field name -> authored value. */
692
+ export interface FieldValues {
693
+ [field: string]: FieldValue;
694
+ }
695
+
685
696
  /** A `"media"` block field, resolved from a stored media id to a servable shape at
686
697
  * assemble time. `url` is the raw (full-size) serving path; pass `key` to `imageUrl()`
687
698
  * for on-the-fly transforms. `null` when the referenced media was deleted. */
@@ -720,7 +731,7 @@ export function imageUrl(
720
731
  }
721
732
 
722
733
  /** Collect the media ids referenced by a fields payload, walking group/repeater nesting. */
723
- function collectMediaIds(fields: Record<string, unknown>, schema: FieldDefinition[] | undefined, acc: Set<string>): void {
734
+ function collectMediaIds(fields: FieldValues, schema: FieldDefinition[] | undefined, acc: Set<string>): void {
724
735
  if (!Array.isArray(schema)) return;
725
736
  for (const def of schema) {
726
737
  const v = fields[def.name];
@@ -738,12 +749,12 @@ function collectMediaIds(fields: Record<string, unknown>, schema: FieldDefinitio
738
749
  /** Return a copy of `fields` with every `"media"` field resolved from its id to a
739
750
  * `ResolvedMedia` (or null), recursing into group/repeater nesting. */
740
751
  function resolveMediaFields(
741
- fields: Record<string, unknown>,
752
+ fields: FieldValues,
742
753
  schema: FieldDefinition[] | undefined,
743
754
  mediaById: Map<string, ResolvedMedia>,
744
- ): Record<string, unknown> {
755
+ ): FieldValues {
745
756
  if (!Array.isArray(schema)) return fields;
746
- const out: Record<string, unknown> = { ...fields };
757
+ const out: FieldValues = { ...fields };
747
758
  for (const def of schema) {
748
759
  const v = out[def.name];
749
760
  if (v == null) continue;
@@ -782,7 +793,7 @@ interface CmsDb {
782
793
  const cdb = (ctx: HandlerContext): CmsDb => ctx.db as unknown as CmsDb;
783
794
 
784
795
  const notFound = (what: string) => new PramenError(`${what} not found`, 404, "not_found");
785
- const asObj = (v: unknown): Record<string, unknown> => (v && typeof v === "object" ? (v as Record<string, unknown>) : {});
796
+ const asObj = (v: unknown): FieldValues => (v && typeof v === "object" ? (v as FieldValues) : {});
786
797
  // Timestamps in the SAME shape as the `expr.now()` column default (`datetime('now')`:
787
798
  // "YYYY-MM-DD HH:MM:SS", UTC, second precision) so a column's insert-default and its
788
799
  // handler-written updates stay lexically comparable (an ISO `T`/`Z` string sorts wrong).
@@ -816,7 +827,9 @@ async function assembleLive(db: CmsDb, page: Record<string, unknown>): Promise<A
816
827
  // fields (id → ResolvedMedia) in one batched lookup across the whole page.
817
828
  const merged = placements.map((p) => {
818
829
  const block = asObj(p.block);
819
- const fields = { ...asObj(block.fields), ...(p.isShared ? asObj(p.overrides) : {}) };
830
+ const fields = p.isShared
831
+ ? { ...asObj(block.fields), ...asObj(p.overrides) }
832
+ : { ...asObj(block.fields) };
820
833
  return { p, block, fields, schema: typeById.get(String(block.typeId))?.fieldsSchema };
821
834
  });
822
835
  const mediaIds = new Set<string>();
@@ -908,7 +921,7 @@ function pageMeta(page: Record<string, unknown>, translations: PageTranslation[]
908
921
  contentType,
909
922
  translationGroupId: (page.translationGroupId as string | null) ?? null,
910
923
  translations,
911
- fields: (page.fields as Record<string, unknown> | null) ?? null,
924
+ fields: (page.fields as FieldValues | null) ?? null,
912
925
  metaTitle,
913
926
  metaDescription,
914
927
  seo: {
@@ -1237,7 +1250,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1237
1250
  * page record itself, which was previously only settable at createPage. A slug/locale
1238
1251
  * change re-checks (slug, locale) uniqueness (excluding this page); `fields` is validated
1239
1252
  * + sanitized against the content type's fieldsSchema, exactly like createPage. */
1240
- updatePage: mutation(async (ctx, input: { pageId: string; title?: string; slug?: string; locale?: string; fields?: Record<string, unknown> }) => {
1253
+ updatePage: mutation(async (ctx, input: { pageId: string; title?: string; slug?: string; locale?: string; fields?: FieldValues }) => {
1241
1254
  const db = cdb(ctx);
1242
1255
  const rows = await db.find({ from: "cms_pages", where: { id: input.pageId }, limit: 1 });
1243
1256
  const page = rows[0];
@@ -1262,7 +1275,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1262
1275
  return { ok: true, page: updated };
1263
1276
  }, {
1264
1277
  ...editor,
1265
- input: (raw): { pageId: string; title?: string; slug?: string; locale?: string; fields?: Record<string, unknown> } => {
1278
+ input: (raw): { pageId: string; title?: string; slug?: string; locale?: string; fields?: FieldValues } => {
1266
1279
  const o = asObj(raw);
1267
1280
  if (typeof o.pageId !== "string") throw new BadRequest("pageId is required");
1268
1281
  for (const k of ["title", "slug", "locale"] as const) {
@@ -1273,7 +1286,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1273
1286
  }),
1274
1287
 
1275
1288
  /** Create a page and auto-scaffold its content type's default blocks. */
1276
- createPage: mutation(async (ctx, input: { typeId: string; title: string; slug: string; locale?: string; fields?: Record<string, unknown> }) => {
1289
+ createPage: mutation(async (ctx, input: { typeId: string; title: string; slug: string; locale?: string; fields?: FieldValues }) => {
1277
1290
  const db = cdb(ctx);
1278
1291
  const ctRows = await db.find({ from: "cms_content_types", where: { id: input.typeId }, limit: 1 });
1279
1292
  const ct = ctRows[0];
@@ -1314,7 +1327,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1314
1327
  return page;
1315
1328
  }, {
1316
1329
  ...editor,
1317
- input: (raw): { typeId: string; title: string; slug: string; locale?: string; fields?: Record<string, unknown> } => {
1330
+ input: (raw): { typeId: string; title: string; slug: string; locale?: string; fields?: FieldValues } => {
1318
1331
  const o = asObj(raw);
1319
1332
  if (typeof o.typeId !== "string" || typeof o.title !== "string" || typeof o.slug !== "string") {
1320
1333
  throw new BadRequest("typeId, title and slug are required");
@@ -1393,7 +1406,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1393
1406
  /** Create a block instance and place it into a page region in one call (the common
1394
1407
  * editor action). Validates the fields against the block type's schema and the region
1395
1408
  * against the content type's allow-list. */
1396
- addBlock: mutation(async (ctx, input: { pageId: string; blockTypeSlug: string; region: string; fields?: Record<string, unknown>; title?: string; position?: number; isReusable?: boolean }) => {
1409
+ addBlock: mutation(async (ctx, input: { pageId: string; blockTypeSlug: string; region: string; fields?: FieldValues; title?: string; position?: number; isReusable?: boolean }) => {
1397
1410
  const db = cdb(ctx);
1398
1411
  const pages = await db.find({ from: "cms_pages", where: { id: input.pageId }, limit: 1 });
1399
1412
  const page = pages[0];
@@ -1420,7 +1433,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1420
1433
  return { block, placement };
1421
1434
  }, {
1422
1435
  ...editor,
1423
- input: (raw): { pageId: string; blockTypeSlug: string; region: string; fields?: Record<string, unknown>; title?: string; position?: number; isReusable?: boolean } => {
1436
+ input: (raw): { pageId: string; blockTypeSlug: string; region: string; fields?: FieldValues; title?: string; position?: number; isReusable?: boolean } => {
1424
1437
  const o = asObj(raw);
1425
1438
  if (typeof o.pageId !== "string" || typeof o.blockTypeSlug !== "string" || typeof o.region !== "string") {
1426
1439
  throw new BadRequest("pageId, blockTypeSlug and region are required");
@@ -1435,7 +1448,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1435
1448
  * can be placed on several pages; editing it updates them all, while `overrides` let one
1436
1449
  * placement diverge. The merged (base + overrides) result is validated against the
1437
1450
  * block type's field schema. */
1438
- placeBlock: mutation(async (ctx, input: { pageId: string; blockId: string; region: string; position?: number; overrides?: Record<string, unknown> }) => {
1451
+ placeBlock: mutation(async (ctx, input: { pageId: string; blockId: string; region: string; position?: number; overrides?: FieldValues }) => {
1439
1452
  const db = cdb(ctx);
1440
1453
  const pages = await db.find({ from: "cms_pages", where: { id: input.pageId }, limit: 1 });
1441
1454
  const page = pages[0];
@@ -1462,7 +1475,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1462
1475
  });
1463
1476
  }, {
1464
1477
  ...editor,
1465
- input: (raw): { pageId: string; blockId: string; region: string; position?: number; overrides?: Record<string, unknown> } => {
1478
+ input: (raw): { pageId: string; blockId: string; region: string; position?: number; overrides?: FieldValues } => {
1466
1479
  const o = asObj(raw);
1467
1480
  if (typeof o.pageId !== "string" || typeof o.blockId !== "string" || typeof o.region !== "string") {
1468
1481
  throw new BadRequest("pageId, blockId and region are required");
@@ -1485,7 +1498,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1485
1498
  }),
1486
1499
 
1487
1500
  /** Update a block's content (re-validated against its type's field schema). */
1488
- updateBlock: mutation(async (ctx, input: { blockId: string; fields?: Record<string, unknown>; title?: string }) => {
1501
+ updateBlock: mutation(async (ctx, input: { blockId: string; fields?: FieldValues; title?: string }) => {
1489
1502
  const db = cdb(ctx);
1490
1503
  const rows = await db.find({ from: "cms_blocks", where: { id: input.blockId }, limit: 1 });
1491
1504
  const block = rows[0];
@@ -1502,7 +1515,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1502
1515
  return db.update("cms_blocks", input.blockId, patch);
1503
1516
  }, {
1504
1517
  ...editor,
1505
- input: (raw): { blockId: string; fields?: Record<string, unknown>; title?: string } => {
1518
+ input: (raw): { blockId: string; fields?: FieldValues; title?: string } => {
1506
1519
  const o = asObj(raw);
1507
1520
  if (typeof o.blockId !== "string") throw new BadRequest("blockId is required");
1508
1521
  return o as never;
@@ -2089,12 +2102,12 @@ export function robotsTxt(opts: { origin: string; disallow?: string[] }): string
2089
2102
 
2090
2103
  // Minimal shape of a pramen public route (see @pramen/server/worker app.routes).
2091
2104
  interface RouteCtx {
2092
- callPrivileged: (opts: { name: string; input?: unknown; tenant?: string; roles?: string[] }) => Promise<Response>;
2105
+ callPrivileged: (opts: { name: string; input?: JsonValue; tenant?: string; roles?: string[] }) => Promise<Response>;
2093
2106
  }
2094
2107
  interface CmsRoute {
2095
2108
  method: string;
2096
2109
  path: string;
2097
- handler: (request: Request, env: Readonly<Record<string, unknown>>, ctx: RouteCtx) => Promise<Response>;
2110
+ handler: (request: Request, env: EnvBag, ctx: RouteCtx) => Promise<Response>;
2098
2111
  }
2099
2112
 
2100
2113
  /** Turnkey public routes for `GET /sitemap.xml` and `GET /robots.txt`. Spread into