mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# 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)
|
## 0.4.0-beta.61 — StartOS v0.4.0-alpha.21 (2026-03-16)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -15,6 +15,21 @@ import { _, once } from '../../../util'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { DeepPartial } from '../../../types'
|
import { DeepPartial } from '../../../types'
|
||||||
|
|
||||||
|
/** Build a union-of-literals validator from object keys, falling back to z.string() when empty */
|
||||||
|
function literalKeysValidator(
|
||||||
|
values: Record<string, unknown>,
|
||||||
|
): z.ZodType<string> {
|
||||||
|
const keys = Object.keys(values)
|
||||||
|
if (keys.length === 0) return z.string()
|
||||||
|
return z.union(
|
||||||
|
keys.map((x) => z.literal(x)) as [
|
||||||
|
z.ZodLiteral<string>,
|
||||||
|
z.ZodLiteral<string>,
|
||||||
|
...z.ZodLiteral<string>[],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Zod schema for a file upload result — validates `{ path, commitment: { hash, size } }`. */
|
/** Zod schema for a file upload result — validates `{ path, commitment: { hash, size } }`. */
|
||||||
export const fileInfoParser = z.object({
|
export const fileInfoParser = z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
@@ -774,13 +789,7 @@ export class Value<
|
|||||||
*/
|
*/
|
||||||
immutable?: boolean
|
immutable?: boolean
|
||||||
}) {
|
}) {
|
||||||
const validator = z.union(
|
const validator = literalKeysValidator(a.values)
|
||||||
Object.keys(a.values).map((x: keyof Values & string) => z.literal(x)) as [
|
|
||||||
z.ZodLiteral<string>,
|
|
||||||
z.ZodLiteral<string>,
|
|
||||||
...z.ZodLiteral<string>[],
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return new Value<keyof Values & string>(
|
return new Value<keyof Values & string>(
|
||||||
() => ({
|
() => ({
|
||||||
spec: {
|
spec: {
|
||||||
@@ -825,15 +834,7 @@ export class Value<
|
|||||||
immutable: false,
|
immutable: false,
|
||||||
...a,
|
...a,
|
||||||
},
|
},
|
||||||
validator: z.union(
|
validator: literalKeysValidator(a.values),
|
||||||
Object.keys(a.values).map((x: keyof Values & string) =>
|
|
||||||
z.literal(x),
|
|
||||||
) as [
|
|
||||||
z.ZodLiteral<string>,
|
|
||||||
z.ZodLiteral<string>,
|
|
||||||
...z.ZodLiteral<string>[],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
z.string(),
|
z.string(),
|
||||||
@@ -891,15 +892,7 @@ export class Value<
|
|||||||
*/
|
*/
|
||||||
immutable?: boolean
|
immutable?: boolean
|
||||||
}) {
|
}) {
|
||||||
const validator = z.array(
|
const validator = z.array(literalKeysValidator(a.values))
|
||||||
z.union(
|
|
||||||
Object.keys(a.values).map((x) => z.literal(x)) as [
|
|
||||||
z.ZodLiteral<string>,
|
|
||||||
z.ZodLiteral<string>,
|
|
||||||
...z.ZodLiteral<string>[],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return new Value<(keyof Values & string)[]>(
|
return new Value<(keyof Values & string)[]>(
|
||||||
() => ({
|
() => ({
|
||||||
spec: {
|
spec: {
|
||||||
@@ -953,15 +946,7 @@ export class Value<
|
|||||||
immutable: false,
|
immutable: false,
|
||||||
...a,
|
...a,
|
||||||
},
|
},
|
||||||
validator: z.array(
|
validator: z.array(literalKeysValidator(a.values)),
|
||||||
z.union(
|
|
||||||
Object.keys(a.values).map((x) => z.literal(x)) as [
|
|
||||||
z.ZodLiteral<string>,
|
|
||||||
z.ZodLiteral<string>,
|
|
||||||
...z.ZodLiteral<string>[],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}, z.array(z.string()))
|
}, z.array(z.string()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { GetSystemSmtp, Patterns } from '../../util'
|
|||||||
import { InputSpec } from './builder/inputSpec'
|
import { InputSpec } from './builder/inputSpec'
|
||||||
import { Value } from './builder/value'
|
import { Value } from './builder/value'
|
||||||
import { Variants } from './builder/variants'
|
import { Variants } from './builder/variants'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
const securityVariants = Variants.of({
|
const securityVariants = Variants.of({
|
||||||
tls: {
|
tls: {
|
||||||
@@ -182,3 +183,87 @@ export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => {
|
|||||||
variants: smtpVariants,
|
variants: smtpVariants,
|
||||||
}
|
}
|
||||||
}, smtpVariants.validator)
|
}, 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<string, never> }
|
||||||
|
| { 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.ZodType<SmtpSelection>> = 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: {} })
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import * as patterns from '../../base/lib/util/patterns'
|
|||||||
import { Backups } from './backup/Backups'
|
import { Backups } from './backup/Backups'
|
||||||
import {
|
import {
|
||||||
smtpInputSpec,
|
smtpInputSpec,
|
||||||
|
smtpShape,
|
||||||
systemSmtpSpec,
|
systemSmtpSpec,
|
||||||
customSmtp,
|
customSmtp,
|
||||||
smtpProviderVariants,
|
smtpProviderVariants,
|
||||||
@@ -408,6 +409,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
|||||||
},
|
},
|
||||||
inputSpecConstants: {
|
inputSpecConstants: {
|
||||||
smtpInputSpec,
|
smtpInputSpec,
|
||||||
|
smtpShape,
|
||||||
systemSmtpSpec,
|
systemSmtpSpec,
|
||||||
customSmtp,
|
customSmtp,
|
||||||
smtpProviderVariants,
|
smtpProviderVariants,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@start9labs/start-sdk",
|
"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",
|
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||||
"main": "./package/lib/index.js",
|
"main": "./package/lib/index.js",
|
||||||
"types": "./package/lib/index.d.ts",
|
"types": "./package/lib/index.d.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user