@beechcms/core 0.4.0-preview.4 → 0.4.0-preview.6

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