Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
# wire.schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the Zod schemas that pin the **wire-format invariants** of the serialize codecs. Both the JSON codec and the MessagePack codec produce structurally identical intermediate values: a recursive tree of JSON-compatible primitives, arrays, plain records, and tagged envelopes (`{ "__t": "<tag>", "v": <encoded> }`). This module defends the public `deserialize` and `decodeBinary` boundaries against accidental or malicious inputs that would otherwise reach the type registry's `decode` functions in an unexpected shape — for example a number where an envelope is expected, or an envelope with `__t` set to a non-string value.
The schemas are exported as both reusable values (`WireNodeSchema`, `TaggedEnvelopeSchema`) and an ergonomic helper (`validateWireNode`) that wraps `safeParse` and exposes the parse outcome as a discriminated union so call sites can wrap rejection in a `MalformedWireError` with stable code metadata.
## Features
- Recursive `WireNodeSchema` covering primitives, arrays, tagged envelopes, and plain records
- Strict `TaggedEnvelopeSchema` that rejects extra keys
- `validateWireNode` helper returning `{ ok: true; value } | { ok: false; error }`
- Used by `deserialize` (JSON) and `decodeBinary` (MessagePack) at the public boundary
- Pure, side-effect free; tree-shakes when unused
## Usage: Basic Example
```ts
import { validateWireNode } from "@suaveplan/serialize";
const result = validateWireNode({ __t: "__Date", v: "2024-01-01T00:00:00.000Z" });
if (result.ok) {
// safe to feed to the type-registry decode pipeline
}
```
## Usage: Advanced Example
```ts
import { MalformedWireError, validateWireNode } from "@suaveplan/serialize";
function strictDeserialize(input: unknown): unknown {
const wire = validateWireNode(input);
if (!wire.ok) {
throw new MalformedWireError(
"input does not match the tagged-wire schema",
"root",
wire.error
);
}
return wire.value;
}
```
## API Reference
- `WireNodeSchema: z.ZodType<unknown>` — recursive schema for any wire-format node.
- `TaggedEnvelopeSchema: z.ZodObject<{ __t: ZodString, v: ZodUnknown }>` — strict envelope schema.
- `validateWireNode(value: unknown): { ok: true; value: unknown } | { ok: false; error: ZodError }` — non-throwing validator.
## Implementation Notes
`WireNodeSchema` is defined via `z.lazy()` to support recursion through arrays and records. The envelope schema uses `.strict()` so an envelope with a smuggled-in third key is rejected — this matches the `serialize`/`encodeBinary` output, which only ever emits `__t` + `v`. We intentionally exclude `BigInt`, `Map`, `Set`, and other JS-only constructs from the schema: the wire format never produces them at this layer (BigInts arrive as a `__BigInt` envelope with a string `v`), so admitting them would weaken the invariant.

View File

@@ -0,0 +1,283 @@
/**
* Spec-conformance tests for SZ-10 — Zod schema validation at the public
* boundary. Pins:
* - WireNodeSchema accepts every valid wire shape produced by serialize.
* - WireNodeSchema rejects malformed envelopes (non-string __t, extra keys).
* - deserialize rejects malformed JSON wire input with MalformedWireError.
* - decodeBinary rejects malformed msgpack wire input with MalformedWireError.
* - serialize / deserialize / decodeBinary reject invalid options with
* InvalidOptionsError (typed by stable error code).
*/
import { describe, expect, expectTypeOf, it } from "@suaveplan/testing/runner";
import {
InvalidOptionsError,
MalformedWireError,
} from "../../errors/serialize-error.js";
import { registerType, unregisterType } from "../../registry/registry.js";
import { deserialize } from "../json/json-decode.js";
import { serialize } from "../json/json-encode.js";
import { decodeBinary } from "../msgpack/msgpack-decode.js";
import { encodeBinary } from "../msgpack/msgpack-encode.js";
import {
TaggedEnvelopeSchema,
validateWireNode,
WireNodeSchema,
} from "./wire.schemas.js";
describe("SZ-10 — WireNodeSchema acceptance", () => {
it("accepts JSON primitives", { timeout: 5000 }, () => {
for (const v of [null, true, false, 1, "x"]) {
expect(WireNodeSchema.safeParse(v).success).toBe(true);
}
});
it("accepts arrays of nodes", { timeout: 5000 }, () => {
expect(WireNodeSchema.safeParse([1, "x", null]).success).toBe(true);
});
it("accepts plain records", { timeout: 5000 }, () => {
expect(WireNodeSchema.safeParse({ a: 1, b: "x" }).success).toBe(true);
});
it("accepts a tagged envelope", { timeout: 5000 }, () => {
expect(
WireNodeSchema.safeParse({ __t: "__Date", v: "2024-01-01" }).success
).toBe(true);
});
});
describe("SZ-10 — WireNodeSchema rejection", () => {
it("rejects undefined", { timeout: 5000 }, () => {
expect(WireNodeSchema.safeParse(undefined).success).toBe(false);
});
it("rejects symbols and functions", { timeout: 5000 }, () => {
expect(WireNodeSchema.safeParse(Symbol("x")).success).toBe(false);
expect(WireNodeSchema.safeParse(() => 0).success).toBe(false);
});
it("strict envelope rejects extra keys", { timeout: 5000 }, () => {
const r = TaggedEnvelopeSchema.safeParse({
__t: "__Date",
v: "x",
extra: 1,
});
expect(r.success).toBe(false);
});
it("validateWireNode returns ZodError on rejection", {
timeout: 5000,
}, () => {
const r = validateWireNode(undefined);
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.error.issues.length).toBeGreaterThan(0);
}
});
});
describe("SZ-10 — deserialize boundary validation", () => {
it("invalid JSON throws MalformedWireError", { timeout: 5000 }, () => {
expect(() => deserialize("{not json")).toThrowError(MalformedWireError);
});
it("malformed envelope (non-string __t) throws MalformedWireError", {
timeout: 5000,
}, () => {
const bad = JSON.stringify({ __t: 42, v: null });
expect(() => deserialize(bad)).toThrowError(MalformedWireError);
});
it("MalformedWireError carries CODEC.MALFORMED_WIRE code", {
timeout: 5000,
}, () => {
try {
deserialize("{not json");
} catch (e) {
expect((e as MalformedWireError).code).toBe(
"SERIALIZE.CODEC.MALFORMED_WIRE"
);
return;
}
throw new Error("expected throw");
});
});
describe("SZ-10 — decodeBinary boundary validation", () => {
it("invalid msgpack bytes throw MalformedWireError", {
timeout: 5000,
}, () => {
// 0xc1 is reserved/never-used in msgpack and triggers a decode error.
const bad = new Uint8Array([0xc1, 0xc1, 0xc1]);
expect(() => decodeBinary(bad)).toThrowError(MalformedWireError);
});
it("round-trips valid msgpack input", { timeout: 5000 }, () => {
const bytes = encodeBinary({ a: 1, b: "two" });
const back = decodeBinary(bytes);
expect(back).toEqual({ a: 1, b: "two" });
});
});
describe("SZ-10 — invalid options rejection", () => {
it("serialize throws InvalidOptionsError for non-positive maxDepth", {
timeout: 5000,
}, () => {
expect(() => serialize({}, { maxDepth: -1 } as never)).toThrowError(
InvalidOptionsError
);
});
it("serialize throws InvalidOptionsError for non-positive maxSize", {
timeout: 5000,
}, () => {
expect(() => serialize({}, { maxSize: 0 } as never)).toThrowError(
InvalidOptionsError
);
});
it("serialize throws InvalidOptionsError for unknown option keys", {
timeout: 5000,
}, () => {
expect(() => serialize({}, { unknown: 1 } as never)).toThrowError(
InvalidOptionsError
);
});
it("deserialize throws InvalidOptionsError for non-AbortSignal signal", {
timeout: 5000,
}, () => {
expect(() =>
deserialize("null", { signal: "nope" as unknown as AbortSignal })
).toThrowError(InvalidOptionsError);
});
it("encodeBinary throws InvalidOptionsError for non-positive maxSize", {
timeout: 5000,
}, () => {
expect(() => encodeBinary({}, { maxSize: -10 } as never)).toThrowError(
InvalidOptionsError
);
});
it("decodeBinary throws InvalidOptionsError for unknown option keys", {
timeout: 5000,
}, () => {
const bytes = encodeBinary({ a: 1 });
expect(() => decodeBinary(bytes, { unknown: 1 } as never)).toThrowError(
InvalidOptionsError
);
});
});
describe("SZ-10 — error class branch coverage", () => {
it("MalformedWireError works without a path argument", {
timeout: 5000,
}, () => {
const err = new MalformedWireError("no-path");
expect(err.code).toBe("SERIALIZE.CODEC.MALFORMED_WIRE");
expect(err.context?.["tag"]).toBe("no-path");
expect(err.context?.["path"]).toBeUndefined();
});
it("InvalidOptionsError works with cause", { timeout: 5000 }, () => {
const cause = new Error("inner");
const err = new InvalidOptionsError("invalid", cause);
expect(err.code).toBe("SERIALIZE.OPTIONS.INVALID");
expect(err.cause).toBe(cause);
});
});
describe("SZ-10 — msgpack wire-schema rejection", () => {
it("decodeBinary rejects msgpack-encoded malformed envelopes", {
timeout: 5000,
}, () => {
// Build a real msgpack buffer for { __t: 1, v: 1 } using @msgpack/msgpack
// directly — bypassing our encoder so the wire-schema rejection path
// can be exercised independently of JSON.parse.
// 0x82 = fixmap of 2; 0xa3 "__t" = fixstr; 0x01 = 1; 0xa1 "v" = fixstr; 0x01 = 1.
const bytes = new Uint8Array([
0x82, 0xa3, 0x5f, 0x5f, 0x74, 0x01, 0xa1, 0x76, 0x01,
]);
expect(() => decodeBinary(bytes)).toThrowError(MalformedWireError);
});
});
describe("SZ-10 — encode/decode error label fallback", () => {
it("encode error path tags non-Error throws as SERIALIZE.CODEC.UNKNOWN", {
timeout: 5000,
}, () => {
// Register a custom encode function that throws a non-Error value to
// exercise the `: "SERIALIZE.CODEC.UNKNOWN"` branch in both codecs.
const tag = "__SerializeEncodeBranchProbe";
const ctor = class EncodeBranchProbe {};
registerType({
tag,
ctor,
encode: () => {
throw "raw-string-throw";
},
decode: (v) => v,
});
try {
const probe = new ctor();
expect(() => serialize(probe)).toThrow();
expect(() => encodeBinary(probe)).toThrow();
} finally {
unregisterType(tag);
}
});
it("decode error path tags non-Error throws as SERIALIZE.CODEC.UNKNOWN", {
timeout: 5000,
}, () => {
// Register a custom decode function that throws a non-Error value to
// exercise the `: "SERIALIZE.CODEC.UNKNOWN"` branch on the decode side.
const tag = "__SerializeDecodeBranchProbe";
const ctor = class DecodeBranchProbe {};
registerType({
tag,
ctor,
encode: () => null,
decode: () => {
throw "raw-string-throw";
},
});
try {
const probe = new ctor();
const json = serialize(probe);
expect(() => deserialize(json)).toThrow();
const bytes = encodeBinary(probe);
expect(() => decodeBinary(bytes)).toThrow();
} finally {
unregisterType(tag);
}
});
});
describe("SZ-10 — generic threading", () => {
it("deserialize<T>() returns T at the type level", {
timeout: 5000,
}, () => {
const json = serialize({ name: "ada", age: 36 });
const result = deserialize<{ name: string; age: number }>(json);
expectTypeOf(result).toEqualTypeOf<{ name: string; age: number }>();
expect(result.name).toBe("ada");
});
it("decodeBinary<T>() returns T at the type level", {
timeout: 5000,
}, () => {
const bytes = encodeBinary([1, 2, 3]);
const result = decodeBinary<number[]>(bytes);
expectTypeOf(result).toEqualTypeOf<number[]>();
expect(result).toEqual([1, 2, 3]);
});
it("deserialize() defaults to unknown when T is omitted", {
timeout: 5000,
}, () => {
const result = deserialize(serialize({ a: 1 }));
expectTypeOf(result).toEqualTypeOf<unknown>();
});
});

View File

@@ -0,0 +1,74 @@
import z from "zod";
/**
* Zod schema describing the structural envelope of a tagged value in the
* serialize wire format: `{ "__t": "<tag>", "v": <encoded> }`.
*
* The `__t` field MUST be a non-empty string and the `v` field MUST be
* present (even if encoded as `null`). Extra keys are forbidden — a tagged
* envelope that smuggles in a third key is by definition malformed and is
* rejected at the public boundary.
*/
export const TaggedEnvelopeSchema = z
.object({
__t: z.string().min(1),
v: z.unknown(),
})
.strict();
/**
* Refinement applied to plain records: if a record has a `__t` key it MUST
* also conform to the strict envelope shape (string `__t`, present `v`,
* no extra keys). Otherwise the record is malformed wire input. Records
* without `__t` are passed through untouched — user data is allowed any
* string-keyed shape that JSON.parse / msgpack can produce.
*/
function recordIsWellFormed(record: Record<string, unknown>): boolean {
if (!("__t" in record)) {
return true;
}
return TaggedEnvelopeSchema.safeParse(record).success;
}
/**
* Zod schema describing one node of the serialize wire format. The shape is
* recursive: any node MAY be a JSON primitive, an array of nodes, a tagged
* envelope, or a plain string-keyed record of nodes (a record with a `__t`
* key MUST conform to the envelope shape — no extra keys, `__t` must be a
* non-empty string, `v` must be present).
*
* Inputs that contain JS-only constructs the wire format never produces
* (e.g. `undefined`, functions, symbols, raw BigInts) are rejected. The
* intent is to defend the deserialize / decodeBinary boundary against
* accidental or malicious inputs that would otherwise reach the type
* registry's decode functions in an unexpected shape.
*/
export const WireNodeSchema: z.ZodType<unknown> = z.lazy(() =>
z.union([
z.null(),
z.boolean(),
z.number(),
z.string(),
z.array(WireNodeSchema),
z.record(z.string(), WireNodeSchema).refine(recordIsWellFormed, {
message:
"record with __t key must conform to the tagged envelope schema (string __t, present v, no extra keys)",
}),
])
);
/**
* Validate a parsed-but-not-yet-decoded wire value against the structural
* invariants of the serialize wire format. Returns the input cast to
* `unknown` if valid; otherwise returns the underlying ZodError so the
* caller can wrap it in a `MalformedWireError` with stable code metadata.
*/
export function validateWireNode(
value: unknown
): { ok: true; value: unknown } | { ok: false; error: z.ZodError } {
const parsed = WireNodeSchema.safeParse(value);
if (parsed.success) {
return { ok: true, value: parsed.data };
}
return { ok: false, error: parsed.error };
}