diff --git a/.changeset/restore-asset-requirements-zod.md b/.changeset/restore-asset-requirements-zod.md new file mode 100644 index 000000000..e4d3f7040 --- /dev/null +++ b/.changeset/restore-asset-requirements-zod.md @@ -0,0 +1,29 @@ +--- +'@adcp/sdk': patch +--- + +Fix(types): restore typed Zod for per-asset-type `*AssetRequirementsSchema` + +Regression in 6.19.0 (introduced by #1654): the 12 `*AssetRequirementsSchema` +exports (`ImageAssetRequirementsSchema`, `TextAssetRequirementsSchema`, …) and +the parent `AssetRequirementsSchema` union were emitted as `z.any()` stubs, and +the `requirements` field on every `Individual*AssetSchema` / `Group*AssetSchema` +slot collapsed to `z.optional(z.any())`. TypeScript types were unaffected. + +Root cause: #1654's codegen post-processor injects +`import type { ImageAssetRequirements, … } from './core.generated';` at the top +of `tools.generated.ts` so the file typechecks standalone. The Zod codegen step +concatenates `core.generated.ts` + `tools.generated.ts` and passes the combined +source to `ts-to-zod`, but `ts-to-zod` still parses the cross-file `import type` +block and treats those names as external — emitting `z.any()` stubs even though +the actual interfaces are present in the same source. + +Fix: strip cross-file `import type { … } from './core.generated';` declarations +from `tools.generated.ts` before merging into the combined source. The types +are already inlined from `core.generated.ts`, so the import is redundant. + +Restores field-level runtime validation on `Individual*AssetSchema.requirements` +and re-exports the typed per-asset-type requirements schemas that consumers +like agentic-api had imported in 6.18. Added a regression test in +`test/lib/zod-schemas.test.js` that asserts the schemas reject wrong-typed +fields (a `z.any()` regression would silently accept them). diff --git a/scripts/generate-zod-from-ts.ts b/scripts/generate-zod-from-ts.ts index 993b66882..669b347bc 100644 --- a/scripts/generate-zod-from-ts.ts +++ b/scripts/generate-zod-from-ts.ts @@ -504,8 +504,29 @@ async function generateZodSchemas() { const coreContent = readFileSync(CORE_SOURCE_FILE, 'utf8'); const toolsContent = readFileSync(TOOLS_SOURCE_FILE, 'utf8'); + // tools.generated.ts imports a handful of *AssetRequirements types from + // core.generated.ts (injected by scripts/generate-types.ts so the standalone + // file typechecks). Since we concatenate both sources for ts-to-zod, those + // imports are redundant — worse, ts-to-zod treats imported names as external + // and emits `z.any()` stubs even when the actual interfaces are present in + // the combined source. Strip cross-file imports before merging. + const toolsWithoutCrossImports = toolsContent.replace( + /^import type \{[^}]*\} from ['"]\.\/core\.generated['"];?\n+/gm, + '' + ); + // Defensive: if the injector in scripts/generate-types.ts ever changes shape + // (different specifier, single-line form, etc.), the strip would silently + // no-op and we'd regress back to z.any() stubs. Fail loudly instead. + if (toolsWithoutCrossImports.includes("from './core.generated'")) { + throw new Error( + "generate-zod-from-ts: cross-file `import type { ... } from './core.generated'` " + + 'survived the strip. Update the regex in this file or the injector in ' + + 'scripts/generate-types.ts — letting it through degrades the matching schemas to z.any().' + ); + } + // Merge both sources so cross-file type dependencies can be resolved - const combinedSource = `${coreContent}\n\n// ====== TOOL TYPES ======\n\n${toolsContent}`; + const combinedSource = `${coreContent}\n\n// ====== TOOL TYPES ======\n\n${toolsWithoutCrossImports}`; console.log('📦 Generating Zod schemas for all types...'); @@ -573,6 +594,23 @@ async function generateZodSchemas() { // Post-process: Add explicit z.ZodType annotations to schemas that trip TS7056. zodSchemas = postProcessTS7056Annotations(zodSchemas); + // Defensive: ts-to-zod emits `const FooSchema = z.any();` stubs when it + // can't resolve a referenced type — usually because a cross-file `import + // type` declaration leaks past the upstream strip (see #1659). A `z.any()` + // stub silently accepts any shape at runtime and erases the per-type Zod + // contract downstream consumers rely on. Fail the build so the regression + // surfaces here, not in a consumer's test suite. + const anyStubs = [...zodSchemas.matchAll(/^const (\w+Schema) = z\.any\(\);$/gm)].map(m => m[1]); + if (anyStubs.length > 0) { + throw new Error( + `generate-zod-from-ts: ${anyStubs.length} schema(s) degenerated to z.any() stubs:\n` + + anyStubs.map(n => ` - ${n}`).join('\n') + + '\nThis usually means a cross-file `import type` declaration leaked past the strip ' + + 'in this script. Check that the referenced TypeScript interfaces are inlined in ' + + 'the combined source, and update the strip regex if a new cross-file import was added.' + ); + } + // Create header with metadata const header = `// Generated Zod v4 schemas from TypeScript types // Generated at: ${new Date().toISOString()} diff --git a/src/lib/types/schemas.generated.ts b/src/lib/types/schemas.generated.ts index 8145df69a..96c3d7d60 100644 --- a/src/lib/types/schemas.generated.ts +++ b/src/lib/types/schemas.generated.ts @@ -1,5 +1,5 @@ // Generated Zod v4 schemas from TypeScript types -// Generated at: 2026-05-10T17:46:04.922Z +// Generated at: 2026-05-11T08:48:10.724Z // Sources: // - core.generated.ts (core types) // - tools.generated.ts (tool types) @@ -3260,6 +3260,123 @@ export const FormatIDParameterSchema = z.union([z.literal("dimensions"), z.liter export const DimensionUnitSchema = z.union([z.literal("px"), z.literal("dp"), z.literal("inches"), z.literal("cm"), z.literal("mm"), z.literal("pt")]); +export const ImageAssetRequirementsSchema = z.object({ + min_width: z.number().optional(), + max_width: z.number().optional(), + min_height: z.number().optional(), + max_height: z.number().optional(), + unit: DimensionUnitSchema.optional(), + aspect_ratio: z.string().optional(), + formats: z.array(z.union([z.literal("jpg"), z.literal("jpeg"), z.literal("png"), z.literal("gif"), z.literal("webp"), z.literal("svg"), z.literal("avif"), z.literal("tiff"), z.literal("pdf"), z.literal("eps")])).optional(), + min_dpi: z.number().optional(), + bleed: z.union([z.object({ + uniform: z.number() + }).passthrough(), z.object({ + top: z.number(), + right: z.number(), + bottom: z.number(), + left: z.number() + }).passthrough()]).optional(), + color_space: z.union([z.literal("rgb"), z.literal("cmyk"), z.literal("grayscale")]).optional(), + max_file_size_kb: z.number().optional(), + transparency_required: z.boolean().optional(), + animation_allowed: z.boolean().optional(), + max_animation_duration_ms: z.number().optional(), + max_weight_grams: z.number().optional() +}).passthrough(); + +export const VideoAssetRequirementsSchema = z.object({ + min_width: z.number().optional(), + max_width: z.number().optional(), + min_height: z.number().optional(), + max_height: z.number().optional(), + aspect_ratio: z.string().optional(), + min_duration_ms: z.number().optional(), + max_duration_ms: z.number().optional(), + containers: z.array(z.union([z.literal("mp4"), z.literal("webm"), z.literal("mov"), z.literal("avi"), z.literal("mkv")])).optional(), + codecs: z.array(z.union([z.literal("h264"), z.literal("h265"), z.literal("vp8"), z.literal("vp9"), z.literal("av1"), z.literal("prores")])).optional(), + max_file_size_kb: z.number().optional(), + min_bitrate_kbps: z.number().optional(), + max_bitrate_kbps: z.number().optional(), + frame_rates: z.array(z.number()).optional(), + audio_required: z.boolean().optional(), + frame_rate_type: FrameRateTypeSchema.optional(), + scan_type: ScanTypeSchema.optional(), + gop_type: GOPTypeSchema.optional(), + min_gop_interval_seconds: z.number().optional(), + max_gop_interval_seconds: z.number().optional(), + moov_atom_position: MoovAtomPositionSchema.optional(), + audio_codecs: z.array(z.union([z.literal("aac"), z.literal("pcm"), z.literal("ac3"), z.literal("eac3"), z.literal("mp3"), z.literal("opus"), z.literal("vorbis"), z.literal("flac")])).optional(), + audio_sample_rates: z.array(z.number()).optional(), + audio_channels: z.array(AudioChannelLayoutSchema).optional(), + loudness_lufs: z.number().optional(), + loudness_tolerance_db: z.number().optional(), + true_peak_dbfs: z.number().optional() +}).passthrough(); + +export const AudioAssetRequirementsSchema = z.object({ + min_duration_ms: z.number().optional(), + max_duration_ms: z.number().optional(), + formats: z.array(z.union([z.literal("mp3"), z.literal("aac"), z.literal("wav"), z.literal("ogg"), z.literal("flac")])).optional(), + max_file_size_kb: z.number().optional(), + sample_rates: z.array(z.number()).optional(), + channels: z.array(z.union([z.literal("mono"), z.literal("stereo")])).optional(), + min_bitrate_kbps: z.number().optional(), + max_bitrate_kbps: z.number().optional() +}).passthrough(); + +export const TextAssetRequirementsSchema = z.object({ + min_length: z.number().optional(), + max_length: z.number().optional(), + min_lines: z.number().optional(), + max_lines: z.number().optional(), + character_pattern: z.string().optional(), + prohibited_terms: z.array(z.string()).optional() +}).passthrough(); + +export const MarkdownAssetRequirementsSchema = z.object({ + max_length: z.number().optional() +}).passthrough(); + +export const HTMLAssetRequirementsSchema = z.object({ + max_file_size_kb: z.number().optional(), + sandbox: z.union([z.literal("none"), z.literal("iframe"), z.literal("safeframe"), z.literal("fencedframe")]).optional(), + external_resources_allowed: z.boolean().optional(), + allowed_external_domains: z.array(z.string()).optional() +}).passthrough(); + +export const CSSAssetRequirementsSchema = z.object({ + max_file_size_kb: z.number().optional() +}).passthrough(); + +export const JavaScriptAssetRequirementsSchema = z.object({ + max_file_size_kb: z.number().optional(), + module_type: z.union([z.literal("script"), z.literal("module"), z.literal("iife")]).optional(), + strict_mode_required: z.boolean().optional(), + external_resources_allowed: z.boolean().optional(), + allowed_external_domains: z.array(z.string()).optional() +}).passthrough(); + +export const VASTAssetRequirementsSchema = z.object({ + vast_version: z.union([z.literal("2.0"), z.literal("3.0"), z.literal("4.0"), z.literal("4.1"), z.literal("4.2")]).optional() +}).passthrough(); + +export const DAASTAssetRequirementsSchema = z.object({ + daast_version: z.literal("1.0").optional() +}).passthrough(); + +export const URLAssetRequirementsSchema = z.object({ + role: z.union([z.literal("clickthrough"), z.literal("landing_page"), z.literal("impression_tracker"), z.literal("click_tracker"), z.literal("viewability_tracker"), z.literal("third_party_tracker")]).optional(), + protocols: z.array(z.union([z.literal("https"), z.literal("http")])).optional(), + allowed_domains: z.array(z.string()).optional(), + max_length: z.number().optional(), + macro_support: z.boolean().optional() +}).passthrough(); + +export const WebhookAssetRequirementsSchema = z.object({ + methods: z.array(z.union([z.literal("GET"), z.literal("POST")])).optional() +}).passthrough(); + export const OverlaySchema = z.object({ id: z.string(), description: z.string().optional(), @@ -3439,6 +3556,8 @@ export const RealEstateItemSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); +export const AssetRequirementsSchema = z.union([ImageAssetRequirementsSchema, VideoAssetRequirementsSchema, AudioAssetRequirementsSchema, TextAssetRequirementsSchema, MarkdownAssetRequirementsSchema, HTMLAssetRequirementsSchema, CSSAssetRequirementsSchema, JavaScriptAssetRequirementsSchema, VASTAssetRequirementsSchema, DAASTAssetRequirementsSchema, URLAssetRequirementsSchema, WebhookAssetRequirementsSchema]); + export const ScalarBindingSchema = z.object({ kind: z.literal("scalar"), asset_id: z.string(), @@ -3453,6 +3572,16 @@ export const AssetPoolBindingSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); +export const OfferingAssetConstraintSchema = z.object({ + asset_group_id: z.string(), + asset_type: AssetContentTypeSchema, + required: z.boolean().optional(), + min_count: z.number().optional(), + max_count: z.number().optional(), + asset_requirements: AssetRequirementsSchema.optional(), + ext: ExtensionObjectSchema.optional() +}).passthrough(); + export const CatalogFieldBindingSchema = z.union([ScalarBindingSchema, AssetPoolBindingSchema, z.object({ kind: z.literal("catalog_group"), format_group_id: z.string(), @@ -3775,6 +3904,66 @@ export const PropertyListChangedWebhookSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); +export const GroupImageAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("image"), + requirements: ImageAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupVideoAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("video"), + requirements: VideoAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupAudioAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("audio"), + requirements: AudioAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupTextAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("text"), + requirements: TextAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupMarkdownAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("markdown"), + requirements: MarkdownAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupHtmlAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("html"), + requirements: HTMLAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupCssAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("css"), + requirements: CSSAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupJavaScriptAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("javascript"), + requirements: JavaScriptAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupVastAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("vast"), + requirements: VASTAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupDaastAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("daast"), + requirements: DAASTAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupUrlAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("url"), + requirements: URLAssetRequirementsSchema.optional() +}).passthrough()); + +export const GroupWebhookAssetSchema = BaseGroupAssetSchema.and(z.object({ + asset_type: z.literal("webhook"), + requirements: WebhookAssetRequirementsSchema.optional() +}).passthrough()); + export const ProductFiltersSchema = z.object({ delivery_type: DeliveryTypeSchema.optional(), exclusivity: ExclusivitySchema.optional(), @@ -3905,6 +4094,61 @@ export const BaseIndividualAssetSchema = z.object({ overlays: z.array(OverlaySchema).optional() }).passthrough(); +export const IndividualVideoAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("video"), + requirements: VideoAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualAudioAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("audio"), + requirements: AudioAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualTextAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("text"), + requirements: TextAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualMarkdownAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("markdown"), + requirements: MarkdownAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualHtmlAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("html"), + requirements: HTMLAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualCssAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("css"), + requirements: CSSAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualJavaScriptAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("javascript"), + requirements: JavaScriptAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualVastAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("vast"), + requirements: VASTAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualDaastAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("daast"), + requirements: DAASTAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualUrlAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("url"), + requirements: URLAssetRequirementsSchema.optional() +}).passthrough()); + +export const IndividualWebhookAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("webhook"), + requirements: WebhookAssetRequirementsSchema.optional() +}).passthrough()); + export const IndividualBriefAssetSchema = BaseIndividualAssetSchema.and(z.object({ asset_type: z.literal("brief") }).passthrough()); @@ -3917,6 +4161,8 @@ export const VendorPricingOptionSchema = z.object({ pricing_option_id: z.string() }).passthrough().and(VendorPricingSchema); +export const GroupAssetSlotSchema = z.union([GroupImageAssetSchema, GroupVideoAssetSchema, GroupAudioAssetSchema, GroupTextAssetSchema, GroupMarkdownAssetSchema, GroupHtmlAssetSchema, GroupCssAssetSchema, GroupJavaScriptAssetSchema, GroupVastAssetSchema, GroupDaastAssetSchema, GroupUrlAssetSchema, GroupWebhookAssetSchema]); + export const CreativeBriefSchema = z.object({ name: z.string(), objective: z.union([z.literal("awareness"), z.literal("consideration"), z.literal("conversion"), z.literal("retention"), z.literal("engagement")]).optional(), @@ -5340,29 +5586,22 @@ export const ControllerErrorSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); -const ImageAssetRequirementsSchema = z.any(); - -const VideoAssetRequirementsSchema = z.any(); - -const AudioAssetRequirementsSchema = z.any(); - -const TextAssetRequirementsSchema = z.any(); - -const MarkdownAssetRequirementsSchema = z.any(); - -const HTMLAssetRequirementsSchema = z.any(); - -const CSSAssetRequirementsSchema = z.any(); - -const JavaScriptAssetRequirementsSchema = z.any(); - -const VASTAssetRequirementsSchema = z.any(); - -const DAASTAssetRequirementsSchema = z.any(); +export const IndividualImageAssetSchema = BaseIndividualAssetSchema.and(z.object({ + asset_type: z.literal("image"), + requirements: ImageAssetRequirementsSchema.optional() +}).passthrough()); -const URLAssetRequirementsSchema = z.any(); +export const IndividualAssetSlotSchema = z.union([IndividualImageAssetSchema, IndividualVideoAssetSchema, IndividualAudioAssetSchema, IndividualTextAssetSchema, IndividualMarkdownAssetSchema, IndividualHtmlAssetSchema, IndividualCssAssetSchema, IndividualJavaScriptAssetSchema, IndividualVastAssetSchema, IndividualDaastAssetSchema, IndividualUrlAssetSchema, IndividualWebhookAssetSchema, IndividualBriefAssetSchema, IndividualCatalogAssetSchema]); -const WebhookAssetRequirementsSchema = z.any(); +export const RepeatableGroupAssetSchema = z.object({ + item_type: z.literal("repeatable_group"), + asset_group_id: z.string(), + required: z.boolean(), + min_count: z.number(), + max_count: z.number(), + selection_mode: z.union([z.literal("sequential"), z.literal("optimize")]).optional(), + assets: z.array(GroupAssetSlotSchema) +}).passthrough(); export const MediaBuySchema = z.object({ media_buy_id: z.string(), @@ -6064,138 +6303,17 @@ export const CollectionSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); -export const IndividualImageAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("image"), - requirements: ImageAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualVideoAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("video"), - requirements: VideoAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualAudioAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("audio"), - requirements: AudioAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualTextAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("text"), - requirements: TextAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualMarkdownAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("markdown"), - requirements: MarkdownAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualHtmlAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("html"), - requirements: HTMLAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualCssAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("css"), - requirements: CSSAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualJavaScriptAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("javascript"), - requirements: JavaScriptAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualVastAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("vast"), - requirements: VASTAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualDaastAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("daast"), - requirements: DAASTAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualUrlAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("url"), - requirements: URLAssetRequirementsSchema.optional() -}).passthrough()); - -export const IndividualWebhookAssetSchema = BaseIndividualAssetSchema.and(z.object({ - asset_type: z.literal("webhook"), - requirements: WebhookAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupImageAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("image"), - requirements: ImageAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupVideoAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("video"), - requirements: VideoAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupAudioAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("audio"), - requirements: AudioAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupTextAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("text"), - requirements: TextAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupMarkdownAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("markdown"), - requirements: MarkdownAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupHtmlAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("html"), - requirements: HTMLAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupCssAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("css"), - requirements: CSSAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupJavaScriptAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("javascript"), - requirements: JavaScriptAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupVastAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("vast"), - requirements: VASTAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupDaastAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("daast"), - requirements: DAASTAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupUrlAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("url"), - requirements: URLAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupWebhookAssetSchema = BaseGroupAssetSchema.and(z.object({ - asset_type: z.literal("webhook"), - requirements: WebhookAssetRequirementsSchema.optional() -}).passthrough()); - -export const GroupAssetSlotSchema = z.union([GroupImageAssetSchema, GroupVideoAssetSchema, GroupAudioAssetSchema, GroupTextAssetSchema, GroupMarkdownAssetSchema, GroupHtmlAssetSchema, GroupCssAssetSchema, GroupJavaScriptAssetSchema, GroupVastAssetSchema, GroupDaastAssetSchema, GroupUrlAssetSchema, GroupWebhookAssetSchema]); - -export const AssetRequirementsSchema = z.union([ImageAssetRequirementsSchema, VideoAssetRequirementsSchema, AudioAssetRequirementsSchema, TextAssetRequirementsSchema, MarkdownAssetRequirementsSchema, HTMLAssetRequirementsSchema, CSSAssetRequirementsSchema, JavaScriptAssetRequirementsSchema, VASTAssetRequirementsSchema, DAASTAssetRequirementsSchema, URLAssetRequirementsSchema, WebhookAssetRequirementsSchema]); +export const FormatAssetSlotSchema = z.union([IndividualAssetSlotSchema, RepeatableGroupAssetSchema]); -export const OfferingAssetConstraintSchema = z.object({ - asset_group_id: z.string(), - asset_type: AssetContentTypeSchema, +export const CatalogRequirementsSchema = z.object({ + catalog_type: CatalogTypeSchema, required: z.boolean().optional(), - min_count: z.number().optional(), - max_count: z.number().optional(), - asset_requirements: AssetRequirementsSchema.optional(), - ext: ExtensionObjectSchema.optional() + min_items: z.number().optional(), + max_items: z.number().optional(), + required_fields: z.array(z.string()).optional(), + feed_formats: z.array(FeedFormatSchema).optional(), + offering_asset_constraints: z.array(OfferingAssetConstraintSchema).optional(), + field_bindings: z.array(CatalogFieldBindingSchema).optional() }).passthrough(); export const SignalPricingOptionSchema = z.object({ @@ -6232,18 +6350,6 @@ export const PropertyFeatureResultSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); -export const IndividualAssetSlotSchema = z.union([IndividualImageAssetSchema, IndividualVideoAssetSchema, IndividualAudioAssetSchema, IndividualTextAssetSchema, IndividualMarkdownAssetSchema, IndividualHtmlAssetSchema, IndividualCssAssetSchema, IndividualJavaScriptAssetSchema, IndividualVastAssetSchema, IndividualDaastAssetSchema, IndividualUrlAssetSchema, IndividualWebhookAssetSchema, IndividualBriefAssetSchema, IndividualCatalogAssetSchema]); - -export const RepeatableGroupAssetSchema = z.object({ - item_type: z.literal("repeatable_group"), - asset_group_id: z.string(), - required: z.boolean(), - min_count: z.number(), - max_count: z.number(), - selection_mode: z.union([z.literal("sequential"), z.literal("optimize")]).optional(), - assets: z.array(GroupAssetSlotSchema) -}).passthrough(); - export const GetProductsResponseSchema = z.object({ products: z.array(ProductSchema), proposals: z.array(ProposalSchema).optional(), @@ -6276,7 +6382,57 @@ export const GetProductsResponseSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); -export const FormatAssetSlotSchema = z.union([IndividualAssetSlotSchema, RepeatableGroupAssetSchema]); +export const FormatSchema = z.object({ + format_id: FormatReferenceStructuredObjectSchema, + name: z.string(), + description: z.string().optional(), + example_url: z.string().optional(), + accepts_parameters: z.array(FormatIDParameterSchema).optional(), + renders: z.array(z.union([z.object({ + role: z.string(), + dimensions: z.object({ + width: z.number().optional(), + height: z.number().optional(), + min_width: z.number().optional(), + min_height: z.number().optional(), + max_width: z.number().optional(), + max_height: z.number().optional(), + unit: DimensionUnitSchema.optional(), + responsive: z.object({ + width: z.boolean(), + height: z.boolean() + }).passthrough().optional(), + aspect_ratio: z.string().optional() + }).passthrough() + }).passthrough(), z.object({ + role: z.string(), + parameters_from_format_id: z.literal(true) + }).passthrough()])).optional(), + assets: z.array(FormatAssetSlotSchema).optional(), + delivery: z.object({}).passthrough().optional(), + supported_macros: z.array(z.union([UniversalMacroSchema, z.string()])).optional(), + input_format_ids: z.array(FormatReferenceStructuredObjectSchema).optional(), + output_format_ids: z.array(FormatReferenceStructuredObjectSchema).optional(), + format_card: z.object({ + format_id: FormatReferenceStructuredObjectSchema, + manifest: z.object({}).passthrough() + }).passthrough().optional(), + accessibility: z.object({ + wcag_level: WCAGLevelSchema, + requires_accessible_assets: z.boolean().optional() + }).passthrough().optional(), + supported_disclosure_positions: z.array(DisclosurePositionSchema).optional(), + disclosure_capabilities: z.array(z.object({ + position: DisclosurePositionSchema, + persistence: z.array(DisclosurePersistenceSchema) + }).passthrough()).optional(), + format_card_detailed: z.object({ + format_id: FormatReferenceStructuredObjectSchema, + manifest: z.object({}).passthrough() + }).passthrough().optional(), + reported_metrics: z.array(AvailableMetricSchema).optional(), + pricing_options: z.array(VendorPricingOptionSchema).optional() +}).passthrough(); export const CreateMediaBuyRequestSchema = z.object({ adcp_major_version: z.number().optional(), @@ -6464,69 +6620,6 @@ export const ValidatePropertyDeliveryResponseSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); -export const FormatSchema = z.object({ - format_id: FormatReferenceStructuredObjectSchema, - name: z.string(), - description: z.string().optional(), - example_url: z.string().optional(), - accepts_parameters: z.array(FormatIDParameterSchema).optional(), - renders: z.array(z.union([z.object({ - role: z.string(), - dimensions: z.object({ - width: z.number().optional(), - height: z.number().optional(), - min_width: z.number().optional(), - min_height: z.number().optional(), - max_width: z.number().optional(), - max_height: z.number().optional(), - unit: DimensionUnitSchema.optional(), - responsive: z.object({ - width: z.boolean(), - height: z.boolean() - }).passthrough().optional(), - aspect_ratio: z.string().optional() - }).passthrough() - }).passthrough(), z.object({ - role: z.string(), - parameters_from_format_id: z.literal(true) - }).passthrough()])).optional(), - assets: z.array(FormatAssetSlotSchema).optional(), - delivery: z.object({}).passthrough().optional(), - supported_macros: z.array(z.union([UniversalMacroSchema, z.string()])).optional(), - input_format_ids: z.array(FormatReferenceStructuredObjectSchema).optional(), - output_format_ids: z.array(FormatReferenceStructuredObjectSchema).optional(), - format_card: z.object({ - format_id: FormatReferenceStructuredObjectSchema, - manifest: z.object({}).passthrough() - }).passthrough().optional(), - accessibility: z.object({ - wcag_level: WCAGLevelSchema, - requires_accessible_assets: z.boolean().optional() - }).passthrough().optional(), - supported_disclosure_positions: z.array(DisclosurePositionSchema).optional(), - disclosure_capabilities: z.array(z.object({ - position: DisclosurePositionSchema, - persistence: z.array(DisclosurePersistenceSchema) - }).passthrough()).optional(), - format_card_detailed: z.object({ - format_id: FormatReferenceStructuredObjectSchema, - manifest: z.object({}).passthrough() - }).passthrough().optional(), - reported_metrics: z.array(AvailableMetricSchema).optional(), - pricing_options: z.array(VendorPricingOptionSchema).optional() -}).passthrough(); - -export const CatalogRequirementsSchema = z.object({ - catalog_type: CatalogTypeSchema, - required: z.boolean().optional(), - min_items: z.number().optional(), - max_items: z.number().optional(), - required_fields: z.array(z.string()).optional(), - feed_formats: z.array(FeedFormatSchema).optional(), - offering_asset_constraints: z.array(OfferingAssetConstraintSchema).optional(), - field_bindings: z.array(CatalogFieldBindingSchema).optional() -}).passthrough(); - export const ListCreativeFormatsResponseSchema = z.object({ formats: z.array(FormatSchema), creative_agents: z.array(z.object({ diff --git a/test/lib/zod-schemas.test.js b/test/lib/zod-schemas.test.js index 184f0a96a..2346a8ac0 100644 --- a/test/lib/zod-schemas.test.js +++ b/test/lib/zod-schemas.test.js @@ -887,4 +887,39 @@ describe('Zod Schema Validation', () => { const valid = geo.safeParse({ metro: { NYC: true } }); assert.ok(valid.success, 'metro record should accept boolean values'); }); + + test('per-asset-type requirements schemas are typed (not z.any)', async () => { + if (!schemas) { + schemas = await import('../../dist/lib/types/schemas.generated.js'); + } + + // Regression guard for #1659: ts-to-zod was emitting z.any() stubs for the 12 + // *AssetRequirementsSchema exports when tools.generated.ts referenced them via + // a cross-file `import type` block. Verify both that the exports exist and that + // they actually validate the field-level shape (z.any() would pass anything). + const exports = [ + ['ImageAssetRequirementsSchema', 'max_animation_duration_ms', 'aspect_ratio'], + ['VideoAssetRequirementsSchema', 'max_duration_ms', 'frame_rates'], + ['TextAssetRequirementsSchema', 'max_length', 'prohibited_terms'], + ]; + + for (const [name, knownField] of exports) { + const schema = schemas[name]; + assert.ok(schema, `${name} should be exported`); + // z.any() would accept a wrong-shape value. A typed object schema rejects it. + const wrongShape = schema.safeParse({ [knownField]: { not: 'the right type' } }); + assert.ok( + !wrongShape.success, + `${name} should reject wrong-typed ${knownField}; if this passes, the schema is z.any()` + ); + } + + // AssetRequirementsSchema must be a real union of the 12 typed schemas — not + // a union of z.any() (which would degenerate to z.any() and validate any value + // including a primitive). passthrough means stray keys are accepted, so we + // probe with a non-object value instead. + assert.ok(schemas.AssetRequirementsSchema, 'AssetRequirementsSchema should be exported'); + const bogus = schemas.AssetRequirementsSchema.safeParse('not-an-object'); + assert.ok(!bogus.success, 'AssetRequirementsSchema should reject non-object values'); + }); });