Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
export type { SandboxConfigPartial } from "./schemas.js";
export {
parseConfig,
parseModule,
parseOptions,
SandboxConfigPartialSchema,
SandboxEngineKindSchema,
SandboxModuleSchema,
SandboxOptionsSchema,
} from "./schemas.js";

View File

@@ -0,0 +1,65 @@
# Schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas guard every public boundary of `@suaveplan/sandbox`. The package never trusts its caller; instead, every argument that crosses the API boundary is parsed against a strict schema and rejected with a typed `SandboxError` (`INVALID_CONFIG` / `INVALID_OPTIONS` / `INVALID_MODULE`) when the shape does not match. This means downstream consumers get the same validation guarantees whether they pass typed TypeScript objects or arbitrary JSON deserialised from a network boundary.
The `parseConfig` helper additionally merges parsed partials with `DEFAULT_CONFIG` so the rest of the package consumes a fully resolved `SandboxConfig` rather than a partial. This keeps the merge logic in one place rather than re-implementing it across the facade and the engines.
## Features
- Strict-mode schemas (`.strict()`) so unknown keys are rejected.
- `parseConfig`, `parseOptions`, `parseModule` helpers that translate Zod
errors into `SandboxError` with the right code.
- `SandboxEngineKindSchema` enum used as the discriminant for engine selection.
- `safeParse` under the hood so the error path stays on the catch-side and
the success path stays on the happy path.
## Usage: Basic
```ts
import { parseConfig } from "@suaveplan/sandbox/schemas";
const cfg = parseConfig({ timeout: 1000, engine: "worker" });
```
## Usage: Advanced
When validating untrusted input (HTTP, IPC), surface the raw Zod issues to
the caller via `SandboxError.details.issues`:
```ts
import { SandboxError } from "@suaveplan/sandbox/error-codes";
import { parseConfig } from "@suaveplan/sandbox/schemas";
try {
parseConfig(JSON.parse(httpBody));
} catch (err) {
if (err instanceof SandboxError) {
return Response.json({ issues: err.details.issues }, { status: 400 });
}
throw err;
}
```
## API Reference
- `SandboxConfigPartialSchema` — `z.ZodObject` for `Partial<SandboxConfig>`.
- `SandboxOptionsSchema` — `z.ZodObject` for `SandboxOptions`.
- `SandboxModuleSchema` — `z.ZodObject` for `SandboxModule`.
- `SandboxEngineKindSchema` — `z.ZodEnum<['vm','worker','spawn','isolate']>`.
- `parseConfig(raw): SandboxConfig` — merges with `DEFAULT_CONFIG`.
- `parseOptions(raw): SandboxOptions`.
- `parseModule(raw): SandboxModule`.
## Implementation Notes
`onAuditLog` is validated via `z.custom` rather than `z.function` because Zod's
function schema would otherwise wrap the callback in a runtime validator and
break referential equality, breaking the "consumer-passes-its-own-callback"
contract. The custom validator only checks `typeof === "function"` (or
undefined), which is the actual invariant the consumer cares about.

View File

@@ -0,0 +1,97 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SANDBOX_CODES, SandboxError } from "../error-codes/error-codes.js";
import { DEFAULT_CONFIG } from "../types/sandbox.types.js";
import { parseConfig, parseModule, parseOptions } from "./schemas.js";
describe("parseConfig", () => {
it("returns DEFAULT_CONFIG when given undefined", { timeout: 5000 }, () => {
const cfg = parseConfig(undefined);
expect(cfg.timeout).toBe(DEFAULT_CONFIG.timeout);
expect(cfg.engine).toBe("vm");
expect(cfg.allowedModules).toEqual([]);
});
it("merges partial config with defaults", { timeout: 5000 }, () => {
const cfg = parseConfig({ timeout: 1000, engine: "worker" });
expect(cfg.timeout).toBe(1000);
expect(cfg.engine).toBe("worker");
expect(cfg.memoryLimit).toBe(DEFAULT_CONFIG.memoryLimit);
});
it("includes onAuditLog when supplied", { timeout: 5000 }, () => {
const cb = () => {};
const cfg = parseConfig({ onAuditLog: cb });
expect(cfg.onAuditLog).toBe(cb);
});
it("rejects negative timeout", { timeout: 5000 }, () => {
try {
parseConfig({ timeout: -1 } as never);
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(SandboxError);
expect((err as SandboxError).code).toBe(
SANDBOX_CODES.INVALID_CONFIG
);
}
});
it("rejects unknown engine kinds", { timeout: 5000 }, () => {
expect(() => parseConfig({ engine: "rust-wasm" } as never)).toThrow(
SandboxError
);
});
});
describe("parseOptions", () => {
it("accepts undefined", { timeout: 5000 }, () => {
expect(parseOptions(undefined)).toEqual({});
});
it("accepts a valid options bag", { timeout: 5000 }, () => {
expect(parseOptions({ timeout: 100 })).toEqual({ timeout: 100 });
});
it("rejects negative timeout", { timeout: 5000 }, () => {
try {
parseOptions({ timeout: -5 });
throw new Error("expected throw");
} catch (err) {
expect((err as SandboxError).code).toBe(
SANDBOX_CODES.INVALID_OPTIONS
);
}
});
it("passes memoryLimit + maxOutputSize through", {
timeout: 5000,
}, () => {
expect(parseOptions({ memoryLimit: 1000, maxOutputSize: 50 })).toEqual({
memoryLimit: 1000,
maxOutputSize: 50,
});
});
});
describe("parseModule", () => {
it("accepts a valid module", { timeout: 5000 }, () => {
const mod = parseModule({ name: "config", exports: { x: 1 } });
expect(mod.name).toBe("config");
expect(mod.exports.x).toBe(1);
});
it("rejects an empty name", { timeout: 5000 }, () => {
try {
parseModule({ name: "", exports: {} });
throw new Error("expected throw");
} catch (err) {
expect((err as SandboxError).code).toBe(
SANDBOX_CODES.INVALID_MODULE
);
}
});
it("rejects a missing exports field", { timeout: 5000 }, () => {
expect(() => parseModule({ name: "x" })).toThrow(SandboxError);
});
});

View File

@@ -0,0 +1,129 @@
import z from "zod";
import { SANDBOX_CODES, SandboxError } from "../error-codes/error-codes.js";
import {
DEFAULT_CONFIG,
type SandboxConfig,
type SandboxModule,
type SandboxOptions,
} from "../types/sandbox.types.js";
/** Engine kinds accepted by `createSandbox`. */
export const SandboxEngineKindSchema = z.enum([
"vm",
"worker",
"spawn",
"isolate",
]);
/** Partial config schema (used at `createSandbox` boundary). */
export const SandboxConfigPartialSchema = z
.object({
timeout: z.number().int().positive().optional(),
memoryLimit: z.number().int().positive().optional(),
allowedModules: z.array(z.string().min(1)).optional(),
maxOutputSize: z.number().int().positive().optional(),
maxExecutionsPerWindow: z.number().int().positive().optional(),
rateLimitWindow: z.number().int().positive().optional(),
maxCodeLength: z.number().int().positive().optional(),
engine: SandboxEngineKindSchema.optional(),
onAuditLog: z
.custom<SandboxConfig["onAuditLog"]>(
(v) => v === undefined || typeof v === "function",
"onAuditLog must be a function"
)
.optional(),
})
.strict();
export type SandboxConfigPartial = z.infer<typeof SandboxConfigPartialSchema>;
/** Per-execution options schema. */
export const SandboxOptionsSchema = z
.object({
timeout: z.number().int().positive().optional(),
memoryLimit: z.number().int().positive().optional(),
maxOutputSize: z.number().int().positive().optional(),
})
.strict();
/** Sandbox-module schema. */
export const SandboxModuleSchema = z
.object({
name: z.string().min(1),
exports: z.record(z.string(), z.unknown()),
})
.strict();
/**
* Parse a partial config and merge with `DEFAULT_CONFIG`. Throws
* `SandboxError(INVALID_CONFIG)` on parse failure.
*/
export function parseConfig(
raw: SandboxConfigPartial | undefined
): SandboxConfig {
const parsed = SandboxConfigPartialSchema.safeParse(raw ?? {});
if (!parsed.success) {
throw new SandboxError(
SANDBOX_CODES.INVALID_CONFIG,
"Invalid sandbox config",
{ details: { issues: parsed.error.issues } }
);
}
const merged: SandboxConfig = {
timeout: parsed.data.timeout ?? DEFAULT_CONFIG.timeout,
memoryLimit: parsed.data.memoryLimit ?? DEFAULT_CONFIG.memoryLimit,
allowedModules:
parsed.data.allowedModules ?? DEFAULT_CONFIG.allowedModules,
maxOutputSize:
parsed.data.maxOutputSize ?? DEFAULT_CONFIG.maxOutputSize,
maxExecutionsPerWindow:
parsed.data.maxExecutionsPerWindow ??
DEFAULT_CONFIG.maxExecutionsPerWindow,
rateLimitWindow:
parsed.data.rateLimitWindow ?? DEFAULT_CONFIG.rateLimitWindow,
maxCodeLength:
parsed.data.maxCodeLength ?? DEFAULT_CONFIG.maxCodeLength,
engine: parsed.data.engine ?? DEFAULT_CONFIG.engine,
...(parsed.data.onAuditLog === undefined
? {}
: { onAuditLog: parsed.data.onAuditLog }),
};
return merged;
}
/** Parse per-execution options. Throws `SandboxError(INVALID_OPTIONS)`. */
export function parseOptions(raw: unknown): SandboxOptions {
const parsed = SandboxOptionsSchema.safeParse(raw ?? {});
if (!parsed.success) {
throw new SandboxError(
SANDBOX_CODES.INVALID_OPTIONS,
"Invalid sandbox options",
{ details: { issues: parsed.error.issues } }
);
}
const out: SandboxOptions = {
...(parsed.data.timeout === undefined
? {}
: { timeout: parsed.data.timeout }),
...(parsed.data.memoryLimit === undefined
? {}
: { memoryLimit: parsed.data.memoryLimit }),
...(parsed.data.maxOutputSize === undefined
? {}
: { maxOutputSize: parsed.data.maxOutputSize }),
};
return out;
}
/** Parse a SandboxModule. Throws `SandboxError(INVALID_MODULE)`. */
export function parseModule(raw: unknown): SandboxModule {
const parsed = SandboxModuleSchema.safeParse(raw);
if (!parsed.success) {
throw new SandboxError(
SANDBOX_CODES.INVALID_MODULE,
"Invalid sandbox module",
{ details: { issues: parsed.error.issues } }
);
}
return parsed.data;
}