From 57e624561862520154350c36d540a13359088e1b Mon Sep 17 00:00:00 2001 From: Samuel Stroschein <35429197+samuelstroschein@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:41:33 -0800 Subject: [PATCH] Refactor validation logic to lazily initialize TypeBox compiler, improving performance in CSP-restricted environments. --- .changeset/worker-safe-validator.md | 5 +++ packages/zettel-ast/src/validate.test.ts | 51 ++++++++++++++++++++++++ packages/zettel-ast/src/validate.ts | 33 +++++++++++++-- 3 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 .changeset/worker-safe-validator.md create mode 100644 packages/zettel-ast/src/validate.test.ts diff --git a/.changeset/worker-safe-validator.md b/.changeset/worker-safe-validator.md new file mode 100644 index 0000000..c33f128 --- /dev/null +++ b/.changeset/worker-safe-validator.md @@ -0,0 +1,5 @@ +--- +"@opral/zettel-ast": minor +--- + +Use a lazy, Cloudflare-safe validator fallback (TypeBox Value.Check) to avoid eval in restricted environments. See https://github.com/sinclairzx81/typebox/issues/1095. diff --git a/packages/zettel-ast/src/validate.test.ts b/packages/zettel-ast/src/validate.test.ts new file mode 100644 index 0000000..6503b52 --- /dev/null +++ b/packages/zettel-ast/src/validate.test.ts @@ -0,0 +1,51 @@ +import { expect, test, vi } from "vitest"; + +const exampleDoc = { + type: "zettel_doc", + content: [ + { + type: "zettel_text_block", + zettel_key: "4ee4134378b1", + style: "zettel_normal", + children: [ + { + type: "zettel_span", + zettel_key: "e60571e00344", + text: "Hello world", + marks: [], + }, + ], + }, + ], +}; + +test("compiles lazily on first validate call", async () => { + vi.resetModules(); + const { TypeCompiler } = await import("@sinclair/typebox/compiler"); + const compileSpy = vi.spyOn(TypeCompiler, "Compile"); + const { validate } = await import("./validate.js"); + + expect(compileSpy).not.toHaveBeenCalled(); + validate(exampleDoc); + expect(compileSpy).toHaveBeenCalledTimes(1); + + compileSpy.mockRestore(); +}); + +test("falls back to dynamic checks when compile is blocked", async () => { + vi.resetModules(); + const { TypeCompiler } = await import("@sinclair/typebox/compiler"); + const compileSpy = vi + .spyOn(TypeCompiler, "Compile") + .mockImplementation(() => { + throw new Error("Eval disabled"); + }); + const { validate } = await import("./validate.js"); + + const result = validate(exampleDoc); + + expect(compileSpy).toHaveBeenCalledTimes(1); + expect(result.errors).toBeUndefined(); + + compileSpy.mockRestore(); +}); diff --git a/packages/zettel-ast/src/validate.ts b/packages/zettel-ast/src/validate.ts index c616fb0..3b17f77 100644 --- a/packages/zettel-ast/src/validate.ts +++ b/packages/zettel-ast/src/validate.ts @@ -1,7 +1,31 @@ -import { TypeCompiler } from "@sinclair/typebox/compiler"; +import { TypeCompiler, TypeCheck } from "@sinclair/typebox/compiler"; +import { Value } from "@sinclair/typebox/value"; import { ZettelDocJsonSchema, type ZettelDoc } from "./schema.js"; -const Z = TypeCompiler.Compile(ZettelDocJsonSchema); +// Lazily initialize to avoid evaluating TypeBox's compiler at module load in +// CSP-restricted runtimes (e.g. Cloudflare Workers). +let cachedValidator: TypeCheck | undefined; + +function getValidator() { + if (cachedValidator) { + return cachedValidator; + } + + try { + // Prefer compiled validator for speed when eval is permitted. + cachedValidator = TypeCompiler.Compile(ZettelDocJsonSchema); + } catch { + // Fall back to dynamic checking when code generation is blocked. + cachedValidator = new TypeCheck( + ZettelDocJsonSchema, + [], + (value) => Value.Check(ZettelDocJsonSchema, value), + "" + ); + } + + return cachedValidator; +} export type SerializableError = { message: string }; @@ -29,9 +53,10 @@ export type ValidationResult = * } */ export function validate(zettel: unknown): ValidationResult { - const result = Z.Check(zettel); + const validator = getValidator(); + const result = validator.Check(zettel); if (!result) { - const errors = [...Z.Errors(zettel)]; + const errors = [...validator.Errors(zettel)]; return { success: false, data: undefined,