@beechcms/core 0.4.0-preview.1

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 (44) hide show
  1. package/dist/define-seed.d.ts +3 -0
  2. package/dist/define-seed.d.ts.map +1 -0
  3. package/dist/define-seed.js +3 -0
  4. package/dist/engine.d.ts +68 -0
  5. package/dist/engine.d.ts.map +1 -0
  6. package/dist/engine.js +401 -0
  7. package/dist/index.d.ts +19 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +18 -0
  10. package/dist/policies.d.ts +10 -0
  11. package/dist/policies.d.ts.map +1 -0
  12. package/dist/policies.js +28 -0
  13. package/dist/richtext-render.d.ts +11 -0
  14. package/dist/richtext-render.d.ts.map +1 -0
  15. package/dist/richtext-render.js +85 -0
  16. package/dist/richtext.d.ts +11 -0
  17. package/dist/richtext.d.ts.map +1 -0
  18. package/dist/richtext.js +11 -0
  19. package/dist/seeds.d.ts +36 -0
  20. package/dist/seeds.d.ts.map +1 -0
  21. package/dist/seeds.js +179 -0
  22. package/dist/slug-utils.d.ts +18 -0
  23. package/dist/slug-utils.d.ts.map +1 -0
  24. package/dist/slug-utils.js +29 -0
  25. package/dist/types.d.ts +110 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/dist/types.js +1 -0
  28. package/dist/validation.d.ts +44 -0
  29. package/dist/validation.d.ts.map +1 -0
  30. package/dist/validation.js +571 -0
  31. package/package.json +35 -0
  32. package/src/define-seed.ts +5 -0
  33. package/src/engine.ts +465 -0
  34. package/src/index.ts +19 -0
  35. package/src/policies.test.ts +127 -0
  36. package/src/policies.ts +32 -0
  37. package/src/richtext-render.ts +87 -0
  38. package/src/richtext.ts +16 -0
  39. package/src/seeds.ts +194 -0
  40. package/src/slug-utils.ts +33 -0
  41. package/src/types.ts +121 -0
  42. package/src/validation.ts +667 -0
  43. package/tsconfig.json +14 -0
  44. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,571 @@
1
+ import { z } from 'zod';
2
+ import { RICHTEXT_SCHEMA_VERSION, isRichtextEnvelopeV1 } from './richtext.js';
3
+ const DEFAULT_MAX_TEXT_LENGTH = 50000;
4
+ const CONTROL_CHARS_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
5
+ const DANGEROUS_TAG_REGEX = /<(script|iframe|object|embed)\b/i;
6
+ const DANGEROUS_ATTR_REGEX = /\son[a-z]+\s*=/i;
7
+ const DANGEROUS_PROTOCOL_REGEX = /^\s*javascript:/i;
8
+ const DANGEROUS_RICHTEXT_NODE_TYPES = new Set(['script', 'iframe', 'object', 'embed']);
9
+ const LINK_LIKE_RICHTEXT_ATTRS = new Set(['href', 'src']);
10
+ const statusSchema = z.enum(['draft', 'review', 'published']);
11
+ const finiteNumberSchema = z.number().refine(Number.isFinite, 'Expected finite number');
12
+ const stringSchema = z.string();
13
+ const booleanSchema = z.boolean();
14
+ const jsonObjectOrArraySchema = z.union([z.record(z.string(), z.unknown()), z.array(z.unknown())]);
15
+ function isAssetListBranch(branch) {
16
+ return branch.type === 'file' && (branch.multiple === true || branch.format === 'asset-list');
17
+ }
18
+ function normalizeHttpUrl(value) {
19
+ if (!stringSchema.safeParse(value).success) {
20
+ return null;
21
+ }
22
+ const cleaned = sanitizePlainString(value);
23
+ if (!cleaned) {
24
+ return null;
25
+ }
26
+ try {
27
+ const parsed = new URL(cleaned);
28
+ if (!parsed.protocol.startsWith('http')) {
29
+ return null;
30
+ }
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ return cleaned;
36
+ }
37
+ function parseJsonString(value) {
38
+ try {
39
+ return JSON.parse(value);
40
+ }
41
+ catch {
42
+ return value;
43
+ }
44
+ }
45
+ function isPlainObject(value) {
46
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
47
+ }
48
+ function normalizeAssetListValue(rawValue) {
49
+ const input = typeof rawValue === 'string' ? parseJsonString(rawValue) : rawValue;
50
+ const values = Array.isArray(input) ? input : [input];
51
+ const normalized = [];
52
+ for (const item of values) {
53
+ if (item == null)
54
+ continue;
55
+ const directUrl = normalizeHttpUrl(item);
56
+ if (directUrl) {
57
+ normalized.push(directUrl);
58
+ continue;
59
+ }
60
+ if (typeof item === 'object' && !Array.isArray(item)) {
61
+ const nestedUrl = normalizeHttpUrl(item.url);
62
+ if (nestedUrl) {
63
+ normalized.push(nestedUrl);
64
+ continue;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ return [...new Set(normalized)];
70
+ }
71
+ function sanitizePlainString(value) {
72
+ return value.replaceAll(CONTROL_CHARS_REGEX, '').trim();
73
+ }
74
+ function collectRichtextVisibleText(value, chunks) {
75
+ if (Array.isArray(value)) {
76
+ for (const item of value)
77
+ collectRichtextVisibleText(item, chunks);
78
+ return;
79
+ }
80
+ if (!isPlainObject(value))
81
+ return;
82
+ if (typeof value.text === 'string') {
83
+ chunks.push(sanitizePlainString(value.text));
84
+ }
85
+ const attrs = typeof value.attrs === 'object' && value.attrs !== null ? value.attrs : null;
86
+ if (attrs && typeof attrs.latex === 'string') {
87
+ chunks.push(sanitizePlainString(attrs.latex));
88
+ }
89
+ if (Array.isArray(value.content)) {
90
+ for (const nested of value.content)
91
+ collectRichtextVisibleText(nested, chunks);
92
+ }
93
+ }
94
+ function isRichtextDocEmpty(value) {
95
+ if (!isPlainObject(value))
96
+ return false;
97
+ if (isRichtextEnvelopeV1(value)) {
98
+ return isRichtextDocEmpty(value.doc);
99
+ }
100
+ if (value.type !== 'doc')
101
+ return false;
102
+ const chunks = [];
103
+ collectRichtextVisibleText(value, chunks);
104
+ return chunks.join('').trim().length === 0;
105
+ }
106
+ function isMissingRequiredValue(value) {
107
+ if (value == null)
108
+ return true;
109
+ if (typeof value === 'string')
110
+ return sanitizePlainString(value).length === 0;
111
+ if (Array.isArray(value))
112
+ return value.length === 0;
113
+ if (isPlainObject(value)) {
114
+ if (isRichtextEnvelopeV1(value)) {
115
+ return isRichtextDocEmpty(value.doc);
116
+ }
117
+ if (isRichtextDocEmpty(value))
118
+ return true;
119
+ return Object.keys(value).length === 0;
120
+ }
121
+ return false;
122
+ }
123
+ function sanitizeRichtextString(value) {
124
+ const noControl = value.replaceAll(CONTROL_CHARS_REGEX, '');
125
+ const dangerous = DANGEROUS_TAG_REGEX.test(noControl) || DANGEROUS_ATTR_REGEX.test(noControl);
126
+ // Best-effort sanitization for Sprint 02; complete policy tracked in TODOs below.
127
+ const strippedTags = noControl.replaceAll(/<\/?(script|iframe|object|embed)[^>]*>/gi, '');
128
+ const strippedHandlers = strippedTags.replaceAll(/\son[a-z]+\s*=\s*(['"]).*?\1/gi, '');
129
+ const nextValue = strippedHandlers.trim();
130
+ return { value: nextValue, dangerous, size: nextValue.length };
131
+ }
132
+ function sanitizeRichtextJsonNode(value, state) {
133
+ if (typeof value === 'string') {
134
+ const cleaned = value.replaceAll(CONTROL_CHARS_REGEX, '');
135
+ if (DANGEROUS_TAG_REGEX.test(cleaned) || DANGEROUS_ATTR_REGEX.test(cleaned)) {
136
+ state.dangerous = true;
137
+ }
138
+ return cleaned;
139
+ }
140
+ if (Array.isArray(value)) {
141
+ return value.map((item) => sanitizeRichtextJsonNode(item, state));
142
+ }
143
+ if (!isPlainObject(value)) {
144
+ return value;
145
+ }
146
+ const next = {};
147
+ for (const [key, rawEntry] of Object.entries(value)) {
148
+ const loweredKey = key.toLowerCase();
149
+ if (loweredKey.startsWith('on')) {
150
+ state.dangerous = true;
151
+ }
152
+ if (loweredKey === 'type' &&
153
+ typeof rawEntry === 'string' &&
154
+ DANGEROUS_RICHTEXT_NODE_TYPES.has(rawEntry.toLowerCase())) {
155
+ state.dangerous = true;
156
+ }
157
+ if (LINK_LIKE_RICHTEXT_ATTRS.has(loweredKey) &&
158
+ typeof rawEntry === 'string' &&
159
+ DANGEROUS_PROTOCOL_REGEX.test(rawEntry)) {
160
+ state.dangerous = true;
161
+ }
162
+ next[key] = sanitizeRichtextJsonNode(rawEntry, state);
163
+ }
164
+ return next;
165
+ }
166
+ function sanitizeRichtextJson(value) {
167
+ const state = { dangerous: false };
168
+ const sanitized = sanitizeRichtextJsonNode(value, state);
169
+ const asObject = isPlainObject(sanitized) ? sanitized : {};
170
+ const valid = asObject.type === 'doc';
171
+ const serialized = JSON.stringify(asObject);
172
+ return {
173
+ value: asObject,
174
+ dangerous: state.dangerous,
175
+ valid,
176
+ size: serialized.length,
177
+ };
178
+ }
179
+ function unwrapRichtextPayload(value) {
180
+ if (isRichtextEnvelopeV1(value)) {
181
+ return { inner: value.doc, wrapAsEnvelope: true };
182
+ }
183
+ return { inner: value, wrapAsEnvelope: false };
184
+ }
185
+ function sanitizeRichtext(value) {
186
+ const { inner, wrapAsEnvelope } = unwrapRichtextPayload(value);
187
+ if (typeof inner === 'string') {
188
+ const sanitized = sanitizeRichtextString(inner);
189
+ return {
190
+ value: sanitized.value,
191
+ dangerous: sanitized.dangerous,
192
+ valid: true,
193
+ size: sanitized.size,
194
+ };
195
+ }
196
+ if (isPlainObject(inner)) {
197
+ const jsonResult = sanitizeRichtextJson(inner);
198
+ if (!jsonResult.valid) {
199
+ return {
200
+ value,
201
+ dangerous: jsonResult.dangerous,
202
+ valid: false,
203
+ size: jsonResult.size,
204
+ };
205
+ }
206
+ const outValue = wrapAsEnvelope
207
+ ? { schemaVersion: RICHTEXT_SCHEMA_VERSION, doc: jsonResult.value }
208
+ : jsonResult.value;
209
+ return {
210
+ value: outValue,
211
+ dangerous: jsonResult.dangerous,
212
+ valid: true,
213
+ size: JSON.stringify(outValue).length,
214
+ };
215
+ }
216
+ return {
217
+ value,
218
+ dangerous: false,
219
+ valid: false,
220
+ size: 0,
221
+ };
222
+ }
223
+ function requiredAliases(seed, operation) {
224
+ return seed.branches
225
+ .filter((branch) => (operation === 'create' ? branch.requiredOnCreate : branch.requiredOnUpdate))
226
+ .map((branch) => branch.alias);
227
+ }
228
+ function buildBranchSchema(branch, options) {
229
+ const nullable = options.allowNull ? z.null() : null;
230
+ switch (branch.type) {
231
+ case 'text': {
232
+ const schema = stringSchema
233
+ .transform((value) => sanitizePlainString(value))
234
+ .refine((value) => value.length <= options.maxTextLength, {
235
+ message: `Expected string(max:${options.maxTextLength})`,
236
+ });
237
+ return nullable ? z.union([schema, nullable]) : schema;
238
+ }
239
+ case 'richtext': {
240
+ const schema = z.any().transform((value, ctx) => {
241
+ const sanitized = sanitizeRichtext(value);
242
+ if (!sanitized.valid) {
243
+ ctx.addIssue({
244
+ code: 'custom',
245
+ message: 'Expected richtext-json|string',
246
+ params: { expected: 'richtext-json|string' },
247
+ });
248
+ }
249
+ if (sanitized.size > options.maxTextLength) {
250
+ ctx.addIssue({
251
+ code: 'custom',
252
+ message: `Expected richtext(max:${options.maxTextLength})`,
253
+ params: { expected: `richtext(max:${options.maxTextLength})` },
254
+ });
255
+ }
256
+ if (sanitized.dangerous) {
257
+ ctx.addIssue({
258
+ code: 'custom',
259
+ message: 'Dangerous richtext content',
260
+ params: { dangerous: true },
261
+ });
262
+ }
263
+ return sanitized.value;
264
+ });
265
+ return nullable ? z.union([schema, nullable]) : schema;
266
+ }
267
+ case 'number': {
268
+ const schema = finiteNumberSchema;
269
+ return nullable ? z.union([schema, nullable]) : schema;
270
+ }
271
+ case 'boolean': {
272
+ const schema = booleanSchema;
273
+ return nullable ? z.union([schema, nullable]) : schema;
274
+ }
275
+ case 'date': {
276
+ const schema = stringSchema
277
+ .transform((value) => sanitizePlainString(value))
278
+ .refine((value) => isIsoDateString(value), { message: 'Expected date(ISO)' });
279
+ return nullable ? z.union([schema, nullable]) : schema;
280
+ }
281
+ case 'json': {
282
+ const schema = jsonObjectOrArraySchema;
283
+ return nullable ? z.union([schema, nullable]) : schema;
284
+ }
285
+ case 'file': {
286
+ if (isAssetListBranch(branch)) {
287
+ const schema = z
288
+ .any()
289
+ .transform((rawValue, ctx) => {
290
+ const normalized = normalizeAssetListValue(rawValue);
291
+ if (!normalized) {
292
+ ctx.addIssue({
293
+ code: 'custom',
294
+ message: 'Expected url-string[]',
295
+ });
296
+ return z.NEVER;
297
+ }
298
+ return normalized;
299
+ })
300
+ .pipe(z.array(z.url()));
301
+ return nullable ? z.union([schema, nullable]) : schema;
302
+ }
303
+ const schema = z
304
+ .any()
305
+ .transform((rawValue, ctx) => {
306
+ const normalized = normalizeHttpUrl(rawValue);
307
+ if (!normalized) {
308
+ ctx.addIssue({
309
+ code: 'custom',
310
+ message: 'Expected url-string',
311
+ });
312
+ return z.NEVER;
313
+ }
314
+ return normalized;
315
+ })
316
+ .pipe(z.url());
317
+ return nullable ? z.union([schema, nullable]) : schema;
318
+ }
319
+ }
320
+ }
321
+ const compiledSchemaCache = new Map();
322
+ function seedFingerprint(seed) {
323
+ return JSON.stringify({
324
+ slug: seed.slug,
325
+ branches: seed.branches.map((branch) => ({
326
+ alias: branch.alias,
327
+ type: branch.type,
328
+ format: branch.format ?? null,
329
+ multiple: branch.multiple ?? false,
330
+ requiredOnCreate: branch.requiredOnCreate ?? false,
331
+ requiredOnUpdate: branch.requiredOnUpdate ?? false,
332
+ })),
333
+ });
334
+ }
335
+ function compileSeedSchema(seed, options) {
336
+ const key = JSON.stringify({
337
+ fingerprint: seedFingerprint(seed),
338
+ operation: options.operation,
339
+ allowNull: options.allowNull,
340
+ maxTextLength: options.maxTextLength,
341
+ });
342
+ const cached = compiledSchemaCache.get(key);
343
+ if (cached)
344
+ return cached;
345
+ const required = new Set(requiredAliases(seed, options.operation));
346
+ const shape = {};
347
+ for (const branch of seed.branches) {
348
+ const baseSchema = buildBranchSchema(branch, options);
349
+ shape[branch.alias] = required.has(branch.alias) ? baseSchema : baseSchema.optional();
350
+ }
351
+ const compiled = z.object(shape).strict();
352
+ compiledSchemaCache.set(key, compiled);
353
+ return compiled;
354
+ }
355
+ function makeDetail(field, expected, received) {
356
+ let receivedType = typeof received;
357
+ if (received === null)
358
+ receivedType = 'null';
359
+ else if (Array.isArray(received))
360
+ receivedType = 'array';
361
+ return {
362
+ field,
363
+ expected,
364
+ received: receivedType,
365
+ message: `Field '${field}' expects type '${expected}' but received '${receivedType}'`,
366
+ };
367
+ }
368
+ function isIsoDateString(value) {
369
+ if (!/^\d{4}-\d{2}-\d{2}/.test(value))
370
+ return false;
371
+ return !Number.isNaN(Date.parse(value));
372
+ }
373
+ function validateBranchValue(branch, alias, rawValue, options) {
374
+ if (rawValue === null) {
375
+ return options.allowNull
376
+ ? { ok: true, value: null }
377
+ : { ok: false, detail: makeDetail(alias, branch.type, rawValue) };
378
+ }
379
+ switch (branch.type) {
380
+ case 'text': {
381
+ if (!stringSchema.safeParse(rawValue).success) {
382
+ return { ok: false, detail: makeDetail(alias, 'string', rawValue) };
383
+ }
384
+ const sanitized = sanitizePlainString(rawValue);
385
+ if (sanitized.length > options.maxTextLength) {
386
+ return { ok: false, detail: makeDetail(alias, `string(max:${options.maxTextLength})`, rawValue) };
387
+ }
388
+ return { ok: true, value: sanitized };
389
+ }
390
+ case 'richtext': {
391
+ const sanitized = sanitizeRichtext(rawValue);
392
+ if (!sanitized.valid) {
393
+ return { ok: false, detail: makeDetail(alias, 'richtext-json|string', rawValue) };
394
+ }
395
+ if (sanitized.size > options.maxTextLength) {
396
+ return {
397
+ ok: false,
398
+ detail: makeDetail(alias, `richtext(max:${options.maxTextLength})`, rawValue),
399
+ };
400
+ }
401
+ return { ok: true, value: sanitized.value, dangerous: sanitized.dangerous };
402
+ }
403
+ case 'number': {
404
+ if (!finiteNumberSchema.safeParse(rawValue).success) {
405
+ return { ok: false, detail: makeDetail(alias, 'number', rawValue) };
406
+ }
407
+ return { ok: true, value: rawValue };
408
+ }
409
+ case 'boolean': {
410
+ if (!booleanSchema.safeParse(rawValue).success) {
411
+ return { ok: false, detail: makeDetail(alias, 'boolean', rawValue) };
412
+ }
413
+ return { ok: true, value: rawValue };
414
+ }
415
+ case 'date': {
416
+ if (!stringSchema.safeParse(rawValue).success) {
417
+ return { ok: false, detail: makeDetail(alias, 'date(ISO)', rawValue) };
418
+ }
419
+ const value = sanitizePlainString(rawValue);
420
+ if (!isIsoDateString(value)) {
421
+ return { ok: false, detail: makeDetail(alias, 'date(ISO)', rawValue) };
422
+ }
423
+ return { ok: true, value };
424
+ }
425
+ case 'json': {
426
+ const isValidJsonLike = (typeof rawValue === 'object' && rawValue !== null) || Array.isArray(rawValue);
427
+ if (!isValidJsonLike || typeof rawValue === 'string') {
428
+ return { ok: false, detail: makeDetail(alias, 'object|array', rawValue) };
429
+ }
430
+ return { ok: true, value: rawValue };
431
+ }
432
+ case 'file': {
433
+ if (isAssetListBranch(branch)) {
434
+ const listValue = normalizeAssetListValue(rawValue);
435
+ if (!listValue) {
436
+ return { ok: false, detail: makeDetail(alias, 'url-string[]', rawValue) };
437
+ }
438
+ return { ok: true, value: listValue };
439
+ }
440
+ const singleUrl = normalizeHttpUrl(rawValue);
441
+ if (!singleUrl) {
442
+ return { ok: false, detail: makeDetail(alias, 'url-string', rawValue) };
443
+ }
444
+ return { ok: true, value: singleUrl };
445
+ }
446
+ }
447
+ }
448
+ /**
449
+ * Foundation comune per validazione e sanitizzazione payload schema-driven.
450
+ * Usata da Public API e riusabile nel Botanical Engine.
451
+ *
452
+ * Ordine di esecuzione obbligatorio al momento della scrittura:
453
+ * validate raw → hash (privacy policy) → store
454
+ * L'hashing avviene DOPO la validazione, non prima.
455
+ * Questo garantisce che il valore validato sia il valore raw originale,
456
+ * non il digest — evitando false failure su campi con formato (es. email).
457
+ */
458
+ export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
459
+ const normalizedOptions = {
460
+ allowNull: options.allowNull ?? false,
461
+ operation: options.operation ?? 'create',
462
+ requireAtLeastOneValidField: options.requireAtLeastOneValidField ?? true,
463
+ enforceRequiredFields: options.enforceRequiredFields ?? true,
464
+ maxTextLength: options.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH,
465
+ };
466
+ const details = [];
467
+ const data = {};
468
+ const unknownAliases = [];
469
+ const dangerousFields = [];
470
+ const requiredFieldsMissing = [];
471
+ const allowedAliases = new Set(seed.branches.map((branch) => branch.alias));
472
+ const filteredPayload = {};
473
+ for (const [alias, rawValue] of Object.entries(payload)) {
474
+ if (!allowedAliases.has(alias)) {
475
+ unknownAliases.push(alias);
476
+ details.push({
477
+ field: alias,
478
+ expected: 'known-seed-alias',
479
+ received: 'unknown-alias',
480
+ message: `Field '${alias}' is not defined in seed '${seed.slug}'`,
481
+ });
482
+ continue;
483
+ }
484
+ filteredPayload[alias] = rawValue;
485
+ }
486
+ const schema = compileSeedSchema(seed, normalizedOptions);
487
+ const parsed = schema.safeParse(filteredPayload);
488
+ if (parsed.success) {
489
+ Object.assign(data, parsed.data);
490
+ }
491
+ else {
492
+ for (const issue of parsed.error.issues) {
493
+ if (issue.code === 'unrecognized_keys') {
494
+ for (const alias of issue.keys) {
495
+ unknownAliases.push(alias);
496
+ details.push({
497
+ field: alias,
498
+ expected: 'known-seed-alias',
499
+ received: 'unknown-alias',
500
+ message: `Field '${alias}' is not defined in seed '${seed.slug}'`,
501
+ });
502
+ }
503
+ continue;
504
+ }
505
+ const field = String(issue.path[0] ?? 'payload');
506
+ const receivedValue = filteredPayload[field];
507
+ const expected = typeof issue.message === 'string' && issue.message.startsWith('Expected ')
508
+ ? issue.message.replace('Expected ', '')
509
+ : 'valid-field-value';
510
+ if (issue.params?.dangerous === true) {
511
+ dangerousFields.push(field);
512
+ }
513
+ details.push(makeDetail(field, expected, receivedValue));
514
+ }
515
+ }
516
+ if (normalizedOptions.enforceRequiredFields) {
517
+ for (const branch of seed.branches) {
518
+ const isRequired = normalizedOptions.operation === 'create' ? branch.requiredOnCreate : branch.requiredOnUpdate;
519
+ if (!isRequired)
520
+ continue;
521
+ const hasProvidedAlias = Object.hasOwn(payload, branch.alias);
522
+ if (!hasProvidedAlias) {
523
+ requiredFieldsMissing.push(branch.alias);
524
+ details.push({
525
+ field: branch.alias,
526
+ expected: 'required-field',
527
+ received: 'missing',
528
+ message: `Field '${branch.alias}' is required for ${normalizedOptions.operation}`,
529
+ });
530
+ continue;
531
+ }
532
+ if (isMissingRequiredValue(data[branch.alias])) {
533
+ requiredFieldsMissing.push(branch.alias);
534
+ details.push({
535
+ field: branch.alias,
536
+ expected: 'required-field',
537
+ received: 'empty',
538
+ message: `Field '${branch.alias}' cannot be empty for ${normalizedOptions.operation}`,
539
+ });
540
+ }
541
+ }
542
+ }
543
+ if (normalizedOptions.requireAtLeastOneValidField && Object.keys(data).length === 0) {
544
+ details.push({
545
+ field: 'data',
546
+ expected: 'at-least-one-valid-field',
547
+ received: 'empty',
548
+ message: 'Payload does not contain any valid fields for this operation',
549
+ });
550
+ }
551
+ return {
552
+ data,
553
+ details,
554
+ unknownAliases: [...new Set(unknownAliases)],
555
+ dangerousFields: [...new Set(dangerousFields)],
556
+ requiredFieldsMissing,
557
+ hasAnyValidField: Object.keys(data).length > 0,
558
+ };
559
+ }
560
+ /**
561
+ * Valida status content supportati dal CMS.
562
+ */
563
+ export function isValidContentStatus(value) {
564
+ return statusSchema.safeParse(value).success;
565
+ }
566
+ /**
567
+ * Full Zod runtime compiler:
568
+ * - compile per-seed/per-operation with cache
569
+ * - strict unknown aliases
570
+ * - required fields by operation
571
+ */
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@beechcms/core",
3
+ "version": "0.4.0-preview.1",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsc -w --preserveWatchOutput",
17
+ "lint": "eslint .",
18
+ "type-check": "tsc --noEmit"
19
+ },
20
+ "dependencies": {
21
+ "@tiptap/core": "^3.22.3",
22
+ "@tiptap/extension-highlight": "^3.22.3",
23
+ "@tiptap/extension-image": "^3.22.3",
24
+ "@tiptap/extension-link": "^3.22.3",
25
+ "@tiptap/extension-subscript": "^3.22.3",
26
+ "@tiptap/extension-superscript": "^3.22.3",
27
+ "@tiptap/extension-table": "^3.22.3",
28
+ "@tiptap/extension-text-align": "^3.22.3",
29
+ "@tiptap/extension-mathematics": "^3.22.3",
30
+ "@tiptap/html": "^3.22.3",
31
+ "@tiptap/starter-kit": "^3.22.3",
32
+ "katex": "^0.16.11",
33
+ "zod": "^4.3.6"
34
+ }
35
+ }
@@ -0,0 +1,5 @@
1
+ import type { Seed } from './types.js'
2
+
3
+ export function defineSeed(seed: Seed): Seed {
4
+ return seed
5
+ }