From 6c72a22178c75853ec0aff187bf5cd62aec03c85 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 19 Mar 2026 11:30:37 -0600 Subject: [PATCH] SDK beta.62: fix dynamicSelect crash on empty values, add smtpShape - Guard z.union() against empty arrays in dynamicSelect/dynamicMultiselect by falling back to z.string() (fixes zod v4 _zod TypeError) - Add smtpShape: typed zod schema for store file models, replacing smtpInputSpec.validator which caused cross-zod-instance errors - Bump version to 0.4.0-beta.62 Co-Authored-By: Claude Opus 4.6 (1M context) --- sdk/CHANGELOG.md | 11 +++ sdk/base/lib/actions/input/builder/value.ts | 53 +++++------- .../lib/actions/input/inputSpecConstants.ts | 85 +++++++++++++++++++ sdk/package/lib/StartSdk.ts | 2 + sdk/package/package.json | 2 +- 5 files changed, 118 insertions(+), 35 deletions(-) diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index f980422b2..9adcecc15 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.4.0-beta.62 (2026-03-19) + +### Fixed + +- Fixed `Value.dynamicSelect` and `Value.dynamicMultiselect` crashing with `z.union([])` when `values` is empty (zod v4 compatibility) + +### Added + +- `FileHelper.xml`: file helper for XML files using `fast-xml-parser` +- `smtpShape`: typed zod schema for persisting SMTP selection in store file models, replacing direct use of `smtpInputSpec.validator` which caused cross-zod-instance errors + ## 0.4.0-beta.61 — StartOS v0.4.0-alpha.21 (2026-03-16) ### Fixed diff --git a/sdk/base/lib/actions/input/builder/value.ts b/sdk/base/lib/actions/input/builder/value.ts index e916e9ecf..431466563 100644 --- a/sdk/base/lib/actions/input/builder/value.ts +++ b/sdk/base/lib/actions/input/builder/value.ts @@ -15,6 +15,21 @@ import { _, once } from '../../../util' import { z } from 'zod' import { DeepPartial } from '../../../types' +/** Build a union-of-literals validator from object keys, falling back to z.string() when empty */ +function literalKeysValidator( + values: Record, +): z.ZodType { + const keys = Object.keys(values) + if (keys.length === 0) return z.string() + return z.union( + keys.map((x) => z.literal(x)) as [ + z.ZodLiteral, + z.ZodLiteral, + ...z.ZodLiteral[], + ], + ) +} + /** Zod schema for a file upload result — validates `{ path, commitment: { hash, size } }`. */ export const fileInfoParser = z.object({ path: z.string(), @@ -774,13 +789,7 @@ export class Value< */ immutable?: boolean }) { - const validator = z.union( - Object.keys(a.values).map((x: keyof Values & string) => z.literal(x)) as [ - z.ZodLiteral, - z.ZodLiteral, - ...z.ZodLiteral[], - ], - ) + const validator = literalKeysValidator(a.values) return new Value( () => ({ spec: { @@ -825,15 +834,7 @@ export class Value< immutable: false, ...a, }, - validator: z.union( - Object.keys(a.values).map((x: keyof Values & string) => - z.literal(x), - ) as [ - z.ZodLiteral, - z.ZodLiteral, - ...z.ZodLiteral[], - ], - ), + validator: literalKeysValidator(a.values), } }, z.string(), @@ -891,15 +892,7 @@ export class Value< */ immutable?: boolean }) { - const validator = z.array( - z.union( - Object.keys(a.values).map((x) => z.literal(x)) as [ - z.ZodLiteral, - z.ZodLiteral, - ...z.ZodLiteral[], - ], - ), - ) + const validator = z.array(literalKeysValidator(a.values)) return new Value<(keyof Values & string)[]>( () => ({ spec: { @@ -953,15 +946,7 @@ export class Value< immutable: false, ...a, }, - validator: z.array( - z.union( - Object.keys(a.values).map((x) => z.literal(x)) as [ - z.ZodLiteral, - z.ZodLiteral, - ...z.ZodLiteral[], - ], - ), - ), + validator: z.array(literalKeysValidator(a.values)), } }, z.array(z.string())) } diff --git a/sdk/base/lib/actions/input/inputSpecConstants.ts b/sdk/base/lib/actions/input/inputSpecConstants.ts index 54c7afd7d..f83427049 100644 --- a/sdk/base/lib/actions/input/inputSpecConstants.ts +++ b/sdk/base/lib/actions/input/inputSpecConstants.ts @@ -2,6 +2,7 @@ import { GetSystemSmtp, Patterns } from '../../util' import { InputSpec } from './builder/inputSpec' import { Value } from './builder/value' import { Variants } from './builder/variants' +import { z } from 'zod' const securityVariants = Variants.of({ tls: { @@ -182,3 +183,87 @@ export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => { variants: smtpVariants, } }, smtpVariants.validator) + +const securityShape = z + .object({ + selection: z.enum(['tls', 'starttls']).catch('tls'), + value: z.object({ port: z.string().catch('465') }).catch({ port: '465' }), + }) + .catch({ selection: 'tls' as const, value: { port: '465' } }) + +const providerShape = z + .object({ + selection: z.string().catch('other'), + value: z + .object({ + host: z.string().catch(''), + from: z.string().catch(''), + username: z.string().catch(''), + password: z.string().nullable().optional().catch(null), + security: securityShape, + }) + .catch({ + host: '', + from: '', + username: '', + password: null, + security: securityShape.parse(undefined), + }), + }) + .catch({ + selection: 'other', + value: { + host: '', + from: '', + username: '', + password: null, + security: securityShape.parse(undefined), + }, + }) + +export type SmtpSelection = + | { selection: 'disabled'; value: Record } + | { selection: 'system'; value: { customFrom?: string | null } } + | { + selection: 'custom' + value: { + provider: { + selection: string + value: { + host: string + from: string + username: string + password?: string | null + security: { + selection: 'tls' | 'starttls' + value: { port: string } + } + } + } + } + } + +/** + * Zod schema for persisting SMTP selection in a store file model. + * Use this instead of `smtpInputSpec.validator` to avoid cross-zod-instance issues. + */ +export const smtpShape: z.ZodCatch> = z + .discriminatedUnion('selection', [ + z.object({ + selection: z.literal('disabled'), + value: z.object({}).catch({}), + }), + z.object({ + selection: z.literal('system'), + value: z + .object({ customFrom: z.string().nullable().optional().catch(null) }) + .catch({ customFrom: null }), + }), + z.object({ + selection: z.literal('custom'), + value: z + .object({ provider: providerShape }) + .catch({ provider: providerShape.parse(undefined) }), + }), + ]) + .catch({ selection: 'disabled' as const, value: {} }) diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 6bf925141..6f4e9cd47 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -11,6 +11,7 @@ import * as patterns from '../../base/lib/util/patterns' import { Backups } from './backup/Backups' import { smtpInputSpec, + smtpShape, systemSmtpSpec, customSmtp, smtpProviderVariants, @@ -408,6 +409,7 @@ export class StartSdk { }, inputSpecConstants: { smtpInputSpec, + smtpShape, systemSmtpSpec, customSmtp, smtpProviderVariants, diff --git a/sdk/package/package.json b/sdk/package/package.json index 855389278..182235178 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.61", + "version": "0.4.0-beta.62", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts",