@beechcms/core 0.6.0-preview.3 → 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 (243) 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 -40
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +48 -40
  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.interface.js +3 -0
  101. package/dist/queue/queue.stub.d.ts +6 -0
  102. package/dist/queue/queue.stub.d.ts.map +1 -0
  103. package/dist/queue/queue.stub.js +8 -0
  104. package/dist/queue/scheduler.interface.d.ts.map +1 -0
  105. package/dist/queue/scheduler.interface.js +3 -0
  106. package/dist/queue/scheduler.stub.d.ts.map +1 -0
  107. package/dist/queue.interface.d.ts +39 -0
  108. package/dist/queue.interface.d.ts.map +1 -0
  109. package/dist/queue.stub.d.ts +6 -0
  110. package/dist/queue.stub.d.ts.map +1 -0
  111. package/dist/queue.stub.js +8 -0
  112. package/dist/rate-limit/rate-limiter.d.ts +5 -0
  113. package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
  114. package/dist/search/search.repository.d.ts +1 -1
  115. package/dist/search/search.repository.d.ts.map +1 -1
  116. package/dist/seed-types-generator.d.ts +10 -0
  117. package/dist/seed-types-generator.d.ts.map +1 -0
  118. package/dist/seed-types-generator.js +86 -0
  119. package/dist/settings/site-settings.repository.d.ts.map +1 -0
  120. package/dist/settings/site-settings.repository.js +3 -0
  121. package/dist/webhook-crypto.d.ts +8 -0
  122. package/dist/webhook-crypto.d.ts.map +1 -0
  123. package/dist/webhook-crypto.js +37 -0
  124. package/dist/webhooks/webhook-crypto.d.ts +8 -0
  125. package/dist/webhooks/webhook-crypto.d.ts.map +1 -0
  126. package/dist/webhooks/webhook-crypto.js +37 -0
  127. package/dist/webhooks/webhook-validation.d.ts.map +1 -0
  128. package/dist/widget/widget.repository.d.ts +1 -1
  129. package/dist/widget/widget.repository.d.ts.map +1 -1
  130. package/package.json +6 -1
  131. package/dist/automations-grammar-words.d.ts.map +0 -1
  132. package/dist/automations.repository.interface.d.ts.map +0 -1
  133. package/dist/automations.runner.interface.d.ts.map +0 -1
  134. package/dist/automations.runner.stub.d.ts +0 -5
  135. package/dist/automations.runner.stub.d.ts.map +0 -1
  136. package/dist/automations.runner.stub.js +0 -7
  137. package/dist/automations.types.d.ts.map +0 -1
  138. package/dist/clock.d.ts.map +0 -1
  139. package/dist/content-scan.repository.d.ts.map +0 -1
  140. package/dist/content.repository.d.ts.map +0 -1
  141. package/dist/dashboard-layout.d.ts.map +0 -1
  142. package/dist/dashboard-layout.repository.d.ts.map +0 -1
  143. package/dist/dashboard-permissions.d.ts.map +0 -1
  144. package/dist/dashboard-scopes.d.ts.map +0 -1
  145. package/dist/define-seed.d.ts.map +0 -1
  146. package/dist/demo-data.repository.d.ts.map +0 -1
  147. package/dist/engine.d.ts +0 -144
  148. package/dist/engine.d.ts.map +0 -1
  149. package/dist/engine.js +0 -723
  150. package/dist/file-types.d.ts +0 -13
  151. package/dist/file-types.d.ts.map +0 -1
  152. package/dist/hooks.d.ts.map +0 -1
  153. package/dist/id-generator.d.ts.map +0 -1
  154. package/dist/idempotency.repository.d.ts.map +0 -1
  155. package/dist/layout-permissions.d.ts.map +0 -1
  156. package/dist/media.repository.d.ts.map +0 -1
  157. package/dist/policies.d.ts.map +0 -1
  158. package/dist/relations.d.ts.map +0 -1
  159. package/dist/richtext-render.d.ts +0 -11
  160. package/dist/richtext-render.d.ts.map +0 -1
  161. package/dist/richtext.d.ts.map +0 -1
  162. package/dist/scheduler.interface.d.ts.map +0 -1
  163. package/dist/scheduler.stub.d.ts.map +0 -1
  164. package/dist/schema-mutator.d.ts.map +0 -1
  165. package/dist/seed-ddl-destructive.d.ts.map +0 -1
  166. package/dist/seed-ddl.d.ts.map +0 -1
  167. package/dist/seed-layout.d.ts.map +0 -1
  168. package/dist/seed-layout.repository.d.ts.map +0 -1
  169. package/dist/seed-registry.d.ts.map +0 -1
  170. package/dist/seed-validation.d.ts.map +0 -1
  171. package/dist/seed.repository.d.ts.map +0 -1
  172. package/dist/seeds.d.ts.map +0 -1
  173. package/dist/site-settings.repository.d.ts.map +0 -1
  174. package/dist/slug-utils.d.ts.map +0 -1
  175. package/dist/storage.d.ts.map +0 -1
  176. package/dist/types.d.ts.map +0 -1
  177. package/dist/validation.d.ts +0 -50
  178. package/dist/validation.d.ts.map +0 -1
  179. package/dist/validation.js +0 -659
  180. package/dist/webhook-validation.d.ts.map +0 -1
  181. /package/dist/{automations-grammar-words.d.ts → automations/automations-grammar-words.d.ts} +0 -0
  182. /package/dist/{automations-grammar-words.js → automations/automations-grammar-words.js} +0 -0
  183. /package/dist/{automations.repository.interface.d.ts → automations/automations.repository.interface.d.ts} +0 -0
  184. /package/dist/{automations.repository.interface.js → automations/automations.repository.interface.js} +0 -0
  185. /package/dist/{automations.runner.interface.d.ts → automations/automations.runner.interface.d.ts} +0 -0
  186. /package/dist/{automations.runner.interface.js → automations/automations.runner.interface.js} +0 -0
  187. /package/dist/{automations.types.d.ts → automations/automations.types.d.ts} +0 -0
  188. /package/dist/{automations.types.js → automations/automations.types.js} +0 -0
  189. /package/dist/{clock.d.ts → common/clock.d.ts} +0 -0
  190. /package/dist/{clock.js → common/clock.js} +0 -0
  191. /package/dist/{hooks.js → common/hooks.js} +0 -0
  192. /package/dist/{id-generator.d.ts → common/id-generator.d.ts} +0 -0
  193. /package/dist/{id-generator.js → common/id-generator.js} +0 -0
  194. /package/dist/{storage.d.ts → common/storage.d.ts} +0 -0
  195. /package/dist/{storage.js → common/storage.js} +0 -0
  196. /package/dist/{content-scan.repository.js → content/content-scan.repository.js} +0 -0
  197. /package/dist/{content.repository.js → content/content.repository.js} +0 -0
  198. /package/dist/{demo-data.repository.d.ts → content/demo-data.repository.d.ts} +0 -0
  199. /package/dist/{demo-data.repository.js → content/demo-data.repository.js} +0 -0
  200. /package/dist/{idempotency.repository.d.ts → content/idempotency.repository.d.ts} +0 -0
  201. /package/dist/{idempotency.repository.js → content/idempotency.repository.js} +0 -0
  202. /package/dist/{richtext.d.ts → content/richtext/richtext.d.ts} +0 -0
  203. /package/dist/{richtext.js → content/richtext/richtext.js} +0 -0
  204. /package/dist/{seed.repository.js → content/seed.repository.js} +0 -0
  205. /package/dist/{slug-utils.d.ts → content/slug-utils.d.ts} +0 -0
  206. /package/dist/{slug-utils.js → content/slug-utils.js} +0 -0
  207. /package/dist/{dashboard-layout.js → dashboard-layout/dashboard-layout.js} +0 -0
  208. /package/dist/{dashboard-layout.repository.d.ts → dashboard-layout/dashboard-layout.repository.d.ts} +0 -0
  209. /package/dist/{dashboard-layout.repository.js → dashboard-layout/dashboard-layout.repository.js} +0 -0
  210. /package/dist/{dashboard-permissions.d.ts → dashboard-layout/dashboard-permissions.d.ts} +0 -0
  211. /package/dist/{dashboard-permissions.js → dashboard-layout/dashboard-permissions.js} +0 -0
  212. /package/dist/{dashboard-scopes.d.ts → dashboard-layout/dashboard-scopes.d.ts} +0 -0
  213. /package/dist/{dashboard-scopes.js → dashboard-layout/dashboard-scopes.js} +0 -0
  214. /package/dist/{media.repository.js → dashboard-layout/kanban/kanban-position.repository.js} +0 -0
  215. /package/dist/{layout-permissions.d.ts → dashboard-layout/layout-permissions.d.ts} +0 -0
  216. /package/dist/{layout-permissions.js → dashboard-layout/layout-permissions.js} +0 -0
  217. /package/dist/{seed-layout.repository.js → dashboard-layout/seed-layout.repository.js} +0 -0
  218. /package/dist/{define-seed.d.ts → engine/define-seed.d.ts} +0 -0
  219. /package/dist/{define-seed.js → engine/define-seed.js} +0 -0
  220. /package/dist/{policies.d.ts → engine/policies.d.ts} +0 -0
  221. /package/dist/{policies.js → engine/policies.js} +0 -0
  222. /package/dist/{relations.d.ts → engine/relations.d.ts} +0 -0
  223. /package/dist/{relations.js → engine/relations.js} +0 -0
  224. /package/dist/{schema-mutator.js → engine/schema-mutator.js} +0 -0
  225. /package/dist/{seed-ddl-destructive.d.ts → engine/seed-ddl-destructive.d.ts} +0 -0
  226. /package/dist/{seed-ddl-destructive.js → engine/seed-ddl-destructive.js} +0 -0
  227. /package/dist/{seed-ddl.d.ts → engine/seed-ddl.d.ts} +0 -0
  228. /package/dist/{seed-ddl.js → engine/seed-ddl.js} +0 -0
  229. /package/dist/{seed-registry.d.ts → engine/seed-registry.d.ts} +0 -0
  230. /package/dist/{seed-validation.d.ts → engine/seed-validation.d.ts} +0 -0
  231. /package/dist/{seeds.d.ts → engine/seeds.d.ts} +0 -0
  232. /package/dist/{seeds.js → engine/seeds.js} +0 -0
  233. /package/dist/{types.js → engine/types.js} +0 -0
  234. /package/dist/{file-types.js → media/file-types.js} +0 -0
  235. /package/dist/{media.repository.d.ts → media/media.repository.d.ts} +0 -0
  236. /package/dist/{scheduler.interface.js → media/media.repository.js} +0 -0
  237. /package/dist/{scheduler.interface.d.ts → queue/scheduler.interface.d.ts} +0 -0
  238. /package/dist/{scheduler.stub.d.ts → queue/scheduler.stub.d.ts} +0 -0
  239. /package/dist/{scheduler.stub.js → queue/scheduler.stub.js} +0 -0
  240. /package/dist/{site-settings.repository.js → queue.interface.js} +0 -0
  241. /package/dist/{site-settings.repository.d.ts → settings/site-settings.repository.d.ts} +0 -0
  242. /package/dist/{webhook-validation.d.ts → webhooks/webhook-validation.d.ts} +0 -0
  243. /package/dist/{webhook-validation.js → webhooks/webhook-validation.js} +0 -0
@@ -1,659 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
- import { z } from 'zod';
4
- import { RICHTEXT_SCHEMA_VERSION, isRichtextEnvelopeV1 } from './richtext.js';
5
- import { extensionFromUrl, isExtensionAccepted } from './file-types.js';
6
- const DEFAULT_MAX_TEXT_LENGTH = 50_000;
7
- const DEFAULT_FILE_MAX_SIZE = 5 * 1024 * 1024;
8
- const CONTROL_CHARS_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
9
- const DANGEROUS_TAG_REGEX = /<(script|iframe|object|embed)\b/i;
10
- const DANGEROUS_TAG_STRIP_REGEX = /<\/?(script|iframe|object|embed)[^>]*>/gi;
11
- const DANGEROUS_ATTR_REGEX = /\son[a-z]+\s*=/i;
12
- const DANGEROUS_HANDLER_STRIP_REGEX = /\son[a-z]+\s*=\s*(['"]).*?\1/gi;
13
- const DANGEROUS_PROTOCOL_REGEX = /^\s*javascript:/i;
14
- const FORBIDDEN_RICHTEXT_NODE_TYPES = new Set(['script', 'iframe', 'object', 'embed']);
15
- const URL_LIKE_RICHTEXT_KEYS = new Set(['href', 'src']);
16
- const STATUS_VALUES = ['draft', 'review', 'published'];
17
- const statusSchema = z.enum(STATUS_VALUES);
18
- // ──────────────────────────────────────────────────────────────────────────────
19
- // Primitive helpers
20
- // ──────────────────────────────────────────────────────────────────────────────
21
- function stripControlChars(input) {
22
- return input.replaceAll(CONTROL_CHARS_REGEX, '');
23
- }
24
- function cleanString(input) {
25
- return stripControlChars(input).trim();
26
- }
27
- function isPlainObject(input) {
28
- return typeof input === 'object' && input !== null && !Array.isArray(input);
29
- }
30
- function tryParseJson(raw) {
31
- try {
32
- return JSON.parse(raw);
33
- }
34
- catch {
35
- return raw;
36
- }
37
- }
38
- function parseHttpUrl(input) {
39
- if (typeof input !== 'string')
40
- return null;
41
- const cleaned = cleanString(input);
42
- if (cleaned.length === 0)
43
- return null;
44
- let parsed;
45
- try {
46
- parsed = new URL(cleaned);
47
- }
48
- catch {
49
- return null;
50
- }
51
- return parsed.protocol.startsWith('http') ? cleaned : null;
52
- }
53
- function isValidIsoDate(value) {
54
- if (!/^\d{4}-\d{2}-\d{2}/.test(value))
55
- return false;
56
- const ts = Date.parse(value);
57
- if (Number.isNaN(ts))
58
- return false;
59
- return new Date(ts).toISOString().startsWith(value.slice(0, 10));
60
- }
61
- // ──────────────────────────────────────────────────────────────────────────────
62
- // File branch helpers
63
- // ──────────────────────────────────────────────────────────────────────────────
64
- export function resolveFileOptions(branch) {
65
- const opts = branch.fileOptions;
66
- return {
67
- accept: opts?.accept ?? 'any',
68
- maxSize: opts?.maxSize ?? DEFAULT_FILE_MAX_SIZE,
69
- };
70
- }
71
- function isAssetListBranch(branch) {
72
- return branch.type === 'file' && (branch.multiple === true || branch.format === 'asset-list');
73
- }
74
- function extractUrlFromCandidate(candidate) {
75
- const direct = parseHttpUrl(candidate);
76
- if (direct)
77
- return direct;
78
- if (isPlainObject(candidate)) {
79
- return parseHttpUrl(candidate.url);
80
- }
81
- return null;
82
- }
83
- function collectAssetListUrls(raw) {
84
- const source = typeof raw === 'string' ? tryParseJson(raw) : raw;
85
- const items = Array.isArray(source) ? source : [source];
86
- const seen = new Set();
87
- const out = [];
88
- for (const item of items) {
89
- if (item === null || item === undefined)
90
- continue;
91
- const url = extractUrlFromCandidate(item);
92
- if (!url)
93
- return null;
94
- if (!seen.has(url)) {
95
- seen.add(url);
96
- out.push(url);
97
- }
98
- }
99
- return out;
100
- }
101
- function sanitizeRichtextString(raw) {
102
- const noControls = stripControlChars(raw);
103
- const dangerous = DANGEROUS_TAG_REGEX.test(noControls) || DANGEROUS_ATTR_REGEX.test(noControls);
104
- const noTags = noControls.replaceAll(DANGEROUS_TAG_STRIP_REGEX, '');
105
- const noHandlers = noTags.replaceAll(DANGEROUS_HANDLER_STRIP_REGEX, '');
106
- const finalValue = noHandlers.trim();
107
- return { value: finalValue, dangerous, valid: true, size: finalValue.length };
108
- }
109
- function walkRichtextNode(node, state) {
110
- if (typeof node === 'string') {
111
- const cleaned = stripControlChars(node);
112
- if (DANGEROUS_TAG_REGEX.test(cleaned) || DANGEROUS_ATTR_REGEX.test(cleaned)) {
113
- state.dangerous = true;
114
- }
115
- return cleaned;
116
- }
117
- if (Array.isArray(node)) {
118
- return node.map((child) => walkRichtextNode(child, state));
119
- }
120
- if (!isPlainObject(node))
121
- return node;
122
- const result = {};
123
- for (const [key, entry] of Object.entries(node)) {
124
- const lower = key.toLowerCase();
125
- if (lower.startsWith('on')) {
126
- state.dangerous = true;
127
- }
128
- if (lower === 'type' && typeof entry === 'string') {
129
- if (FORBIDDEN_RICHTEXT_NODE_TYPES.has(entry.toLowerCase())) {
130
- state.dangerous = true;
131
- }
132
- }
133
- if (URL_LIKE_RICHTEXT_KEYS.has(lower) && typeof entry === 'string' && DANGEROUS_PROTOCOL_REGEX.test(entry)) {
134
- state.dangerous = true;
135
- }
136
- result[key] = walkRichtextNode(entry, state);
137
- }
138
- return result;
139
- }
140
- function sanitizeRichtextJson(raw) {
141
- const state = { dangerous: false };
142
- const cleaned = walkRichtextNode(raw, state);
143
- const asObject = isPlainObject(cleaned) ? cleaned : {};
144
- const valid = asObject.type === 'doc';
145
- const serialized = JSON.stringify(asObject);
146
- return { value: asObject, dangerous: state.dangerous, valid, size: serialized.length };
147
- }
148
- function sanitizeRichtext(raw) {
149
- const envelopeMode = isRichtextEnvelopeV1(raw);
150
- const payload = envelopeMode ? raw.doc : raw;
151
- if (typeof payload === 'string') {
152
- return sanitizeRichtextString(payload);
153
- }
154
- if (isPlainObject(payload)) {
155
- const jsonResult = sanitizeRichtextJson(payload);
156
- if (!jsonResult.valid) {
157
- return { value: raw, dangerous: jsonResult.dangerous, valid: false, size: jsonResult.size };
158
- }
159
- const finalValue = envelopeMode
160
- ? { schemaVersion: RICHTEXT_SCHEMA_VERSION, doc: jsonResult.value }
161
- : jsonResult.value;
162
- return {
163
- value: finalValue,
164
- dangerous: jsonResult.dangerous,
165
- valid: true,
166
- size: JSON.stringify(finalValue).length,
167
- };
168
- }
169
- return { value: raw, dangerous: false, valid: false, size: 0 };
170
- }
171
- function gatherRichtextText(node, sink) {
172
- if (Array.isArray(node)) {
173
- for (const child of node)
174
- gatherRichtextText(child, sink);
175
- return;
176
- }
177
- if (!isPlainObject(node))
178
- return;
179
- if (typeof node.text === 'string') {
180
- sink.push(cleanString(node.text));
181
- }
182
- if (isPlainObject(node.attrs) && typeof node.attrs.latex === 'string') {
183
- sink.push(cleanString(node.attrs.latex));
184
- }
185
- if (Array.isArray(node.content)) {
186
- for (const child of node.content)
187
- gatherRichtextText(child, sink);
188
- }
189
- }
190
- function isRichtextDocEmpty(value) {
191
- if (!isPlainObject(value))
192
- return false;
193
- if (isRichtextEnvelopeV1(value)) {
194
- return isRichtextDocEmpty(value.doc);
195
- }
196
- if (value.type !== 'doc')
197
- return false;
198
- const sink = [];
199
- gatherRichtextText(value, sink);
200
- return sink.join('').trim().length === 0;
201
- }
202
- // ──────────────────────────────────────────────────────────────────────────────
203
- // Schema builders per branch type
204
- // ──────────────────────────────────────────────────────────────────────────────
205
- function withNullable(schema, allowNull) {
206
- return allowNull ? z.union([schema, z.null()]) : schema;
207
- }
208
- function withEmptyPreprocessing(schema, allowNull) {
209
- const fallback = allowNull ? null : undefined;
210
- return z.preprocess((val) => (val === '' || val === null ? fallback : val), schema.optional());
211
- }
212
- function textSchema(options, allowNull) {
213
- const inner = z
214
- .string()
215
- .transform((value) => cleanString(value))
216
- .refine((value) => value.length <= options.maxTextLength, {
217
- message: `Expected string(max:${options.maxTextLength})`,
218
- });
219
- return withNullable(inner, allowNull);
220
- }
221
- function richtextSchema(options, allowNull) {
222
- const inner = z.any().transform((value, ctx) => {
223
- const sanitized = sanitizeRichtext(value);
224
- if (!sanitized.valid) {
225
- ctx.addIssue({
226
- code: 'custom',
227
- message: 'Expected richtext-json|string',
228
- params: { expected: 'richtext-json|string' },
229
- });
230
- }
231
- if (sanitized.size > options.maxTextLength) {
232
- ctx.addIssue({
233
- code: 'custom',
234
- message: `Expected richtext(max:${options.maxTextLength})`,
235
- params: { expected: `richtext(max:${options.maxTextLength})` },
236
- });
237
- }
238
- if (sanitized.dangerous) {
239
- ctx.addIssue({
240
- code: 'custom',
241
- message: 'Dangerous richtext content',
242
- params: { dangerous: true },
243
- });
244
- }
245
- return sanitized.value;
246
- });
247
- return withNullable(inner, allowNull);
248
- }
249
- function numberSchema(branch, allowNull) {
250
- const opts = branch.numberOptions;
251
- const base = z.number().superRefine((val, ctx) => {
252
- if (!Number.isFinite(val)) {
253
- ctx.addIssue({ code: 'custom', message: 'Expected finite number' });
254
- return;
255
- }
256
- if (opts?.min !== undefined && val < opts.min) {
257
- ctx.addIssue({ code: 'custom', message: `Expected number(min:${opts.min})` });
258
- return;
259
- }
260
- if (opts?.max !== undefined && val > opts.max) {
261
- ctx.addIssue({ code: 'custom', message: `Expected number(max:${opts.max})` });
262
- return;
263
- }
264
- if (opts?.step !== undefined) {
265
- const step = opts.step;
266
- const origin = opts.min ?? 0;
267
- const valDecimals = (val.toString().split('.')[1] ?? '').length;
268
- const stepDecimals = (step.toString().split('.')[1] ?? '').length;
269
- const scale = 10 ** Math.max(valDecimals, stepDecimals);
270
- const offset = Math.round((val - origin) * scale);
271
- const stepScaled = Math.round(step * scale);
272
- if (stepScaled !== 0 && offset % stepScaled !== 0) {
273
- ctx.addIssue({ code: 'custom', message: `Expected number(step:${step})` });
274
- }
275
- }
276
- });
277
- return withNullable(withEmptyPreprocessing(base, allowNull), allowNull);
278
- }
279
- function booleanSchema(allowNull) {
280
- return withNullable(withEmptyPreprocessing(z.boolean(), allowNull), allowNull);
281
- }
282
- function dateSchema(allowNull) {
283
- const base = z
284
- .string()
285
- .transform((value) => cleanString(value))
286
- .refine((value) => isValidIsoDate(value), { message: 'Expected date(ISO)' });
287
- return withNullable(withEmptyPreprocessing(base, allowNull), allowNull);
288
- }
289
- function jsonOrTagsSchema(allowNull) {
290
- const base = z.union([z.record(z.string(), z.unknown()), z.array(z.unknown())]);
291
- return withNullable(withEmptyPreprocessing(base, allowNull), allowNull);
292
- }
293
- function relationSchema(branch, options) {
294
- const gen = options.idGenerator;
295
- if (!gen) {
296
- throw new Error('IIdGenerator must be provided in ValidateSeedPayloadOptions when validating seeds with relation branches. ' +
297
- 'Pass `idGenerator` (e.g. SystemIdGenerator) in the options object.');
298
- }
299
- const idSchema = z.string().refine((value) => gen.isValid(value), { message: 'Invalid relation id format' });
300
- // Many-to-many: expect string[] with no duplicates
301
- if (branch.multiple === true) {
302
- const arraySchema = z.array(idSchema).refine((arr) => new Set(arr).size === arr.length, { message: 'Duplicate ids in multi-relation array' });
303
- return options.allowNull ? z.union([arraySchema, z.null()]) : arraySchema;
304
- }
305
- return withNullable(idSchema, options.allowNull);
306
- }
307
- function fileSchema(branch, allowNull) {
308
- const { accept } = resolveFileOptions(branch);
309
- const verifyExtension = (url, ctx) => {
310
- if (accept === 'any')
311
- return true;
312
- const ext = extensionFromUrl(url);
313
- if (isExtensionAccepted(ext, accept))
314
- return true;
315
- ctx.addIssue({ code: 'custom', message: `Expected file(accept:${accept})` });
316
- return false;
317
- };
318
- if (isAssetListBranch(branch)) {
319
- const inner = z
320
- .any()
321
- .transform((raw, ctx) => {
322
- const urls = collectAssetListUrls(raw);
323
- if (urls === null) {
324
- ctx.addIssue({ code: 'custom', message: 'Expected url-string[]' });
325
- return z.NEVER;
326
- }
327
- for (const url of urls) {
328
- if (!verifyExtension(url, ctx))
329
- return z.NEVER;
330
- }
331
- return urls;
332
- })
333
- .pipe(z.array(z.string().url()));
334
- return withNullable(withEmptyPreprocessing(inner, allowNull), allowNull);
335
- }
336
- const inner = z
337
- .any()
338
- .transform((raw, ctx) => {
339
- const url = parseHttpUrl(raw);
340
- if (!url) {
341
- ctx.addIssue({ code: 'custom', message: 'Expected url-string' });
342
- return z.NEVER;
343
- }
344
- if (!verifyExtension(url, ctx))
345
- return z.NEVER;
346
- return url;
347
- })
348
- .pipe(z.string().url());
349
- return withNullable(withEmptyPreprocessing(inner, allowNull), allowNull);
350
- }
351
- // Sub-branches of a `repeater` are restricted to leaf/scalar types — no nested
352
- // `repeater`, `relation`, or `file` (v1 restriction, see types.ts Branch.fields).
353
- const REPEATER_DISALLOWED_SUBTYPES = new Set(['repeater', 'relation', 'file']);
354
- function repeaterSchema(branch, options) {
355
- const requiredFlag = options.operation === 'create' ? 'requiredOnCreate' : 'requiredOnUpdate';
356
- const subBranches = (branch.fields ?? []).filter((sub) => !REPEATER_DISALLOWED_SUBTYPES.has(sub.type));
357
- const shape = {};
358
- for (const sub of subBranches) {
359
- const subSchema = schemaForBranch(sub, options);
360
- shape[sub.alias] = sub[requiredFlag] ? subSchema : subSchema.optional();
361
- }
362
- // z.object() strips unknown keys by default — old item shapes from a renamed/
363
- // removed sub-field are dropped rather than rejected (sprint 10 §5.1).
364
- const itemSchema = z.object(shape);
365
- let arraySchema = z.array(itemSchema);
366
- if (Number.isInteger(branch.minItems) && branch.minItems >= 0) {
367
- arraySchema = arraySchema.min(branch.minItems, {
368
- message: `Expected array(min:${branch.minItems})`,
369
- });
370
- }
371
- if (Number.isInteger(branch.maxItems) && branch.maxItems >= 0) {
372
- arraySchema = arraySchema.max(branch.maxItems, {
373
- message: `Expected array(max:${branch.maxItems})`,
374
- });
375
- }
376
- return withNullable(withEmptyPreprocessing(arraySchema, options.allowNull), options.allowNull);
377
- }
378
- const BRANCH_SCHEMA_BUILDERS = {
379
- text: (_branch, options) => textSchema(options, options.allowNull),
380
- richtext: (_branch, options) => richtextSchema(options, options.allowNull),
381
- number: (branch, options) => numberSchema(branch, options.allowNull),
382
- boolean: (_branch, options) => booleanSchema(options.allowNull),
383
- date: (_branch, options) => dateSchema(options.allowNull),
384
- json: (_branch, options) => jsonOrTagsSchema(options.allowNull),
385
- tags: (_branch, options) => jsonOrTagsSchema(options.allowNull),
386
- file: (branch, options) => fileSchema(branch, options.allowNull),
387
- relation: (branch, options) => relationSchema(branch, options),
388
- repeater: (branch, options) => repeaterSchema(branch, options),
389
- };
390
- function schemaForBranch(branch, options) {
391
- const builder = BRANCH_SCHEMA_BUILDERS[branch.type];
392
- if (!builder) {
393
- throw new Error(`Unhandled branch type: ${branch.type}`);
394
- }
395
- return builder(branch, options);
396
- }
397
- // ──────────────────────────────────────────────────────────────────────────────
398
- // Seed schema compilation with cache
399
- // ──────────────────────────────────────────────────────────────────────────────
400
- const seedSchemaCache = new Map();
401
- function buildSeedFingerprint(seed) {
402
- const parts = seed.branches.map((branch) => ({
403
- a: branch.alias,
404
- t: branch.type,
405
- f: branch.format ?? null,
406
- m: branch.multiple === true,
407
- rc: branch.requiredOnCreate === true,
408
- ru: branch.requiredOnUpdate === true,
409
- n: branch.numberOptions ?? null,
410
- fi: branch.fileOptions ?? null,
411
- mi: branch.minItems ?? null,
412
- ma: branch.maxItems ?? null,
413
- sub: branch.fields?.map((sub) => ({
414
- a: sub.alias,
415
- t: sub.type,
416
- rc: sub.requiredOnCreate === true,
417
- ru: sub.requiredOnUpdate === true,
418
- })) ?? null,
419
- }));
420
- return JSON.stringify({ s: seed.slug, b: parts });
421
- }
422
- function buildCacheKey(seed, options) {
423
- return [
424
- buildSeedFingerprint(seed),
425
- options.operation,
426
- options.allowNull ? '1' : '0',
427
- String(options.maxTextLength),
428
- ].join('|');
429
- }
430
- function compileSeedSchema(seed, options) {
431
- // Seeds with relation branches cannot be safely cached because the schema
432
- // captures the idGenerator by closure, and different generators (e.g.
433
- // SystemIdGenerator vs SequentialIdGenerator) have different isValid() semantics.
434
- const hasRelation = seed.branches.some((b) => b.type === 'relation');
435
- if (!hasRelation) {
436
- const key = buildCacheKey(seed, options);
437
- const cached = seedSchemaCache.get(key);
438
- if (cached)
439
- return cached;
440
- }
441
- const requiredFlag = options.operation === 'create' ? 'requiredOnCreate' : 'requiredOnUpdate';
442
- const shape = {};
443
- for (const branch of seed.branches) {
444
- const branchSchema = schemaForBranch(branch, options);
445
- shape[branch.alias] = branch[requiredFlag] ? branchSchema : branchSchema.optional();
446
- }
447
- const compiled = z.object(shape).strict();
448
- if (!hasRelation) {
449
- const key = buildCacheKey(seed, options);
450
- seedSchemaCache.set(key, compiled);
451
- }
452
- return compiled;
453
- }
454
- // ──────────────────────────────────────────────────────────────────────────────
455
- // Required-field enforcement
456
- // ──────────────────────────────────────────────────────────────────────────────
457
- function isEffectivelyEmpty(value) {
458
- if (value === null || value === undefined)
459
- return true;
460
- if (typeof value === 'string')
461
- return cleanString(value).length === 0;
462
- if (Array.isArray(value))
463
- return value.length === 0;
464
- if (isPlainObject(value)) {
465
- if (isRichtextEnvelopeV1(value))
466
- return isRichtextDocEmpty(value.doc);
467
- if (isRichtextDocEmpty(value))
468
- return true;
469
- return Object.keys(value).length === 0;
470
- }
471
- return false;
472
- }
473
- function detectMissingRequired(seed, rawPayload, filtered, parsedData, parseSucceeded, options) {
474
- const missing = [];
475
- const details = [];
476
- const op = options.operation;
477
- for (const branch of seed.branches) {
478
- const isRequired = op === 'create' ? branch.requiredOnCreate : branch.requiredOnUpdate;
479
- if (!isRequired)
480
- continue;
481
- if (!Object.hasOwn(rawPayload, branch.alias)) {
482
- missing.push(branch.alias);
483
- details.push({
484
- field: branch.alias,
485
- expected: 'required-field',
486
- received: 'missing',
487
- message: `Field '${branch.alias}' is required for ${op}`,
488
- });
489
- continue;
490
- }
491
- const candidate = parseSucceeded ? parsedData[branch.alias] : filtered[branch.alias];
492
- if (isEffectivelyEmpty(candidate)) {
493
- missing.push(branch.alias);
494
- details.push({
495
- field: branch.alias,
496
- expected: 'required-field',
497
- received: 'empty',
498
- message: `Field '${branch.alias}' cannot be empty for ${op}`,
499
- });
500
- }
501
- }
502
- return { missing, details };
503
- }
504
- // ──────────────────────────────────────────────────────────────────────────────
505
- // Issue mapping
506
- // ──────────────────────────────────────────────────────────────────────────────
507
- function describeReceivedType(value) {
508
- if (value === null)
509
- return 'null';
510
- if (Array.isArray(value))
511
- return 'array';
512
- return typeof value;
513
- }
514
- function expectedFromMessage(message) {
515
- if (typeof message !== 'string')
516
- return 'valid-field-value';
517
- return message.startsWith('Expected ') ? message.slice('Expected '.length) : 'valid-field-value';
518
- }
519
- function splitUnknownAliases(seed, payload) {
520
- const knownAliases = new Set(seed.branches.map((branch) => branch.alias));
521
- const filtered = {};
522
- const unknown = [];
523
- const details = [];
524
- for (const alias of Object.keys(payload)) {
525
- if (knownAliases.has(alias)) {
526
- filtered[alias] = payload[alias];
527
- continue;
528
- }
529
- unknown.push(alias);
530
- details.push({
531
- field: alias,
532
- expected: 'known-seed-alias',
533
- received: 'unknown-alias',
534
- message: `Field '${alias}' is not defined in seed '${seed.slug}'`,
535
- });
536
- }
537
- return { filtered, unknown, details };
538
- }
539
- function flattenZodIssues(issues, parentPath = []) {
540
- const result = [];
541
- for (const issue of issues) {
542
- const issuePath = issue.path;
543
- const currentPath = [...parentPath, ...issuePath];
544
- if (issue.code === 'invalid_union' && 'errors' in issue) {
545
- const unionErrors = issue.errors;
546
- for (const subIssues of unionErrors) {
547
- result.push(...flattenZodIssues(subIssues, currentPath));
548
- }
549
- }
550
- else {
551
- result.push({
552
- ...issue,
553
- path: currentPath,
554
- });
555
- }
556
- }
557
- return result;
558
- }
559
- function processZodIssues(seed, issues, filtered, options) {
560
- const details = [];
561
- const unknown = [];
562
- const dangerous = [];
563
- const flatIssues = flattenZodIssues(issues);
564
- for (const issue of flatIssues) {
565
- if (issue.code === 'unrecognized_keys') {
566
- for (const alias of issue.keys) {
567
- unknown.push(alias);
568
- details.push({
569
- field: alias,
570
- expected: 'known-seed-alias',
571
- received: 'unknown-alias',
572
- message: `Field '${alias}' is not defined in seed '${seed.slug}'`,
573
- });
574
- }
575
- continue;
576
- }
577
- if (issue.message === 'Required' && options.enforceRequiredFields) {
578
- continue;
579
- }
580
- const field = String(issue.path[0] ?? 'payload');
581
- const params = issue.params;
582
- if (params?.dangerous === true) {
583
- dangerous.push(field);
584
- }
585
- details.push({
586
- field,
587
- expected: expectedFromMessage(issue.message),
588
- received: describeReceivedType(filtered[field]),
589
- message: `Field '${field}' expects type '${expectedFromMessage(issue.message)}' but received '${describeReceivedType(filtered[field])}'`,
590
- });
591
- }
592
- return { details, unknown, dangerous };
593
- }
594
- // ──────────────────────────────────────────────────────────────────────────────
595
- // Public entry points
596
- // ──────────────────────────────────────────────────────────────────────────────
597
- /**
598
- * Schema-driven validation and sanitization of a seed payload.
599
- * - Drops unknown aliases (reports them as details).
600
- * - Runs branch-specific schemas to coerce / sanitize known values.
601
- * - Optionally enforces required-on-create / required-on-update branches.
602
- * - Never throws on malformed input — returns a structured result.
603
- */
604
- export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
605
- const resolved = {
606
- allowNull: options.allowNull ?? false,
607
- operation: options.operation ?? 'create',
608
- requireAtLeastOneValidField: options.requireAtLeastOneValidField ?? true,
609
- enforceRequiredFields: options.enforceRequiredFields ?? true,
610
- maxTextLength: options.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH,
611
- idGenerator: options.idGenerator,
612
- };
613
- const { filtered, unknown: preUnknown, details: preDetails } = splitUnknownAliases(seed, payload);
614
- const schema = compileSeedSchema(seed, resolved);
615
- const parsed = schema.safeParse(filtered);
616
- const accumulatedDetails = [...preDetails];
617
- const unknownAliases = new Set(preUnknown);
618
- const dangerousFields = new Set();
619
- let data = {};
620
- if (parsed.success) {
621
- data = { ...parsed.data };
622
- }
623
- else {
624
- const zodOutcome = processZodIssues(seed, parsed.error.issues, filtered, resolved);
625
- accumulatedDetails.push(...zodOutcome.details);
626
- for (const alias of zodOutcome.unknown)
627
- unknownAliases.add(alias);
628
- for (const field of zodOutcome.dangerous)
629
- dangerousFields.add(field);
630
- }
631
- let requiredFieldsMissing = [];
632
- if (resolved.enforceRequiredFields) {
633
- const requiredOutcome = detectMissingRequired(seed, payload, filtered, data, parsed.success, resolved);
634
- requiredFieldsMissing = requiredOutcome.missing;
635
- accumulatedDetails.push(...requiredOutcome.details);
636
- }
637
- if (resolved.requireAtLeastOneValidField && Object.keys(data).length === 0) {
638
- accumulatedDetails.push({
639
- field: 'data',
640
- expected: 'at-least-one-valid-field',
641
- received: 'empty',
642
- message: 'Payload does not contain any valid fields for this operation',
643
- });
644
- }
645
- return {
646
- data,
647
- details: accumulatedDetails,
648
- unknownAliases: [...unknownAliases],
649
- dangerousFields: [...dangerousFields],
650
- requiredFieldsMissing,
651
- hasAnyValidField: Object.keys(data).length > 0,
652
- };
653
- }
654
- /**
655
- * Type-guard for the content status enum.
656
- */
657
- export function isValidContentStatus(value) {
658
- return statusSchema.safeParse(value).success;
659
- }
@@ -1 +0,0 @@
1
- {"version":3,"file":"webhook-validation.d.ts","sourceRoot":"","sources":["../src/webhook-validation.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,kBAAkB,QAC0H,CAAA;AAEzJ,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEvD"}
File without changes
File without changes
File without changes