})
.strict()
+/**
+ * UIServerListenOptionsObjectSchema — typed object layer for `node:net`
+ * `ListenOptions`. Validates known primitive fields (port, host, backlog, ...)
+ * at boot time so that bad transport-level values (e.g. `port: "not-a-number"`)
+ * fail in `ConfigurationSchema.safeParse` rather than later in `Server.listen`.
+ * Unknown keys are passed through (`.loose()`) to preserve the `ListenOptions`
+ * extension surface (e.g. `signal: AbortSignal`).
+ */
+const UIServerListenOptionsObjectSchema = z
+ .object({
+ backlog: z.number().int().nonnegative().optional(),
+ exclusive: z.boolean().optional(),
+ host: z.string().min(1).optional(),
+ ipv6Only: z.boolean().optional(),
+ path: z.string().min(1).optional(),
+ port: z.number().int().min(0).max(65535).optional(),
+ readableAll: z.boolean().optional(),
+ writableAll: z.boolean().optional(),
+ })
+ .loose()
+
+/**
+ * UIServerListenOptionsSchema — composite schema for `uiServer.options`:
+ * non-array object guard → `accessPolicy` misplacement refinement → typed
+ * field validation via `UIServerListenOptionsObjectSchema`.
+ */
const UIServerListenOptionsSchema = z
.custom<ListenOptions>(
value => value != null && typeof value === 'object' && !Array.isArray(value),
.refine(value => !Object.hasOwn(value as object, 'accessPolicy'), {
message: "'accessPolicy' must be configured under 'uiServer', not 'uiServer.options'",
})
+ .pipe(UIServerListenOptionsObjectSchema)
/**
* UIServerConfiguration — UI server configuration section.
- * `options` is structurally typed as `ListenOptions` from node:net; the schema
- * uses `z.custom<ListenOptions>()` to bridge the external surface.
+ * `options` is structurally typed as `ListenOptions` from node:net and
+ * validated by `UIServerListenOptionsSchema` (object guard → `accessPolicy`
+ * refinement → typed field validation).
*/
export const UIServerConfigurationSchema = z
.object({
)
})
+ await describe('uiServer.options.port', async () => {
+ await it('should reject non-numeric string port "not-a-number"', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: 'localhost', port: 'not-a-number' } },
+ })
+ )
+ assert.ok(!result.success)
+ const paths = result.error.issues.map(i => i.path.join('.'))
+ assert.ok(
+ paths.includes('uiServer.options.port'),
+ `Expected error path 'uiServer.options.port' in ${JSON.stringify(paths)}`
+ )
+ })
+
+ await it('should reject negative port (-1)', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: 'localhost', port: -1 } },
+ })
+ )
+ assert.ok(!result.success)
+ assert.ok(result.error.issues.some(i => i.path.join('.') === 'uiServer.options.port'))
+ })
+
+ await it('should reject port 65536 (out of range)', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: 'localhost', port: 65536 } },
+ })
+ )
+ assert.ok(!result.success)
+ assert.ok(result.error.issues.some(i => i.path.join('.') === 'uiServer.options.port'))
+ })
+
+ await it('should reject non-integer port 3.14', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: 'localhost', port: 3.14 } },
+ })
+ )
+ assert.ok(!result.success)
+ assert.ok(result.error.issues.some(i => i.path.join('.') === 'uiServer.options.port'))
+ })
+
+ await it('should accept port 8080', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: 'localhost', port: 8080 } },
+ })
+ )
+ assert.ok(
+ result.success,
+ `Expected port 8080 to be accepted: ${result.success ? '' : JSON.stringify(result.error.issues)}`
+ )
+ })
+
+ await it('should accept port 0 (OS-picked port)', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: 'localhost', port: 0 } },
+ })
+ )
+ assert.ok(
+ result.success,
+ `Expected port 0 to be accepted: ${result.success ? '' : JSON.stringify(result.error.issues)}`
+ )
+ })
+
+ await it('should accept port 65535 (max valid)', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: 'localhost', port: 65535 } },
+ })
+ )
+ assert.ok(
+ result.success,
+ `Expected port 65535 to be accepted: ${result.success ? '' : JSON.stringify(result.error.issues)}`
+ )
+ })
+ })
+
+ await describe('uiServer.options.host', async () => {
+ await it('should reject empty host string', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: '', port: 8080 } },
+ })
+ )
+ assert.ok(!result.success)
+ const paths = result.error.issues.map(i => i.path.join('.'))
+ assert.ok(
+ paths.includes('uiServer.options.host'),
+ `Expected error path 'uiServer.options.host' in ${JSON.stringify(paths)}`
+ )
+ })
+
+ await it('should accept host "localhost"', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: 'localhost', port: 8080 } },
+ })
+ )
+ assert.ok(
+ result.success,
+ `Expected host 'localhost' to be accepted: ${result.success ? '' : JSON.stringify(result.error.issues)}`
+ )
+ })
+
+ await it('should reject non-string host', () => {
+ const result = ConfigurationSchema.safeParse(
+ buildMinimalConfiguration({
+ uiServer: { options: { host: 1234, port: 8080 } },
+ })
+ )
+ assert.ok(!result.success)
+ assert.ok(result.error.issues.some(i => i.path.join('.') === 'uiServer.options.host'))
+ })
+ })
+
await it('should reject hostnames in trustedProxies', () => {
const result = ConfigurationSchema.safeParse(
buildMinimalConfiguration({
assert.strictEqual(error.message, EXPECTED_SNAPSHOT)
}
})
+
+ await it('should fail-fast with structured uiServer.options.port path on invalid port', () => {
+ const parsed = buildMinimalConfiguration({
+ uiServer: {
+ enabled: true,
+ options: { host: 'localhost', port: 'not-a-number' },
+ type: 'ws',
+ },
+ })
+ try {
+ validateConfiguration(parsed, 'bad-port.json')
+ assert.fail('Expected ConfigurationValidationError')
+ } catch (error) {
+ assert.ok(error instanceof ConfigurationValidationError)
+ assert.strictEqual(error.phase, 'schema')
+ const portErrors = error.fieldErrors.filter(e => e.path === 'uiServer.options.port')
+ assert.strictEqual(
+ portErrors.length,
+ 1,
+ `Expected one fieldError on 'uiServer.options.port', got ${JSON.stringify(error.fieldErrors)}`
+ )
+ assert.match(error.message, /uiServer\.options\.port/)
+ assert.match(error.message, /\[schema\]/)
+ }
+ })
})
})