import * as z from "zod" import { isPlainObject } from "./lib/utils" import { packr } from "./packr" import { bufferAsyncValues } from "./walk" export { isSensitivityMask, projectSensitivityMask, redactSensitiveValue, type SensitivityMask, type StructuralSensitivityMask, } from "./sensitivity" export type { ObjectProducingSchema, ProducingSchema } from "./schema" /** * Values accepted by the codec encoder, including recursively nested containers * and registered web-platform extension types. */ export type Encodable = // primitives | null | undefined | void | boolean | number | bigint | string // typed arrays | Uint8Array | Uint8ClampedArray | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | Float32Array | Float64Array | BigUint64Array | BigInt64Array // other types | Encodable[] | readonly Encodable[] | { readonly [key: string]: Encodable } | Map | Set | Date | RegExp | ArrayBuffer | DataView | Request | Response | Blob | File | Headers | URL | URLSearchParams /** * Asserts that a value belongs to the codec's supported input surface. * * @param value - Value to validate. * @throws {TypeError} When the value cannot be encoded. */ function assertEncodable(value: unknown): asserts value is Encodable { if (!isEncodable(value)) { throw new TypeError( `Value is not encodable: ${Object.prototype.toString.call(value)}`, ) } } /** * Checks whether an unknown value fits the codec's supported input surface. * * @param value - Value to inspect. */ export function isEncodable(value: unknown): value is Encodable { const activePath = new WeakSet() /** * Validates one value while tracking the active traversal path for cycles. * * @param value - Value at the current traversal position. */ function visit(value: unknown): value is Encodable { if (value == null) return true switch (typeof value) { case "boolean": case "bigint": case "number": case "string": return true case "function": case "symbol": return false case "object": break default: return false } if ( value instanceof Date || value instanceof RegExp || value instanceof ArrayBuffer || ArrayBuffer.isView(value) || value instanceof Request || value instanceof Response || value instanceof Blob || value instanceof File || value instanceof Headers || value instanceof URL || value instanceof URLSearchParams ) { return true } if (activePath.has(value)) return false activePath.add(value) try { return Array.isArray(value) ? value.every(visit) : value instanceof Map ? [...value].every(([key, item]) => visit(key) && visit(item)) : value instanceof Set ? [...value].every(visit) : isPlainObject(value) && Object.values(value).every(visit) } finally { activePath.delete(value) } } return visit(value) } /** Zod schema for values accepted by the codec encoder. */ export const encodableSchema = z .custom(isEncodable, { message: "Value is not encodable", }) .optional() /** * Serializes a value after checking it against the codec's supported input * surface. * * Pre-walks the value to buffer any async-bodied values (`Request`, `Response`) * before handing off to the synchronous packer. * * @param value - Value to validate and serialize. * @throws {TypeError} When the value is outside the supported input surface. */ export async function encode(value: unknown): Promise { assertEncodable(value) return packr.pack(await bufferAsyncValues(value)) } /** * Deserializes msgpack bytes. * * Currently synchronous under the hood, but typed as async to leave room for * future concerns that need await (e.g. resolving blob refs from external * storage, streaming decodes). * * Returns the codec's supported value surface. Callers should still validate * the domain-specific shape where they know the expected type. * * @param bytes - Msgpack bytes to deserialize. * @throws {TypeError} When the decoded payload is outside the supported * surface. */ export async function decode(bytes: Uint8Array): Promise { const value = packr.unpack(bytes) assertEncodable(value) return value }