import { z } from "zod"; import type { NodeRuntimeCreateOptions } from "./node-runtime.js"; const permissionModeSchema = z.enum(["allow", "deny"]); const stringArray = z.array(z.string()); const vmIdSchema = z.number().int().min(0).max(0xffffffff); const vmAccountNameSchema = z .string() .min(1) .refine((value) => !/[:\s\0]/u.test(value), "Invalid account name"); const vmGuestPathSchema = z.string().startsWith("/"); const vmUserAccountSchema = z .object({ uid: vmIdSchema, gid: vmIdSchema, username: vmAccountNameSchema, homedir: vmGuestPathSchema, shell: vmGuestPathSchema, gecos: z.string().optional(), supplementaryGids: z.array(vmIdSchema).max(64), }) .strict(); const vmGroupSchema = z .object({ gid: vmIdSchema, name: vmAccountNameSchema, members: z.array(vmAccountNameSchema), }) .strict(); const vmUserConfigSchema = z .object({ uid: vmIdSchema.optional(), gid: vmIdSchema.optional(), euid: vmIdSchema.optional(), egid: vmIdSchema.optional(), username: z.string().optional(), homedir: z.string().optional(), shell: z.string().optional(), gecos: z.string().optional(), groupName: z.string().optional(), supplementaryGids: z.array(vmIdSchema).max(64).optional(), accounts: z.array(vmUserAccountSchema).max(64).optional(), groups: z.array(vmGroupSchema).max(128).optional(), }) .strict(); const fsPermissionRuleSchema = z .object({ mode: permissionModeSchema, operations: stringArray.optional(), paths: stringArray.optional(), }) .strict(); const patternPermissionRuleSchema = z .object({ mode: permissionModeSchema, operations: stringArray.optional(), patterns: stringArray.optional(), }) .strict(); const fsRulePermissionsSchema = z .object({ default: permissionModeSchema.optional(), rules: z.array(fsPermissionRuleSchema), }) .strict(); const patternRulePermissionsSchema = z .object({ default: permissionModeSchema.optional(), rules: z.array(patternPermissionRuleSchema), }) .strict(); const fsPermissionsSchema = z.union([permissionModeSchema, fsRulePermissionsSchema]); const patternPermissionsSchema = z.union([ permissionModeSchema, patternRulePermissionsSchema, ]); export const nodeRuntimePermissionsSchema = z .object({ fs: fsPermissionsSchema.optional(), network: patternPermissionsSchema.optional(), childProcess: patternPermissionsSchema.optional(), process: patternPermissionsSchema.optional(), env: patternPermissionsSchema.optional(), binding: patternPermissionsSchema.optional(), }) .strict(); const uint8ArraySchema = z.custom( (value: unknown) => value instanceof Uint8Array, { message: "Expected Uint8Array" }, ); const hostDirectoryMountSchema = z .object({ guestPath: z.string(), hostPath: z.string(), readOnly: z.boolean().optional(), }) .strict(); const nodeModulesMountSchema = z .object({ hostPath: z.string(), guestPath: z.string().optional(), }) .strict(); const jsRuntimeSchema = z .object({ platform: z.enum(["node", "browser", "neutral", "bare"]).optional(), moduleResolution: z.enum(["node", "relative", "none"]).optional(), allowedBuiltins: stringArray.optional(), highResolutionTime: z.boolean().optional(), }) .strict(); const bindingExampleSchema = z .object({ description: z.string(), input: z.unknown(), }) .strict(); const bindingDefinitionSchema = z .object({ description: z.string(), inputSchema: z.custom( (value: unknown) => typeof value === "object" && value !== null, { message: "Expected JSON Schema object" }, ), timeoutMs: z.number().int().nonnegative().optional(), examples: z.array(bindingExampleSchema).optional(), commandAliases: stringArray.optional(), handler: z.custom<(input: unknown) => unknown | Promise>( (value: unknown) => typeof value === "function", { message: "Expected function" }, ), }) .strict(); /** * Runtime validation for the public `NodeRuntime.create(...)` API. * * This is the TS-side guard for the ergonomic options shape. The sidecar VM * JSON it eventually produces is still validated by * `crates/vm-config/src/lib.rs::CreateVmConfig` with `deny_unknown_fields`. * Keep these in sync when adding high-level create options that translate into * the Rust VM config. */ export const nodeRuntimeCreateOptionsSchema = z .object({ filesystem: z.custom( (value: unknown) => typeof value === "object" && value !== null, { message: "Expected caller-owned VirtualFileSystem object" }, ), env: z.record(z.string(), z.string()).optional(), cwd: z.string().optional(), user: vmUserConfigSchema.optional(), permissions: nodeRuntimePermissionsSchema.optional(), commandsDir: z.string().optional(), wasmCommandDirs: stringArray.optional(), sidecar: z .custom((value: unknown) => typeof value === "object" && value !== null, { message: "Expected SidecarProcess object", }) .optional(), onBootTiming: z .custom<(timing: unknown) => void>( (value: unknown) => typeof value === "function", { message: "Expected function" }, ) .optional(), files: z .record(z.string(), z.union([z.string(), uint8ArraySchema])) .optional(), mounts: z.array(hostDirectoryMountSchema).optional(), nodeModules: z.union([z.string(), nodeModulesMountSchema]).optional(), bindings: z.record(z.string(), bindingDefinitionSchema).optional(), loopbackExemptPorts: z .array(z.number().int().min(0).max(65535)) .optional(), jsRuntime: jsRuntimeSchema.optional(), }) .strict() as z.ZodType; export function parseNodeRuntimeCreateOptions( options: NodeRuntimeCreateOptions, ): NodeRuntimeCreateOptions { return nodeRuntimeCreateOptionsSchema.parse(options); }