@beechcms/core 0.6.0-preview.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/dist/automations/automations-grammar-words.d.ts.map +1 -0
  2. package/dist/automations/automations.repository.interface.d.ts.map +1 -0
  3. package/dist/automations/automations.runner.interface.d.ts.map +1 -0
  4. package/dist/automations/automations.types.d.ts.map +1 -0
  5. package/dist/common/clock.d.ts.map +1 -0
  6. package/dist/common/email.provider.d.ts +32 -0
  7. package/dist/common/email.provider.d.ts.map +1 -0
  8. package/dist/common/email.provider.js +4 -0
  9. package/dist/{hooks.d.ts → common/hooks.d.ts} +2 -2
  10. package/dist/common/hooks.d.ts.map +1 -0
  11. package/dist/common/id-generator.d.ts.map +1 -0
  12. package/dist/common/storage.d.ts.map +1 -0
  13. package/dist/{content-scan.repository.d.ts → content/content-scan.repository.d.ts} +1 -1
  14. package/dist/content/content-scan.repository.d.ts.map +1 -0
  15. package/dist/{content.repository.d.ts → content/content.repository.d.ts} +13 -4
  16. package/dist/content/content.repository.d.ts.map +1 -0
  17. package/dist/content/demo-data.repository.d.ts.map +1 -0
  18. package/dist/content/idempotency.repository.d.ts.map +1 -0
  19. package/dist/content/richtext/richtext-render.d.ts +12 -0
  20. package/dist/content/richtext/richtext-render.d.ts.map +1 -0
  21. package/dist/{richtext-render.js → content/richtext/richtext-render.js} +7 -14
  22. package/dist/content/richtext/richtext.d.ts.map +1 -0
  23. package/dist/{seed.repository.d.ts → content/seed.repository.d.ts} +1 -1
  24. package/dist/content/seed.repository.d.ts.map +1 -0
  25. package/dist/content/slug-utils.d.ts.map +1 -0
  26. package/dist/{dashboard-layout.d.ts → dashboard-layout/dashboard-layout.d.ts} +1 -1
  27. package/dist/dashboard-layout/dashboard-layout.d.ts.map +1 -0
  28. package/dist/dashboard-layout/dashboard-layout.repository.d.ts.map +1 -0
  29. package/dist/dashboard-layout/dashboard-permissions.d.ts.map +1 -0
  30. package/dist/dashboard-layout/dashboard-scopes.d.ts.map +1 -0
  31. package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts +19 -0
  32. package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts.map +1 -0
  33. package/dist/dashboard-layout/kanban/kanban.d.ts +75 -0
  34. package/dist/dashboard-layout/kanban/kanban.d.ts.map +1 -0
  35. package/dist/dashboard-layout/kanban/kanban.js +82 -0
  36. package/dist/dashboard-layout/layout-permissions.d.ts.map +1 -0
  37. package/dist/{seed-layout.d.ts → dashboard-layout/seed-layout.d.ts} +77 -3
  38. package/dist/dashboard-layout/seed-layout.d.ts.map +1 -0
  39. package/dist/{seed-layout.js → dashboard-layout/seed-layout.js} +101 -8
  40. package/dist/{seed-layout.repository.d.ts → dashboard-layout/seed-layout.repository.d.ts} +5 -1
  41. package/dist/dashboard-layout/seed-layout.repository.d.ts.map +1 -0
  42. package/dist/dashboard-layout/view-authorization.d.ts +18 -0
  43. package/dist/dashboard-layout/view-authorization.d.ts.map +1 -0
  44. package/dist/dashboard-layout/view-authorization.js +28 -0
  45. package/dist/engine/ddl.d.ts +173 -0
  46. package/dist/engine/ddl.d.ts.map +1 -0
  47. package/dist/engine/ddl.js +454 -0
  48. package/dist/engine/define-seed.d.ts.map +1 -0
  49. package/dist/engine/engine.d.ts +4 -0
  50. package/dist/engine/engine.d.ts.map +1 -0
  51. package/dist/engine/engine.js +4 -0
  52. package/dist/engine/policies.d.ts.map +1 -0
  53. package/dist/engine/query.d.ts +23 -0
  54. package/dist/engine/query.d.ts.map +1 -0
  55. package/dist/engine/query.js +208 -0
  56. package/dist/engine/relations.d.ts.map +1 -0
  57. package/dist/{schema-mutator.d.ts → engine/schema-mutator.d.ts} +4 -0
  58. package/dist/engine/schema-mutator.d.ts.map +1 -0
  59. package/dist/engine/seed-ddl-destructive.d.ts.map +1 -0
  60. package/dist/engine/seed-ddl.d.ts.map +1 -0
  61. package/dist/engine/seed-registry.d.ts.map +1 -0
  62. package/dist/{seed-registry.js → engine/seed-registry.js} +1 -1
  63. package/dist/engine/seed-types-generator.d.ts +10 -0
  64. package/dist/engine/seed-types-generator.d.ts.map +1 -0
  65. package/dist/engine/seed-types-generator.js +86 -0
  66. package/dist/engine/seed-validation.d.ts.map +1 -0
  67. package/dist/{seed-validation.js → engine/seed-validation.js} +14 -1
  68. package/dist/engine/seeds.d.ts.map +1 -0
  69. package/dist/engine/serialize.d.ts +20 -0
  70. package/dist/engine/serialize.d.ts.map +1 -0
  71. package/dist/engine/serialize.js +160 -0
  72. package/dist/{types.d.ts → engine/types.d.ts} +102 -63
  73. package/dist/engine/types.d.ts.map +1 -0
  74. package/dist/engine/validation/cache.d.ts +12 -0
  75. package/dist/engine/validation/cache.d.ts.map +1 -0
  76. package/dist/engine/validation/cache.js +115 -0
  77. package/dist/engine/validation/file-branch.d.ts +48 -0
  78. package/dist/engine/validation/file-branch.d.ts.map +1 -0
  79. package/dist/engine/validation/file-branch.js +127 -0
  80. package/dist/engine/validation/index.d.ts +115 -0
  81. package/dist/engine/validation/index.d.ts.map +1 -0
  82. package/dist/engine/validation/index.js +333 -0
  83. package/dist/engine/validation/primitives.d.ts +33 -0
  84. package/dist/engine/validation/primitives.d.ts.map +1 -0
  85. package/dist/engine/validation/primitives.js +43 -0
  86. package/dist/engine/validation/richtext-sanitizer.d.ts +30 -0
  87. package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -0
  88. package/dist/engine/validation/richtext-sanitizer.js +193 -0
  89. package/dist/engine/validation/schema-builders.d.ts +12 -0
  90. package/dist/engine/validation/schema-builders.d.ts.map +1 -0
  91. package/dist/engine/validation/schema-builders.js +322 -0
  92. package/dist/index.d.ts +48 -44
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +48 -44
  95. package/dist/media/file-types.d.ts +13 -0
  96. package/dist/media/file-types.d.ts.map +1 -0
  97. package/dist/media/media.repository.d.ts.map +1 -0
  98. package/dist/queue/queue.interface.d.ts +39 -0
  99. package/dist/queue/queue.interface.d.ts.map +1 -0
  100. package/dist/queue/queue.stub.d.ts +6 -0
  101. package/dist/queue/queue.stub.d.ts.map +1 -0
  102. package/dist/queue/queue.stub.js +8 -0
  103. package/dist/queue/scheduler.interface.d.ts.map +1 -0
  104. package/dist/queue/scheduler.interface.js +3 -0
  105. package/dist/queue/scheduler.stub.d.ts.map +1 -0
  106. package/dist/queue.interface.d.ts +4 -4
  107. package/dist/queue.interface.d.ts.map +1 -1
  108. package/dist/queue.stub.d.ts.map +1 -1
  109. package/dist/rate-limit/rate-limiter.d.ts +5 -0
  110. package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
  111. package/dist/search/search.repository.d.ts +1 -1
  112. package/dist/search/search.repository.d.ts.map +1 -1
  113. package/dist/seed-types-generator.d.ts +1 -1
  114. package/dist/seed-types-generator.d.ts.map +1 -1
  115. package/dist/settings/site-settings.repository.d.ts.map +1 -0
  116. package/dist/settings/site-settings.repository.js +3 -0
  117. package/dist/webhooks/webhook-crypto.d.ts +8 -0
  118. package/dist/webhooks/webhook-crypto.d.ts.map +1 -0
  119. package/dist/webhooks/webhook-crypto.js +37 -0
  120. package/dist/webhooks/webhook-validation.d.ts.map +1 -0
  121. package/dist/widget/widget.repository.d.ts +1 -1
  122. package/dist/widget/widget.repository.d.ts.map +1 -1
  123. package/package.json +4 -3
  124. package/dist/automations-grammar-words.d.ts.map +0 -1
  125. package/dist/automations.repository.interface.d.ts.map +0 -1
  126. package/dist/automations.runner.interface.d.ts.map +0 -1
  127. package/dist/automations.runner.stub.d.ts +0 -5
  128. package/dist/automations.runner.stub.d.ts.map +0 -1
  129. package/dist/automations.runner.stub.js +0 -7
  130. package/dist/automations.types.d.ts.map +0 -1
  131. package/dist/clock.d.ts.map +0 -1
  132. package/dist/content-scan.repository.d.ts.map +0 -1
  133. package/dist/content.repository.d.ts.map +0 -1
  134. package/dist/dashboard-layout.d.ts.map +0 -1
  135. package/dist/dashboard-layout.repository.d.ts.map +0 -1
  136. package/dist/dashboard-permissions.d.ts.map +0 -1
  137. package/dist/dashboard-scopes.d.ts.map +0 -1
  138. package/dist/define-seed.d.ts.map +0 -1
  139. package/dist/demo-data.repository.d.ts.map +0 -1
  140. package/dist/engine.d.ts +0 -144
  141. package/dist/engine.d.ts.map +0 -1
  142. package/dist/engine.js +0 -723
  143. package/dist/file-types.d.ts +0 -13
  144. package/dist/file-types.d.ts.map +0 -1
  145. package/dist/hooks.d.ts.map +0 -1
  146. package/dist/id-generator.d.ts.map +0 -1
  147. package/dist/idempotency.repository.d.ts.map +0 -1
  148. package/dist/layout-permissions.d.ts.map +0 -1
  149. package/dist/media.repository.d.ts.map +0 -1
  150. package/dist/policies.d.ts.map +0 -1
  151. package/dist/relations.d.ts.map +0 -1
  152. package/dist/richtext-render.d.ts +0 -11
  153. package/dist/richtext-render.d.ts.map +0 -1
  154. package/dist/richtext.d.ts.map +0 -1
  155. package/dist/scheduler.interface.d.ts.map +0 -1
  156. package/dist/scheduler.stub.d.ts.map +0 -1
  157. package/dist/schema-mutator.d.ts.map +0 -1
  158. package/dist/seed-ddl-destructive.d.ts.map +0 -1
  159. package/dist/seed-ddl.d.ts.map +0 -1
  160. package/dist/seed-layout.d.ts.map +0 -1
  161. package/dist/seed-layout.repository.d.ts.map +0 -1
  162. package/dist/seed-registry.d.ts.map +0 -1
  163. package/dist/seed-validation.d.ts.map +0 -1
  164. package/dist/seed.repository.d.ts.map +0 -1
  165. package/dist/seeds.d.ts.map +0 -1
  166. package/dist/site-settings.repository.d.ts.map +0 -1
  167. package/dist/slug-utils.d.ts.map +0 -1
  168. package/dist/storage.d.ts.map +0 -1
  169. package/dist/types.d.ts.map +0 -1
  170. package/dist/validation.d.ts +0 -50
  171. package/dist/validation.d.ts.map +0 -1
  172. package/dist/validation.js +0 -659
  173. package/dist/webhook-validation.d.ts.map +0 -1
  174. /package/dist/{automations-grammar-words.d.ts → automations/automations-grammar-words.d.ts} +0 -0
  175. /package/dist/{automations-grammar-words.js → automations/automations-grammar-words.js} +0 -0
  176. /package/dist/{automations.repository.interface.d.ts → automations/automations.repository.interface.d.ts} +0 -0
  177. /package/dist/{automations.repository.interface.js → automations/automations.repository.interface.js} +0 -0
  178. /package/dist/{automations.runner.interface.d.ts → automations/automations.runner.interface.d.ts} +0 -0
  179. /package/dist/{automations.runner.interface.js → automations/automations.runner.interface.js} +0 -0
  180. /package/dist/{automations.types.d.ts → automations/automations.types.d.ts} +0 -0
  181. /package/dist/{automations.types.js → automations/automations.types.js} +0 -0
  182. /package/dist/{clock.d.ts → common/clock.d.ts} +0 -0
  183. /package/dist/{clock.js → common/clock.js} +0 -0
  184. /package/dist/{hooks.js → common/hooks.js} +0 -0
  185. /package/dist/{id-generator.d.ts → common/id-generator.d.ts} +0 -0
  186. /package/dist/{id-generator.js → common/id-generator.js} +0 -0
  187. /package/dist/{storage.d.ts → common/storage.d.ts} +0 -0
  188. /package/dist/{storage.js → common/storage.js} +0 -0
  189. /package/dist/{content-scan.repository.js → content/content-scan.repository.js} +0 -0
  190. /package/dist/{content.repository.js → content/content.repository.js} +0 -0
  191. /package/dist/{demo-data.repository.d.ts → content/demo-data.repository.d.ts} +0 -0
  192. /package/dist/{demo-data.repository.js → content/demo-data.repository.js} +0 -0
  193. /package/dist/{idempotency.repository.d.ts → content/idempotency.repository.d.ts} +0 -0
  194. /package/dist/{idempotency.repository.js → content/idempotency.repository.js} +0 -0
  195. /package/dist/{richtext.d.ts → content/richtext/richtext.d.ts} +0 -0
  196. /package/dist/{richtext.js → content/richtext/richtext.js} +0 -0
  197. /package/dist/{seed.repository.js → content/seed.repository.js} +0 -0
  198. /package/dist/{slug-utils.d.ts → content/slug-utils.d.ts} +0 -0
  199. /package/dist/{slug-utils.js → content/slug-utils.js} +0 -0
  200. /package/dist/{dashboard-layout.js → dashboard-layout/dashboard-layout.js} +0 -0
  201. /package/dist/{dashboard-layout.repository.d.ts → dashboard-layout/dashboard-layout.repository.d.ts} +0 -0
  202. /package/dist/{dashboard-layout.repository.js → dashboard-layout/dashboard-layout.repository.js} +0 -0
  203. /package/dist/{dashboard-permissions.d.ts → dashboard-layout/dashboard-permissions.d.ts} +0 -0
  204. /package/dist/{dashboard-permissions.js → dashboard-layout/dashboard-permissions.js} +0 -0
  205. /package/dist/{dashboard-scopes.d.ts → dashboard-layout/dashboard-scopes.d.ts} +0 -0
  206. /package/dist/{dashboard-scopes.js → dashboard-layout/dashboard-scopes.js} +0 -0
  207. /package/dist/{media.repository.js → dashboard-layout/kanban/kanban-position.repository.js} +0 -0
  208. /package/dist/{layout-permissions.d.ts → dashboard-layout/layout-permissions.d.ts} +0 -0
  209. /package/dist/{layout-permissions.js → dashboard-layout/layout-permissions.js} +0 -0
  210. /package/dist/{seed-layout.repository.js → dashboard-layout/seed-layout.repository.js} +0 -0
  211. /package/dist/{define-seed.d.ts → engine/define-seed.d.ts} +0 -0
  212. /package/dist/{define-seed.js → engine/define-seed.js} +0 -0
  213. /package/dist/{policies.d.ts → engine/policies.d.ts} +0 -0
  214. /package/dist/{policies.js → engine/policies.js} +0 -0
  215. /package/dist/{relations.d.ts → engine/relations.d.ts} +0 -0
  216. /package/dist/{relations.js → engine/relations.js} +0 -0
  217. /package/dist/{schema-mutator.js → engine/schema-mutator.js} +0 -0
  218. /package/dist/{seed-ddl-destructive.d.ts → engine/seed-ddl-destructive.d.ts} +0 -0
  219. /package/dist/{seed-ddl-destructive.js → engine/seed-ddl-destructive.js} +0 -0
  220. /package/dist/{seed-ddl.d.ts → engine/seed-ddl.d.ts} +0 -0
  221. /package/dist/{seed-ddl.js → engine/seed-ddl.js} +0 -0
  222. /package/dist/{seed-registry.d.ts → engine/seed-registry.d.ts} +0 -0
  223. /package/dist/{seed-validation.d.ts → engine/seed-validation.d.ts} +0 -0
  224. /package/dist/{seeds.d.ts → engine/seeds.d.ts} +0 -0
  225. /package/dist/{seeds.js → engine/seeds.js} +0 -0
  226. /package/dist/{types.js → engine/types.js} +0 -0
  227. /package/dist/{file-types.js → media/file-types.js} +0 -0
  228. /package/dist/{media.repository.d.ts → media/media.repository.d.ts} +0 -0
  229. /package/dist/{scheduler.interface.js → media/media.repository.js} +0 -0
  230. /package/dist/{site-settings.repository.js → queue/queue.interface.js} +0 -0
  231. /package/dist/{scheduler.interface.d.ts → queue/scheduler.interface.d.ts} +0 -0
  232. /package/dist/{scheduler.stub.d.ts → queue/scheduler.stub.d.ts} +0 -0
  233. /package/dist/{scheduler.stub.js → queue/scheduler.stub.js} +0 -0
  234. /package/dist/{site-settings.repository.d.ts → settings/site-settings.repository.d.ts} +0 -0
  235. /package/dist/{webhook-validation.d.ts → webhooks/webhook-validation.d.ts} +0 -0
  236. /package/dist/{webhook-validation.js → webhooks/webhook-validation.js} +0 -0
@@ -0,0 +1,115 @@
1
+ import type { Seed } from '../types.js';
2
+ import type { IIdGenerator } from '../../common/id-generator.js';
3
+ export { resolveFileOptions } from './file-branch.js';
4
+ /**
5
+ * @module Engine/Validation
6
+ *
7
+ * Provides schema-driven validation and sanitization utilities for seed payloads
8
+ * within BeechCMS. This module handles type checks, required fields, text limits,
9
+ * rich text sanitization, relational key checks, and format constraints using Zod schemas.
10
+ */
11
+ /**
12
+ * Represents a single validation error detail returned when a seed payload field fails validation.
13
+ */
14
+ export interface ValidationDetail {
15
+ /** The name of the alias or field that failed validation. */
16
+ field: string;
17
+ /** The expected type, format, or constraint (e.g., `'string'`, `'number(min:1)'`, `'required-field'`). */
18
+ expected: string;
19
+ /** The type or status of the value actually received (e.g., `'null'`, `'array'`, `'missing'`). */
20
+ received: string;
21
+ /** A human-readable error message explaining why validation failed. */
22
+ message: string;
23
+ }
24
+ /**
25
+ * Options to configure the validation and sanitization behavior of the seed payload.
26
+ */
27
+ export interface ValidateSeedPayloadOptions {
28
+ /**
29
+ * Whether to allow fields to be explicitly `null`.
30
+ * @default false
31
+ */
32
+ allowNull?: boolean;
33
+ /**
34
+ * The type of operation being validated, which determines whether `requiredOnCreate`
35
+ * or `requiredOnUpdate` branch constraints are applied.
36
+ * @default 'create'
37
+ */
38
+ operation?: 'create' | 'update';
39
+ /**
40
+ * If true, validation will fail if the resulting data payload contains no valid fields.
41
+ * @default true
42
+ */
43
+ requireAtLeastOneValidField?: boolean;
44
+ /**
45
+ * Whether to enforce validation of required fields (`requiredOnCreate` or `requiredOnUpdate`).
46
+ * @default true
47
+ */
48
+ enforceRequiredFields?: boolean;
49
+ /**
50
+ * The maximum allowed length (in characters/bytes) for text and rich text fields.
51
+ * @default 50000
52
+ */
53
+ maxTextLength?: number;
54
+ /**
55
+ * Required when the seed has branches of type `'relation'`.
56
+ * Must be the same `IIdGenerator` instance used for id generation so that
57
+ * swapping implementations (e.g. ULIDs) automatically updates validation.
58
+ * Do NOT pass a concrete class — inject via the middleware / factory.
59
+ */
60
+ idGenerator?: IIdGenerator;
61
+ }
62
+ /**
63
+ * The structured result returned by the seed payload validation process.
64
+ */
65
+ export interface ValidateSeedPayloadResult {
66
+ /**
67
+ * The successfully validated and sanitized fields.
68
+ * Only contains valid fields that matched the seed branches.
69
+ */
70
+ data: Record<string, unknown>;
71
+ /** Detailed information about all validation errors or unrecognized fields. */
72
+ details: ValidationDetail[];
73
+ /** List of field aliases that were present in the payload but are not defined in the seed. */
74
+ unknownAliases: string[];
75
+ /** List of fields containing potentially dangerous rich text content (e.g. XSS vectors). */
76
+ dangerousFields: string[];
77
+ /** List of required fields that were missing or effectively empty. */
78
+ requiredFieldsMissing: string[];
79
+ /** Flag indicating if the validated data payload contains at least one valid field. */
80
+ hasAnyValidField: boolean;
81
+ }
82
+ /**
83
+ * Internal representation of fully resolved validation options with default values applied.
84
+ */
85
+ export type ResolvedOptions = {
86
+ allowNull: boolean;
87
+ operation: 'create' | 'update';
88
+ requireAtLeastOneValidField: boolean;
89
+ enforceRequiredFields: boolean;
90
+ maxTextLength: number;
91
+ idGenerator: IIdGenerator | undefined;
92
+ };
93
+ /** Set of valid content status values. */
94
+ declare const STATUS_VALUES: readonly ['draft', 'review', 'published'];
95
+ /**
96
+ * Validates and sanitizes a seed payload against its schema definition.
97
+ *
98
+ * This is the public entry point for seed payload validation. It strips unknown
99
+ * aliases, coerces types, checks required fields on create/update, checks limits,
100
+ * and performs rich text XSS checks without throwing exceptions.
101
+ *
102
+ * @param seed - The seed definition containing field/branch definitions.
103
+ * @param payload - The raw payload values to validate and sanitize.
104
+ * @param options - Validation settings to override defaults.
105
+ * @returns A structured validation result containing parsed data, details of any issues, and flags.
106
+ */
107
+ export declare function validateAndSanitizeSeedPayload(seed: Seed, payload: Record<string, unknown>, options?: ValidateSeedPayloadOptions): ValidateSeedPayloadResult;
108
+ /**
109
+ * Type-guard validating if a value is a valid content status ('draft', 'review', or 'published').
110
+ *
111
+ * @param value - The value to check.
112
+ * @returns True if the value is a valid content status, false otherwise.
113
+ */
114
+ export declare function isValidContentStatus(value: unknown): value is (typeof STATUS_VALUES)[number];
115
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAOhE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAErD;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb,0GAA0G;IAC1G,QAAQ,EAAE,MAAM,CAAA;IAChB,kGAAkG;IAClG,QAAQ,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC/B;;;OAGG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAA;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,+EAA+E;IAC/E,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,8FAA8F;IAC9F,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,4FAA4F;IAC5F,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,sEAAsE;IACtE,qBAAqB,EAAE,MAAM,EAAE,CAAA;IAC/B,uFAAuF;IACvF,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC9B,2BAA2B,EAAE,OAAO,CAAA;IACpC,qBAAqB,EAAE,OAAO,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,YAAY,GAAG,SAAS,CAAA;CACtC,CAAA;AAKD,0CAA0C;AAC1C,QAAA,MAAM,aAAa,YAAI,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAU,CAAA;AAkR/D;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,GAAE,0BAA+B,GACvC,yBAAyB,CAqD3B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAE5F"}
@@ -0,0 +1,333 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { z } from 'zod';
4
+ import { isRichtextEnvelopeV1 } from '../../content/richtext/richtext.js';
5
+ import { cleanString, isPlainObject } from './primitives.js';
6
+ import { isRichtextDocEmpty } from './richtext-sanitizer.js';
7
+ import { compileSeedSchema } from './cache.js';
8
+ // Re-export the public file-branch symbol so the barrel surface stays complete.
9
+ export { resolveFileOptions } from './file-branch.js';
10
+ /** Default maximum character length allowed for text and rich text branches. */
11
+ const DEFAULT_MAX_TEXT_LENGTH = 50_000;
12
+ /** Set of valid content status values. */
13
+ const STATUS_VALUES = ['draft', 'review', 'published'];
14
+ /** Zod schema for content status enum validation. */
15
+ const statusSchema = z.enum(STATUS_VALUES);
16
+ /**
17
+ * Checks if a value is effectively empty (e.g. null, undefined, empty string, empty array, or empty rich text).
18
+ *
19
+ * @param value - The value to check.
20
+ * @returns True if effectively empty, false otherwise.
21
+ */
22
+ function isEffectivelyEmpty(value) {
23
+ if (value === null || value === undefined)
24
+ return true;
25
+ if (typeof value === 'string')
26
+ return cleanString(value).length === 0;
27
+ if (Array.isArray(value))
28
+ return value.length === 0;
29
+ if (isPlainObject(value)) {
30
+ if (isRichtextEnvelopeV1(value))
31
+ return isRichtextDocEmpty(value.doc);
32
+ if (isRichtextDocEmpty(value))
33
+ return true;
34
+ return Object.keys(value).length === 0;
35
+ }
36
+ return false;
37
+ }
38
+ /**
39
+ * Detects missing or empty required fields based on whether the operation is create or update.
40
+ *
41
+ * @param seed - The seed definition.
42
+ * @param rawPayload - Raw inputs provided to validation.
43
+ * @param filtered - Payload with unknown aliases stripped.
44
+ * @param parsedData - Parsed and validated data fields.
45
+ * @param parseSucceeded - Whether Zod parsing succeeded.
46
+ * @param options - The resolved validation options.
47
+ * @returns An object containing lists of missing fields and structured validation details.
48
+ */
49
+ function detectMissingRequired(seed, rawPayload, filtered, parsedData, parseSucceeded, options) {
50
+ const missing = [];
51
+ const details = [];
52
+ const op = options.operation;
53
+ for (const branch of seed.branches) {
54
+ const isRequired = op === 'create' ? branch.requiredOnCreate : branch.requiredOnUpdate;
55
+ if (!isRequired)
56
+ continue;
57
+ if (!Object.hasOwn(rawPayload, branch.alias)) {
58
+ missing.push(branch.alias);
59
+ details.push({
60
+ field: branch.alias,
61
+ expected: 'required-field',
62
+ received: 'missing',
63
+ message: `Field '${branch.alias}' is required for ${op}`,
64
+ });
65
+ continue;
66
+ }
67
+ const candidate = parseSucceeded ? parsedData[branch.alias] : filtered[branch.alias];
68
+ if (isEffectivelyEmpty(candidate)) {
69
+ missing.push(branch.alias);
70
+ details.push({
71
+ field: branch.alias,
72
+ expected: 'required-field',
73
+ received: 'empty',
74
+ message: `Field '${branch.alias}' cannot be empty for ${op}`,
75
+ });
76
+ }
77
+ }
78
+ return { missing, details };
79
+ }
80
+ /**
81
+ * Returns a string representing the runtime type of a value for error reporting.
82
+ *
83
+ * @param value - The value to describe.
84
+ * @returns A string description of the value type (e.g. 'null', 'array', 'string').
85
+ */
86
+ function describeReceivedType(value) {
87
+ if (value === null)
88
+ return 'null';
89
+ if (Array.isArray(value))
90
+ return 'array';
91
+ return typeof value;
92
+ }
93
+ /**
94
+ * Builds a dotted/bracketed field path from a Zod issue path (e.g. 'items[0].name').
95
+ *
96
+ * @param path - Zod issue path segments.
97
+ * @returns The full field path as a string, or 'payload' if the path is empty.
98
+ */
99
+ function buildFieldPath(path) {
100
+ if (path.length === 0)
101
+ return 'payload';
102
+ return path.reduce((result, segment, index) => {
103
+ if (index === 0)
104
+ return String(segment);
105
+ return typeof segment === 'number' ? `${result}[${segment}]` : `${result}.${String(segment)}`;
106
+ }, '');
107
+ }
108
+ /**
109
+ * Navigates a payload object by a Zod issue path to find the actual value that failed validation.
110
+ *
111
+ * @param filtered - Payload with unknown aliases stripped.
112
+ * @param path - Zod issue path segments.
113
+ * @returns The value found at the path, or undefined if unreachable.
114
+ */
115
+ function resolveByPath(filtered, path) {
116
+ let current = filtered;
117
+ for (const segment of path) {
118
+ if (current === null || current === undefined)
119
+ return current;
120
+ if (typeof segment === 'number') {
121
+ if (!Array.isArray(current))
122
+ return undefined;
123
+ current = current[segment];
124
+ }
125
+ else {
126
+ if (typeof current !== 'object')
127
+ return undefined;
128
+ current = current[segment];
129
+ }
130
+ }
131
+ return current;
132
+ }
133
+ /**
134
+ * Formats expected error messages based on validation failure details.
135
+ *
136
+ * Zod v4 native `invalid_type` issues carry the expected type on `issue.expected`
137
+ * directly. Custom `.refine`/`ctx.addIssue` checks in this codebase still encode
138
+ * it as an `"Expected <type>"` message prefix, so that convention is kept as a fallback.
139
+ *
140
+ * @param issue - The Zod issue to derive the expected type from.
141
+ * @returns The expected type format string.
142
+ */
143
+ function expectedFromIssue(issue) {
144
+ if (issue.code === 'invalid_type')
145
+ return issue.expected;
146
+ const message = issue.message;
147
+ if (typeof message !== 'string')
148
+ return 'valid-field-value';
149
+ return message.startsWith('Expected ') ? message.slice('Expected '.length) : 'valid-field-value';
150
+ }
151
+ /**
152
+ * Separates known fields (present in seed definition) from unknown fields in a raw payload.
153
+ *
154
+ * @param seed - The seed definition.
155
+ * @param payload - The raw payload.
156
+ * @returns An object with filtered payload, array of unknown keys, and detail issues.
157
+ */
158
+ function splitUnknownAliases(seed, payload) {
159
+ const knownAliases = new Set(seed.branches.map((branch) => branch.alias));
160
+ const filtered = {};
161
+ const unknown = [];
162
+ const details = [];
163
+ for (const alias of Object.keys(payload)) {
164
+ if (knownAliases.has(alias)) {
165
+ filtered[alias] = payload[alias];
166
+ continue;
167
+ }
168
+ unknown.push(alias);
169
+ details.push({
170
+ field: alias,
171
+ expected: 'known-seed-alias',
172
+ received: 'unknown-alias',
173
+ message: `Field '${alias}' is not defined in seed '${seed.slug}'`,
174
+ });
175
+ }
176
+ return { filtered, unknown, details };
177
+ }
178
+ /**
179
+ * Recursively flattens Zod union validation issues to report underlying errors.
180
+ *
181
+ * @param issues - Raw Zod issues.
182
+ * @param parentPath - Path context for nested structures.
183
+ * @returns Flattened array of Zod issues.
184
+ */
185
+ function flattenZodIssues(issues, parentPath = []) {
186
+ const result = [];
187
+ for (const issue of issues) {
188
+ const issuePath = issue.path;
189
+ const currentPath = [...parentPath, ...issuePath];
190
+ if (issue.code === 'invalid_union' && 'errors' in issue) {
191
+ const unionErrors = issue.errors;
192
+ for (const subIssues of unionErrors) {
193
+ result.push(...flattenZodIssues(subIssues, currentPath));
194
+ }
195
+ }
196
+ else {
197
+ result.push({
198
+ ...issue,
199
+ path: currentPath,
200
+ });
201
+ }
202
+ }
203
+ return result;
204
+ }
205
+ /**
206
+ * Map Zod issues into structured ValidationDetail objects, identifying dangerous or unknown fields.
207
+ *
208
+ * @param seed - The seed definition.
209
+ * @param issues - Raw Zod issues.
210
+ * @param filtered - Payload with unknown aliases stripped.
211
+ * @param options - The resolved validation options.
212
+ * @returns Structured details, unknown lists, and dangerous lists.
213
+ */
214
+ function processZodIssues(seed, issues, filtered, options) {
215
+ const details = [];
216
+ const unknown = [];
217
+ const dangerous = [];
218
+ const flatIssues = flattenZodIssues(issues);
219
+ for (const issue of flatIssues) {
220
+ if (issue.code === 'unrecognized_keys') {
221
+ for (const alias of issue.keys) {
222
+ unknown.push(alias);
223
+ details.push({
224
+ field: alias,
225
+ expected: 'known-seed-alias',
226
+ received: 'unknown-alias',
227
+ message: `Field '${alias}' is not defined in seed '${seed.slug}'`,
228
+ });
229
+ }
230
+ continue;
231
+ }
232
+ const issuePath = issue.path;
233
+ // detectMissingRequired() already reports top-level required-and-missing branches;
234
+ // skip the redundant native invalid_type/undefined issue Zod v4 raises for the same field.
235
+ if (issue.code === 'invalid_type' &&
236
+ options.enforceRequiredFields &&
237
+ issuePath.length === 1 &&
238
+ resolveByPath(filtered, issuePath) === undefined) {
239
+ continue;
240
+ }
241
+ const field = buildFieldPath(issuePath);
242
+ const params = issue.params;
243
+ if (params?.dangerous === true) {
244
+ dangerous.push(field);
245
+ }
246
+ const expected = expectedFromIssue(issue);
247
+ const received = describeReceivedType(resolveByPath(filtered, issuePath));
248
+ let message = `Field '${field}' expects type '${expected}' but received '${received}'`;
249
+ if (received === 'string' && typeof issue.message === 'string' && issue.message.startsWith('Expected ')) {
250
+ if (expected.includes('url') || expected.includes('string') || expected.includes('date') || expected.includes('file')) {
251
+ message = `Field '${field}' has invalid format. Expected '${expected}' but received a non-matching string.`;
252
+ }
253
+ }
254
+ details.push({
255
+ field,
256
+ expected,
257
+ received,
258
+ message,
259
+ });
260
+ }
261
+ return { details, unknown, dangerous };
262
+ }
263
+ /**
264
+ * Validates and sanitizes a seed payload against its schema definition.
265
+ *
266
+ * This is the public entry point for seed payload validation. It strips unknown
267
+ * aliases, coerces types, checks required fields on create/update, checks limits,
268
+ * and performs rich text XSS checks without throwing exceptions.
269
+ *
270
+ * @param seed - The seed definition containing field/branch definitions.
271
+ * @param payload - The raw payload values to validate and sanitize.
272
+ * @param options - Validation settings to override defaults.
273
+ * @returns A structured validation result containing parsed data, details of any issues, and flags.
274
+ */
275
+ export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
276
+ const resolved = {
277
+ allowNull: options.allowNull ?? false,
278
+ operation: options.operation ?? 'create',
279
+ requireAtLeastOneValidField: options.requireAtLeastOneValidField ?? true,
280
+ enforceRequiredFields: options.enforceRequiredFields ?? true,
281
+ maxTextLength: options.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH,
282
+ idGenerator: options.idGenerator,
283
+ };
284
+ const { filtered, unknown: preUnknown, details: preDetails } = splitUnknownAliases(seed, payload);
285
+ const schema = compileSeedSchema(seed, resolved);
286
+ const parsed = schema.safeParse(filtered);
287
+ const accumulatedDetails = [...preDetails];
288
+ const unknownAliases = new Set(preUnknown);
289
+ const dangerousFields = new Set();
290
+ let data = {};
291
+ if (parsed.success) {
292
+ data = { ...parsed.data };
293
+ }
294
+ else {
295
+ const zodOutcome = processZodIssues(seed, parsed.error.issues, filtered, resolved);
296
+ accumulatedDetails.push(...zodOutcome.details);
297
+ for (const alias of zodOutcome.unknown)
298
+ unknownAliases.add(alias);
299
+ for (const field of zodOutcome.dangerous)
300
+ dangerousFields.add(field);
301
+ }
302
+ let requiredFieldsMissing = [];
303
+ if (resolved.enforceRequiredFields) {
304
+ const requiredOutcome = detectMissingRequired(seed, payload, filtered, data, parsed.success, resolved);
305
+ requiredFieldsMissing = requiredOutcome.missing;
306
+ accumulatedDetails.push(...requiredOutcome.details);
307
+ }
308
+ if (resolved.requireAtLeastOneValidField && Object.keys(data).length === 0) {
309
+ accumulatedDetails.push({
310
+ field: 'data',
311
+ expected: 'at-least-one-valid-field',
312
+ received: 'empty',
313
+ message: 'Payload does not contain any valid fields for this operation',
314
+ });
315
+ }
316
+ return {
317
+ data,
318
+ details: accumulatedDetails,
319
+ unknownAliases: [...unknownAliases],
320
+ dangerousFields: [...dangerousFields],
321
+ requiredFieldsMissing,
322
+ hasAnyValidField: Object.keys(data).length > 0,
323
+ };
324
+ }
325
+ /**
326
+ * Type-guard validating if a value is a valid content status ('draft', 'review', or 'published').
327
+ *
328
+ * @param value - The value to check.
329
+ * @returns True if the value is a valid content status, false otherwise.
330
+ */
331
+ export function isValidContentStatus(value) {
332
+ return statusSchema.safeParse(value).success;
333
+ }
@@ -0,0 +1,33 @@
1
+ /** Matches standard control characters that should be stripped. */
2
+ export declare const CONTROL_CHARS_REGEX: RegExp;
3
+ /**
4
+ * Strips non-printable and dangerous control characters from a string.
5
+ *
6
+ * @param input - The string to clean.
7
+ * @returns The cleaned string.
8
+ */
9
+ export declare function stripControlChars(input: string): string;
10
+ /**
11
+ * Cleans a string by stripping control characters and trimming leading/trailing whitespace.
12
+ *
13
+ * @param input - The string to clean.
14
+ * @returns The cleaned and trimmed string.
15
+ */
16
+ export declare function cleanString(input: string): string;
17
+ /**
18
+ * Type-guard checking if a value is a plain object (excluding arrays and null).
19
+ *
20
+ * @param input - The value to check.
21
+ * @returns True if the value is a plain object, false otherwise.
22
+ */
23
+ export declare function isPlainObject(input: unknown): input is Record<string, unknown>;
24
+ /**
25
+ * Measures the real UTF-8 byte length of a string, not UTF-16 code units.
26
+ * `.length` undercounts multi-byte chars (CJK, emoji) by up to 4x, which matters
27
+ * for size limits meant to bound storage (D1/SQLite is byte-addressed).
28
+ *
29
+ * @param input - The string to measure.
30
+ * @returns The UTF-8 byte length.
31
+ */
32
+ export declare function byteLength(input: string): number;
33
+ //# sourceMappingURL=primitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/primitives.ts"],"names":[],"mappings":"AAGA,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,QAAoD,CAAA;AAEpF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE9E;AAID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD"}
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ /** Matches standard control characters that should be stripped. */
4
+ export const CONTROL_CHARS_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
5
+ /**
6
+ * Strips non-printable and dangerous control characters from a string.
7
+ *
8
+ * @param input - The string to clean.
9
+ * @returns The cleaned string.
10
+ */
11
+ export function stripControlChars(input) {
12
+ return input.replaceAll(CONTROL_CHARS_REGEX, '');
13
+ }
14
+ /**
15
+ * Cleans a string by stripping control characters and trimming leading/trailing whitespace.
16
+ *
17
+ * @param input - The string to clean.
18
+ * @returns The cleaned and trimmed string.
19
+ */
20
+ export function cleanString(input) {
21
+ return stripControlChars(input).trim();
22
+ }
23
+ /**
24
+ * Type-guard checking if a value is a plain object (excluding arrays and null).
25
+ *
26
+ * @param input - The value to check.
27
+ * @returns True if the value is a plain object, false otherwise.
28
+ */
29
+ export function isPlainObject(input) {
30
+ return typeof input === 'object' && input !== null && !Array.isArray(input);
31
+ }
32
+ const byteLengthEncoder = new TextEncoder();
33
+ /**
34
+ * Measures the real UTF-8 byte length of a string, not UTF-16 code units.
35
+ * `.length` undercounts multi-byte chars (CJK, emoji) by up to 4x, which matters
36
+ * for size limits meant to bound storage (D1/SQLite is byte-addressed).
37
+ *
38
+ * @param input - The string to measure.
39
+ * @returns The UTF-8 byte length.
40
+ */
41
+ export function byteLength(input) {
42
+ return byteLengthEncoder.encode(input).length;
43
+ }
@@ -0,0 +1,30 @@
1
+ /** Result structure of the rich text sanitization function. */
2
+ interface RichtextSanitizeResult {
3
+ /** The sanitized string or JSON object. */
4
+ value: unknown;
5
+ /** Flag indicating if any dangerous content was detected. */
6
+ dangerous: boolean;
7
+ /** Flag indicating if the overall structure is valid rich text. */
8
+ valid: boolean;
9
+ /** The size of the sanitized result. */
10
+ size: number;
11
+ oversize?: boolean;
12
+ }
13
+ /**
14
+ * Main entrance helper to sanitize rich text, handling both v1 envelope formats and raw JSON payloads.
15
+ * String-form input is rejected (JSON-only). Byte size is fail-fast checked before the sanitizing walk.
16
+ *
17
+ * @param raw - The raw rich text input.
18
+ * @param maxBytes - Maximum allowed serialized size, checked before the walk.
19
+ * @returns The sanitization result.
20
+ */
21
+ export declare function sanitizeRichtext(raw: unknown, maxBytes: number): RichtextSanitizeResult;
22
+ /**
23
+ * Checks if a rich text document is effectively empty (contains no text or LaTeX blocks).
24
+ *
25
+ * @param value - The rich text document structure.
26
+ * @returns True if empty, false otherwise.
27
+ */
28
+ export declare function isRichtextDocEmpty(value: unknown): boolean;
29
+ export {};
30
+ //# sourceMappingURL=richtext-sanitizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"richtext-sanitizer.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/richtext-sanitizer.ts"],"names":[],"mappings":"AA0BA,+DAA+D;AAC/D,UAAU,sBAAsB;IAC9B,2CAA2C;IAC3C,KAAK,EAAE,OAAO,CAAA;IACd,6DAA6D;IAC7D,SAAS,EAAE,OAAO,CAAA;IAClB,mEAAmE;IACnE,KAAK,EAAE,OAAO,CAAA;IACd,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAsGD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,sBAAsB,CAgCvF;AAyBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS1D"}