From 4301a0097276083d6f4d6ec28faec29544cb150c Mon Sep 17 00:00:00 2001 From: David Antoon Date: Mon, 2 Nov 2026 17:27:35 +0200 Subject: [PATCH 1/5] perf(lazy-zod): add internalZ, which defers leaf schemas too internalZ defers every schema factory, leaves included. A stand-in builds its leaf children with it, so a built schema holds Zod's own leaves and parses on Zod's fast path, and it builds without Zod's cycle-check walk, which would otherwise build every stand-in under a container on its first parse. The public z is unchanged: its leaf factories are still Zod's own. Stand-ins share one Proxy handler and drop their factory once built. forceMaterialize also follows catchall, rest and pipe in/out. --- libs/lazy-zod/README.md | 21 +- .../lazy-zod/src/__tests__/internal-z.spec.ts | 389 ++++++++++++++++++ libs/lazy-zod/src/factories.ts | 35 ++ libs/lazy-zod/src/index.ts | 1 + libs/lazy-zod/src/internal-z.ts | 157 +++++++ libs/lazy-zod/src/lazy-schema.ts | 176 ++++---- libs/lazy-zod/src/lazy-z.ts | 21 +- libs/lazy-zod/src/utils.ts | 11 +- 8 files changed, 707 insertions(+), 104 deletions(-) create mode 100644 libs/lazy-zod/src/__tests__/internal-z.spec.ts create mode 100644 libs/lazy-zod/src/internal-z.ts diff --git a/libs/lazy-zod/README.md b/libs/lazy-zod/README.md index ed8fc1f33..adabae676 100644 --- a/libs/lazy-zod/README.md +++ b/libs/lazy-zod/README.md @@ -39,16 +39,17 @@ const Deferred = lazyZ(() => eagerZ.object({ id: eagerZ.string() })); A stand-in behaves like the schema it stands for: it parses the same, `instanceof ZodObject` is `true`, and `z.infer` gives the same type. -| Export | What it is | -| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `z` (also the default export) | Zod 4's `z`, with the factories above deferred. | -| `eagerZ` | Zod's own `z`. Use it for a schema that something reads as soon as it's defined. | -| `lazyZ(build)` | A schema that calls `build()` the first time it's used, and only then. | -| `isLazy(schema)` | `true` for a stand-in, from `z` or from `lazyZ()`. | -| `forceMaterialize(schema)` | The real Zod schema, with every stand-in inside it built. | -| `hasTransform(schema)` | `true` when the schema runs a transform anywhere in its tree (`.transform()`, `z.preprocess()`, `.pipe()`, a codec), so what it parses to can differ in shape from its input. | -| `toJSONSchema(schema)` | Zod's JSON Schema converter, run on `forceMaterialize(schema)`. Use it instead of Zod's own `z.toJSONSchema()`, which throws `TypeError: Cannot set properties of undefined (setting 'ref')` when an object or union in the schema is `.optional()` or has a `.default()`. | -| `ZodObject`, `ZodError`, … | Zod's classes and types (`ZodTypeAny`, `infer`, `input`, `output`), so your code needs no import from `zod`. | +| Export | What it is | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `z` (also the default export) | Zod 4's `z`, with the factories above deferred. | +| `eagerZ` | Zod's own `z`. Use it for a schema that something reads as soon as it's defined. | +| `lazyZ(build)` | A schema that calls `build()` the first time it's used, and only then. | +| `internalZ` | What FrontMCP's own packages build their schemas with. Every factory, leaves included, returns a stand-in, and a stand-in builds its leaf children with it. Zod's cycle check isn't attached to them, so a recursive schema is built with `z`. It isn't part of the public API and can change in any release: use `z`. | +| `isLazy(schema)` | `true` for a stand-in, from `z` or from `lazyZ()`. | +| `forceMaterialize(schema)` | The real Zod schema, with every stand-in inside it built. | +| `hasTransform(schema)` | `true` when the schema runs a transform anywhere in its tree (`.transform()`, `z.preprocess()`, `.pipe()`, a codec), so what it parses to can differ in shape from its input. | +| `toJSONSchema(schema)` | Zod's JSON Schema converter, run on `forceMaterialize(schema)`. Use it instead of Zod's own `z.toJSONSchema()`, which throws `TypeError: Cannot set properties of undefined (setting 'ref')` when an object or union in the schema is `.optional()` or has a `.default()`. | +| `ZodObject`, `ZodError`, … | Zod's classes and types (`ZodTypeAny`, `infer`, `input`, `output`), so your code needs no import from `zod`. | ## Performance diff --git a/libs/lazy-zod/src/__tests__/internal-z.spec.ts b/libs/lazy-zod/src/__tests__/internal-z.spec.ts new file mode 100644 index 000000000..e5a0bc976 --- /dev/null +++ b/libs/lazy-zod/src/__tests__/internal-z.spec.ts @@ -0,0 +1,389 @@ +import { z as realZ } from 'zod'; + +import { HEAVY_FACTORIES, LEAF_FACTORIES } from '../factories'; +import { internalZ, toJSONSchema, z } from '../index'; +import { LAZY_TARGET, type LazyZodSchema } from '../lazy-schema'; +import { isLazy } from '../utils'; + +type Namespace = typeof realZ; + +function targetOf(schema: unknown): LazyZodSchema { + return (schema as Record)[LAZY_TARGET]; +} + +function isBuilt(schema: unknown): boolean { + return targetOf(schema).isMaterialized; +} + +enum Color { + Red = 'red', + Blue = 'blue', +} + +class Widget { + constructor(readonly id: string) {} +} + +type FactoryCase = { + factory: string; + build: (Z: Namespace) => realZ.ZodType; + valid: unknown[]; + invalid: unknown[]; +}; + +const factoryCases: FactoryCase[] = [ + { factory: 'string', build: (Z) => Z.string().min(2), valid: ['ab'], invalid: ['a', 1, null] }, + { factory: 'number', build: (Z) => Z.number().positive(), valid: [1, 2.5], invalid: [0, '1', NaN] }, + { factory: 'int', build: (Z) => Z.int(), valid: [3], invalid: [1.5, '3'] }, + { factory: 'boolean', build: (Z) => Z.boolean(), valid: [true, false], invalid: ['true', 0] }, + { factory: 'bigint', build: (Z) => Z.bigint(), valid: [1n], invalid: [1] }, + { factory: 'symbol', build: (Z) => Z.symbol(), valid: [Symbol.for('a')], invalid: ['a'] }, + { factory: 'date', build: (Z) => Z.date(), valid: [new Date(0)], invalid: ['2020-01-01', new Date('x')] }, + { factory: 'undefined', build: (Z) => Z.undefined(), valid: [undefined], invalid: [null] }, + { factory: 'null', build: (Z) => Z.null(), valid: [null], invalid: [undefined] }, + { factory: 'void', build: (Z) => Z.void(), valid: [undefined], invalid: [0] }, + { factory: 'any', build: (Z) => Z.any(), valid: [1, 'x', null], invalid: [] }, + { factory: 'unknown', build: (Z) => Z.unknown(), valid: [1, undefined], invalid: [] }, + { factory: 'never', build: (Z) => Z.never(), valid: [], invalid: [1, undefined] }, + { factory: 'nan', build: (Z) => Z.nan(), valid: [NaN], invalid: [1] }, + { factory: 'literal', build: (Z) => Z.literal('on'), valid: ['on'], invalid: ['off'] }, + { factory: 'enum', build: (Z) => Z.enum(['a', 'b']), valid: ['a'], invalid: ['c', 1] }, + { factory: 'nativeEnum', build: (Z) => Z.nativeEnum(Color), valid: ['red'], invalid: ['green'] }, + { factory: 'url', build: (Z) => Z.url(), valid: ['https://example.com'], invalid: ['not a url'] }, + { factory: 'email', build: (Z) => Z.email(), valid: ['a@b.co'], invalid: ['a@'] }, + { factory: 'uuid', build: (Z) => Z.uuid(), valid: ['123e4567-e89b-42d3-a456-426614174000'], invalid: ['123'] }, + { factory: 'array', build: (Z) => Z.array(Z.string()).max(2), valid: [[], ['a']], invalid: [['a', 'b', 'c'], [1]] }, + { factory: 'set', build: (Z) => Z.set(Z.number()), valid: [new Set([1])], invalid: [new Set(['a']), [1]] }, + { + factory: 'map', + build: (Z) => Z.map(Z.string(), Z.number()), + valid: [new Map([['a', 1]])], + invalid: [new Map([['a', 'b']])], + }, + { + factory: 'partialRecord', + build: (Z) => Z.partialRecord(Z.enum(['a', 'b']), Z.number()), + valid: [{}, { a: 1 }], + invalid: [{ c: 1 }, { a: 'x' }], + }, + { factory: 'optional', build: (Z) => Z.optional(Z.string()), valid: [undefined, 'x'], invalid: [1] }, + { factory: 'nullable', build: (Z) => Z.nullable(Z.string()), valid: [null, 'x'], invalid: [undefined] }, + { factory: 'lazy', build: (Z) => Z.lazy(() => Z.string()), valid: ['x'], invalid: [1] }, + { factory: 'function', build: (Z) => Z.function(), valid: [() => 1], invalid: ['fn'] }, + { factory: 'custom', build: (Z) => Z.custom((v) => typeof v === 'string'), valid: ['x'], invalid: [1] }, + { factory: 'instanceof', build: (Z) => Z.instanceof(Widget), valid: [new Widget('w')], invalid: [{ id: 'w' }] }, + { + factory: 'preprocess', + build: (Z) => Z.preprocess((v) => (typeof v === 'string' ? Number(v) : v), Z.number()), + valid: ['2', 3], + invalid: ['x', null], + }, + { + factory: 'object', + build: (Z) => Z.object({ name: Z.string(), age: Z.number().int().optional() }), + valid: [{ name: 'a' }, { name: 'a', age: 2, extra: true }], + invalid: [{}, { name: 1 }, { name: 'a', age: 1.5 }, null], + }, + { + factory: 'strictObject', + build: (Z) => Z.strictObject({ name: Z.string() }), + valid: [{ name: 'a' }], + invalid: [{ name: 'a', extra: true }], + }, + { + factory: 'looseObject', + build: (Z) => Z.looseObject({ name: Z.string() }), + valid: [{ name: 'a', extra: true }], + invalid: [{ extra: true }], + }, + { + factory: 'union', + build: (Z) => Z.union([Z.string(), Z.object({ n: Z.number() })]), + valid: ['x', { n: 1 }], + invalid: [{ n: 'x' }, 1], + }, + { + factory: 'discriminatedUnion', + build: (Z) => + Z.discriminatedUnion('kind', [ + Z.object({ kind: Z.literal('a'), a: Z.string() }), + Z.object({ kind: Z.literal('b'), b: Z.number() }), + ]), + valid: [ + { kind: 'a', a: 'x' }, + { kind: 'b', b: 1 }, + ], + invalid: [{ kind: 'c' }, { kind: 'a', a: 1 }, {}], + }, + { + factory: 'intersection', + build: (Z) => Z.intersection(Z.object({ a: Z.string() }), Z.object({ b: Z.number() })), + valid: [{ a: 'x', b: 1 }], + invalid: [{ a: 'x' }, { b: 1 }], + }, + { + factory: 'record', + build: (Z) => Z.record(Z.string(), Z.array(Z.number())), + valid: [{}, { a: [1] }], + invalid: [{ a: ['x'] }, []], + }, + { + factory: 'tuple', + build: (Z) => Z.tuple([Z.string(), Z.number()], Z.boolean()), + valid: [ + ['a', 1], + ['a', 1, true], + ], + invalid: [['a'], ['a', 1, 'x']], + }, +]; + +type ChainCase = { name: string; build: (Z: Namespace) => realZ.ZodType; valid: unknown[]; invalid: unknown[] }; + +const chainCases: ChainCase[] = [ + { + name: 'string checks and formats', + build: (Z) => + Z.string() + .trim() + .min(1) + .max(5) + .regex(/^[a-z]+$/), + valid: [' abc '], + invalid: ['', 'abcdef', 'ABC'], + }, + { + name: 'number checks', + build: (Z) => Z.number().int().nonnegative().max(10), + valid: [0, 10], + invalid: [-1, 11, 1.2], + }, + { + name: 'default and describe', + build: (Z) => Z.string().default('d').describe('text'), + valid: [undefined, 'x'], + invalid: [1], + }, + { name: 'nullish', build: (Z) => Z.number().nullish(), valid: [null, undefined, 1], invalid: ['1'] }, + { name: 'refine', build: (Z) => Z.number().refine((n) => n % 2 === 0, 'must be even'), valid: [2], invalid: [3] }, + { + name: 'superRefine', + build: (Z) => + Z.string().superRefine((value, ctx) => { + if (value === 'bad') ctx.addIssue({ code: 'custom', message: 'no bad' }); + }), + valid: ['ok'], + invalid: ['bad'], + }, + { name: 'transform', build: (Z) => Z.string().transform((s) => s.length), valid: ['abc'], invalid: [1] }, + { name: 'pipe', build: (Z) => Z.string().transform(Number).pipe(Z.number().int()), valid: ['3'], invalid: ['1.5'] }, + { name: 'or / and', build: (Z) => Z.string().or(Z.number()).and(Z.any()), valid: ['x', 1], invalid: [true] }, + { name: 'array() and nonempty', build: (Z) => Z.string().array().nonempty(), valid: [['a']], invalid: [[]] }, + { name: 'catch', build: (Z) => Z.number().catch(0), valid: [1, 'x'], invalid: [] }, + { + name: 'object helpers', + build: (Z) => + Z.object({ a: Z.string(), b: Z.number(), c: Z.boolean() }) + .extend({ d: Z.string().optional() }) + .pick({ a: true, b: true, d: true }) + .omit({ b: true }) + .partial({ a: true }) + .strict(), + valid: [{}, { a: 'x', d: 'y' }], + invalid: [{ a: 1 }, { b: 1 }], + }, + { + name: 'catchall', + build: (Z) => Z.object({ a: Z.string() }).catchall(Z.number()), + valid: [{ a: 'x', n: 1 }], + invalid: [{ a: 'x', n: 'y' }], + }, + { + name: 'enum extract / exclude', + build: (Z) => Z.enum(['a', 'b', 'c']).exclude(['c']), + valid: ['a'], + invalid: ['c'], + }, + { + name: 'nested optional sections', + build: (Z) => + Z.object({ + info: Z.object({ name: Z.string().min(1), version: Z.string() }), + auth: Z.object({ mode: Z.enum(['public', 'remote']), issuer: Z.url().optional() }).optional(), + tags: Z.array(Z.string()).optional().default([]), + }), + valid: [{ info: { name: 'a', version: '1' } }, { info: { name: 'a', version: '1' }, auth: { mode: 'public' } }], + invalid: [{ info: { name: '', version: '1' } }, { info: { name: 'a', version: '1' }, auth: { mode: 'x' } }], + }, +]; + +function expectSameResult(internal: realZ.ZodType, eager: realZ.ZodType, input: unknown): void { + const expected = eager.safeParse(input); + const actual = internal.safeParse(input); + expect(actual.success).toBe(expected.success); + if (expected.success && typeof expected.data === 'function') { + expect(typeof actual.data).toBe('function'); + } else if (expected.success) { + expect(actual.data).toEqual(expected.data); + } else { + expect(actual.error?.issues).toEqual(expected.error.issues); + expect(actual.error?.message).toBe(expected.error.message); + } +} + +describe('public z', () => { + it('passes every leaf factory through as Zod’s own function', () => { + for (const name of LEAF_FACTORIES) { + expect((z as unknown as Record)[name]).toBe((realZ as unknown as Record)[name]); + } + }); + + it('returns Zod’s own schemas from leaf factories', () => { + const schema = z.string(); + expect(isLazy(schema)).toBe(false); + expect(schema).toBeInstanceOf(realZ.ZodString); + expect(isLazy(z.array(z.string()))).toBe(false); + }); + + it('defers only the compound factories', () => { + for (const name of HEAVY_FACTORIES) { + expect((z as unknown as Record)[name]).not.toBe( + (realZ as unknown as Record)[name], + ); + } + }); +}); + +describe('internalZ', () => { + it('has a parity case for every factory it defers', () => { + const covered = new Set(factoryCases.map((c) => c.factory)); + expect([...LEAF_FACTORIES, ...HEAVY_FACTORIES].filter((name) => !covered.has(name))).toEqual([]); + }); + + it('defers only factories Zod has', () => { + for (const name of [...LEAF_FACTORIES, ...HEAVY_FACTORIES]) { + expect(typeof (realZ as unknown as Record)[name]).toBe('function'); + } + }); + + describe.each(factoryCases)('z.$factory()', ({ build, valid, invalid }) => { + it('returns a stand-in that builds nothing until it is used', () => { + const schema = build(internalZ); + expect(isLazy(schema)).toBe(true); + expect(isBuilt(schema)).toBe(false); + }); + + it('accepts and rejects the same inputs as Zod, with the same issues', () => { + const internal = build(internalZ); + const eager = build(realZ); + for (const input of [...valid, ...invalid]) expectSameResult(internal, eager, input); + }); + + it('converts to the same JSON Schema', () => { + const eager = build(realZ); + let expected: unknown; + try { + expected = toJSONSchema(eager); + } catch (error) { + expect(() => toJSONSchema(build(internalZ))).toThrow((error as Error).message); + return; + } + expect(toJSONSchema(build(internalZ))).toEqual(expected); + }); + }); + + describe.each(chainCases)('$name', ({ build, valid, invalid }) => { + it('accepts and rejects the same inputs as Zod, with the same issues', () => { + const internal = build(internalZ); + const eager = build(realZ); + for (const input of [...valid, ...invalid]) expectSameResult(internal, eager, input); + }); + }); + + it('builds leaf children with their parent, so the built schema holds Zod’s own leaves', () => { + const name = internalZ.string().min(1); + const tags = internalZ.array(internalZ.string()); + const schema = internalZ.object({ name, tags }); + + expect(schema.parse({ name: 'a', tags: [] })).toEqual({ name: 'a', tags: [] }); + + expect(schema.shape.name).toBe(targetOf(name).materialize()); + expect(isLazy(schema.shape.name)).toBe(false); + expect(isLazy(schema.shape.tags)).toBe(false); + expect(schema.shape.tags.element).toBeInstanceOf(realZ.ZodString); + }); + + it('keeps compound children deferred until they are parsed', () => { + const deep = internalZ.object({ issuer: internalZ.string() }); + const auth = internalZ.object({ deep }); + const schema = internalZ.object({ name: internalZ.string(), auth: auth.optional() }); + + schema.parse({ name: 'a' }); + + expect(isBuilt(auth)).toBe(true); + expect(isBuilt(deep)).toBe(false); + + schema.parse({ name: 'a', auth: { deep: { issuer: 'x' } } }); + expect(isBuilt(deep)).toBe(true); + }); + + it('reuses a compound child that is already built', () => { + const shared = internalZ.object({ id: internalZ.string() }); + shared.parse({ id: 'x' }); + const schema = internalZ.object({ shared }); + + schema.parse({ shared: { id: 'y' } }); + + expect(schema.shape.shared).toBe(targetOf(shared).materialize()); + }); + + it('leaves Zod’s memoizer in place for every other schema', () => { + const before = realZ.config().memoizer; + internalZ.object({ a: internalZ.string() }).parse({ a: 'x' }); + expect(realZ.config().memoizer).toBe(before); + + type Node = { next?: Node }; + const node: realZ.ZodType = realZ.lazy(() => realZ.object({ next: node.optional() })); + const cyclic: Node = {}; + cyclic.next = cyclic; + const parsed = node.parse(cyclic); + expect(parsed.next).toBe(parsed); + }); + + it('restores Zod’s memoizer when a build throws', () => { + const before = realZ.config().memoizer; + const broken = internalZ + .object({ a: internalZ.string() }) + .refine(() => true) + .pick({ a: true }); + expect(() => broken.parse({ a: 'x' })).toThrow('.pick() cannot be used on object schemas containing refinements'); + expect(realZ.config().memoizer).toBe(before); + }); + + it('answers introspection like the schema it stands for', () => { + const base = internalZ.object({ kind: internalZ.literal('a'), size: internalZ.number().optional() }); + const union = internalZ.discriminatedUnion('kind', [base, internalZ.object({ kind: internalZ.literal('b') })]); + const extended = base.extend({ label: internalZ.string().describe('Label') }); + + expect(base).toBeInstanceOf(realZ.ZodObject); + expect(internalZ.string()).toBeInstanceOf(realZ.ZodString); + expect(internalZ.enum(['x', 'y']).options).toEqual(['x', 'y']); + expect(Object.keys(base.shape)).toEqual(['kind', 'size']); + expect(base._def.type).toBe('object'); + expect(base.shape.size.isOptional()).toBe(true); + expect(extended.shape.label.description).toBe('Label'); + expect(union.parse({ kind: 'b' })).toEqual({ kind: 'b' }); + expect(realZ.fromJSONSchema(toJSONSchema(extended) as never).parse({ kind: 'a', label: 'l' })).toEqual({ + kind: 'a', + label: 'l', + }); + }); + + it('carries the same types as z', () => { + const schema = internalZ.object({ a: internalZ.string(), b: internalZ.number().optional() }); + type Parsed = internalZ.infer; + const value: Parsed = { a: 'x' }; + const typed: internalZ.ZodType = schema; + expect(typed.parse(value)).toEqual({ a: 'x' }); + }); +}); diff --git a/libs/lazy-zod/src/factories.ts b/libs/lazy-zod/src/factories.ts index 2d14115aa..0200a3002 100644 --- a/libs/lazy-zod/src/factories.ts +++ b/libs/lazy-zod/src/factories.ts @@ -22,6 +22,41 @@ export const HEAVY_FACTORIES: ReadonlySet = new Set([ 'tuple', ]); +/** The other schema factories, which `internalZ` defers too. The public `z` leaves them as Zod's own. */ +export const LEAF_FACTORIES: ReadonlySet = new Set([ + 'string', + 'number', + 'int', + 'boolean', + 'bigint', + 'symbol', + 'date', + 'undefined', + 'null', + 'void', + 'any', + 'unknown', + 'never', + 'nan', + 'literal', + 'enum', + 'nativeEnum', + 'url', + 'email', + 'uuid', + 'array', + 'set', + 'map', + 'partialRecord', + 'optional', + 'nullable', + 'lazy', + 'function', + 'custom', + 'instanceof', + 'preprocess', +]); + export const CHAIN_METHODS: ReadonlySet = new Set([ // ZodType generic 'optional', diff --git a/libs/lazy-zod/src/index.ts b/libs/lazy-zod/src/index.ts index a306e63af..c146207e0 100644 --- a/libs/lazy-zod/src/index.ts +++ b/libs/lazy-zod/src/index.ts @@ -20,6 +20,7 @@ export { z } from './lazy-z'; // cascades into `any.ZodObject` etc. and breaks decorator inference. export { z as default } from './lazy-z'; export { eagerZ } from './eager-z'; +export { internalZ } from './internal-z'; export { lazyZ, LazyZodSchema, LAZY_BRAND, LAZY_TARGET, type InferLazy } from './lazy-schema'; export { isLazy, forceMaterialize, hasTransform } from './utils'; diff --git a/libs/lazy-zod/src/internal-z.ts b/libs/lazy-zod/src/internal-z.ts new file mode 100644 index 000000000..3d7916cd6 --- /dev/null +++ b/libs/lazy-zod/src/internal-z.ts @@ -0,0 +1,157 @@ +import { z as realZ, type ZodTypeAny } from 'zod'; +import type { $ZodMemoizer } from 'zod/v4/core'; + +import { HEAVY_FACTORIES, LEAF_FACTORIES } from './factories'; +import { LAZY_TARGET, LazyZodSchema, wrapLazy } from './lazy-schema'; +import { createLazyZ, type StandInFactory, type z } from './lazy-z'; +import { isLazy } from './utils'; + +const BUILT_WITH_PARENT: unique symbol = Symbol('builtWithParent'); + +const zodConfig = realZ.config(); +const defaultMemoizer = (realZ as { memoizer?: () => $ZodMemoizer }).memoizer; +const walklessMemoizers = new WeakMap<$ZodMemoizer, $ZodMemoizer>(); + +/** Zod's memoizer walks a container's whole subtree on its first parse, which would build every stand-in in it. */ +function withoutRecursionWalk(build: () => T): T { + const saved = zodConfig.memoizer; + const memoizer = saved ?? defaultMemoizer?.(); + if (!memoizer) return build(); + let walkless = walklessMemoizers.get(memoizer); + if (!walkless) { + walkless = Object.create(memoizer, { attach: { value: () => undefined } }) as $ZodMemoizer; + walklessMemoizers.set(memoizer, walkless); + } + zodConfig.memoizer = walkless; + try { + return build(); + } finally { + zodConfig.memoizer = saved; + } +} + +class InternalLazySchema extends LazyZodSchema { + readonly [BUILT_WITH_PARENT]: boolean; + + constructor(factory: () => ZodTypeAny, builtWithParent: boolean) { + super(factory); + this[BUILT_WITH_PARENT] = builtWithParent; + } + + protected override build(factory: () => ZodTypeAny): ZodTypeAny { + return withoutRecursionWalk(factory); + } + + override chain(method: string, args: unknown[]): unknown { + return internalStandIn( + () => { + const real = this.materialize() as unknown as Record ZodTypeAny>; + return real[method](...args.map(withBuiltChildren)); + }, + this[BUILT_WITH_PARENT] && args.every(holdsOnlyLeaves), + ); + } +} + +function internalStandIn(build: () => ZodTypeAny, builtWithParent: boolean): unknown { + return wrapLazy(new InternalLazySchema(build, builtWithParent)); +} + +function targetOf(value: unknown): LazyZodSchema | undefined { + return isLazy(value) ? (value as Record)[LAZY_TARGET] : undefined; +} + +function isLeaf(value: unknown): boolean { + const target = targetOf(value); + return target === undefined || (target instanceof InternalLazySchema && target[BUILT_WITH_PARENT]); +} + +function builtChild(value: unknown): unknown { + const target = targetOf(value); + return target && (target.isMaterialized || isLeaf(value)) ? target.materialize() : value; +} + +/** A plain object of data properties: a getter can be a recursive reference, so it is never read. */ +function isShape(value: unknown): value is object { + if (value === null || typeof value !== 'object' || isLazy(value) || Array.isArray(value)) return false; + const prototype = Object.getPrototypeOf(value); + if (prototype !== Object.prototype && prototype !== null) return false; + return Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => 'value' in descriptor); +} + +function childrenOf(arg: unknown): unknown[] { + if (Array.isArray(arg)) return arg; + if (isShape(arg)) return Reflect.ownKeys(arg).map((key) => (arg as Record)[key]); + return [arg]; +} + +function holdsOnlyLeaves(arg: unknown): boolean { + return childrenOf(arg).every(isLeaf); +} + +function withBuiltChildren(arg: unknown): unknown { + if (Array.isArray(arg)) { + const built = arg.map(builtChild); + return built.some((child, index) => child !== arg[index]) ? built : arg; + } + if (!isShape(arg)) return builtChild(arg); + const descriptors = Object.getOwnPropertyDescriptors(arg) as Record; + let changed = false; + for (const key of Reflect.ownKeys(descriptors)) { + const built = builtChild(descriptors[key].value); + if (built !== descriptors[key].value) { + descriptors[key] = { ...descriptors[key], value: built }; + changed = true; + } + } + return changed ? Object.create(Object.getPrototypeOf(arg), descriptors) : arg; +} + +const deferEveryFactory: StandInFactory = (name, args, build) => + internalStandIn(() => build(args.map(withBuiltChildren)), LEAF_FACTORIES.has(name) && args.every(holdsOnlyLeaves)); + +/** FrontMCP's own `z`: every factory returns a stand-in that builds its leaf children with it. Not public API. */ +export const internalZ: typeof realZ = createLazyZ( + realZ, + new Set([...HEAVY_FACTORIES, ...LEAF_FACTORIES]), + deferEveryFactory, +); + +/* eslint-disable @typescript-eslint/no-namespace -- the same aliases as z's */ +export namespace internalZ { + export type infer = z.infer; + export type output = z.output; + export type input = z.input; + export type ZodType = z.ZodType; + export type ZodTypeAny = z.ZodTypeAny; + export type ZodRawShape = z.ZodRawShape; + export type ZodObject = z.ZodObject; + export type ZodArray = z.ZodArray; + export type ZodUnion = z.ZodUnion; + export type ZodDiscriminatedUnion = z.ZodDiscriminatedUnion; + export type ZodIntersection< + L extends realZ.ZodTypeAny = realZ.ZodTypeAny, + R extends realZ.ZodTypeAny = realZ.ZodTypeAny, + > = z.ZodIntersection; + export type ZodTuple = z.ZodTuple; + export type ZodRecord = z.ZodRecord; + export type ZodString = z.ZodString; + export type ZodNumber = z.ZodNumber; + export type ZodBoolean = z.ZodBoolean; + export type ZodBigInt = z.ZodBigInt; + export type ZodDate = z.ZodDate; + export type ZodSymbol = z.ZodSymbol; + export type ZodEnum = z.ZodEnum; + export type ZodLiteral = z.ZodLiteral; + export type ZodNull = z.ZodNull; + export type ZodUndefined = z.ZodUndefined; + export type ZodVoid = z.ZodVoid; + export type ZodAny = z.ZodAny; + export type ZodUnknown = z.ZodUnknown; + export type ZodNever = z.ZodNever; + export type ZodOptional = z.ZodOptional; + export type ZodNullable = z.ZodNullable; + export type ZodDefault = z.ZodDefault; + export type ZodError = z.ZodError; +} +/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/libs/lazy-zod/src/lazy-schema.ts b/libs/lazy-zod/src/lazy-schema.ts index 26138d719..1055ec9c6 100644 --- a/libs/lazy-zod/src/lazy-schema.ts +++ b/libs/lazy-zod/src/lazy-schema.ts @@ -24,8 +24,10 @@ const HOT_PATH_METHODS = new Set(['parse', 'safeParse', 'parseAsync', 'safeParse */ export class LazyZodSchema { private _resolved: ZodTypeAny | undefined; + private _materialize: (() => ZodTypeAny) | undefined; - constructor(private readonly _materialize: () => ZodTypeAny) { + constructor(materialize: () => ZodTypeAny) { + this._materialize = materialize; Object.defineProperty(this, LAZY_BRAND, { value: true, enumerable: false, @@ -34,15 +36,34 @@ export class LazyZodSchema { }); } - /** Materialize (and memoize) the underlying real zod schema. */ + /** Materialize (and memoize) the underlying real zod schema, then drop the factory and what it captured. */ materialize(): ZodTypeAny { - return this._resolved ?? (this._resolved = this._materialize()); + const factory = this._materialize; + if (factory) { + this._resolved = this.build(factory); + this._materialize = undefined; + } + return this._resolved as ZodTypeAny; + } + + protected build(factory: () => ZodTypeAny): ZodTypeAny { + return factory(); } /** True once the underlying schema has been constructed. */ get isMaterialized(): boolean { return this._resolved !== undefined; } + + /** The stand-in for `materialize()[method](...args)`. */ + chain(method: string, args: unknown[]): unknown { + return wrapLazy( + new LazyZodSchema(() => { + const real = this.materialize() as unknown as Record ZodTypeAny>; + return real[method](...args); + }), + ); + } } /** @@ -58,85 +79,80 @@ export class LazyZodSchema { * - `getPrototypeOf` forwards to the materialized schema so `instanceof` * works (materializes on first check). */ -export function wrapLazy(schema: LazyZodSchema): unknown { - const proxy: unknown = new Proxy(schema, { - get(target, key, _receiver) { - // Brand check — zero cost, avoids materializing during `isLazy` - if (key === LAZY_BRAND) return true; - // Escape hatch — return the underlying LazyZodSchema instance - if (key === LAZY_TARGET) return target; - - // Own-prop fast path — self-patched hot-path methods land here on - // every call after the first. No function-creation, no materialize. - if (typeof key !== 'symbol' && Object.prototype.hasOwnProperty.call(target, key)) { - return (target as unknown as Record)[key as string]; - } - - // Hot path — materialize, bind, self-patch, call. - // Forward ALL arguments (zod's `parse` / `safeParse` accept optional - // params objects like error maps); taking only `data` would drop them - // on the cold path and make cold/warm calls behave differently. - if (typeof key === 'string' && HOT_PATH_METHODS.has(key)) { - return (...args: unknown[]) => { - const real = target.materialize(); - const bound = (real as unknown as Record unknown>)[key].bind(real); - Object.defineProperty(target, key, { - value: bound, - writable: true, - configurable: true, - enumerable: false, - }); - return bound(...args); - }; +const lazyHandler: ProxyHandler = { + get(target, key, _receiver) { + // Brand check — zero cost, avoids materializing during `isLazy` + if (key === LAZY_BRAND) return true; + // Escape hatch — return the underlying LazyZodSchema instance + if (key === LAZY_TARGET) return target; + + // Own-prop fast path — self-patched hot-path methods land here on + // every call after the first. No function-creation, no materialize. + if (typeof key !== 'symbol' && Object.prototype.hasOwnProperty.call(target, key)) { + return (target as unknown as Record)[key as string]; + } + + // Hot path — materialize, bind, self-patch, call. + // Forward ALL arguments (zod's `parse` / `safeParse` accept optional + // params objects like error maps); taking only `data` would drop them + // on the cold path and make cold/warm calls behave differently. + if (typeof key === 'string' && HOT_PATH_METHODS.has(key)) { + return (...args: unknown[]) => { + const real = target.materialize(); + const bound = (real as unknown as Record unknown>)[key].bind(real); + Object.defineProperty(target, key, { + value: bound, + writable: true, + configurable: true, + enumerable: false, + }); + return bound(...args); + }; + } + + // Chainable — capture into a new lazy + if (typeof key === 'string' && CHAIN_METHODS.has(key)) { + return (...args: unknown[]) => target.chain(key, args); + } + + // Anything else — materialize and forward. Bind methods so callers + // using `schema.method(...)` get the right `this`, but NOT constructors + // or classes (binding turns them into "bound X" which breaks + // `.constructor.name` identity checks downstream). `key === 'constructor'` + // is the common case we have to protect; a broader heuristic covers + // any function whose `prototype` has own properties (i.e. a class). + const real = target.materialize() as unknown as Record; + const val = real[key as PropertyKey]; + if (typeof val === 'function') { + if (key === 'constructor') return val; + const maybeCtor = val as { prototype?: object }; + if ( + typeof maybeCtor.prototype === 'object' && + maybeCtor.prototype !== null && + Object.getOwnPropertyNames(maybeCtor.prototype).length > 1 + ) { + return val; } + return (val as (...a: unknown[]) => unknown).bind(real); + } + return val; + }, + + has(target, key) { + if (key === LAZY_BRAND) return true; + if (typeof key === 'string' && (HOT_PATH_METHODS.has(key) || CHAIN_METHODS.has(key))) { + return true; + } + return Reflect.has(target.materialize(), key); + }, + + getPrototypeOf(target) { + return Object.getPrototypeOf(target.materialize()); + }, +}; - // Chainable — capture into a new lazy - if (typeof key === 'string' && CHAIN_METHODS.has(key)) { - return (...args: unknown[]) => - wrapLazy( - new LazyZodSchema(() => { - const real = target.materialize() as unknown as Record ZodTypeAny>; - return real[key](...args); - }), - ); - } - - // Anything else — materialize and forward. Bind methods so callers - // using `schema.method(...)` get the right `this`, but NOT constructors - // or classes (binding turns them into "bound X" which breaks - // `.constructor.name` identity checks downstream). `key === 'constructor'` - // is the common case we have to protect; a broader heuristic covers - // any function whose `prototype` has own properties (i.e. a class). - const real = target.materialize() as unknown as Record; - const val = real[key as PropertyKey]; - if (typeof val === 'function') { - if (key === 'constructor') return val; - const maybeCtor = val as { prototype?: object }; - if ( - typeof maybeCtor.prototype === 'object' && - maybeCtor.prototype !== null && - Object.getOwnPropertyNames(maybeCtor.prototype).length > 1 - ) { - return val; - } - return (val as (...a: unknown[]) => unknown).bind(real); - } - return val; - }, - - has(target, key) { - if (key === LAZY_BRAND) return true; - if (typeof key === 'string' && (HOT_PATH_METHODS.has(key) || CHAIN_METHODS.has(key))) { - return true; - } - return Reflect.has(target.materialize(), key); - }, - - getPrototypeOf(target) { - return Object.getPrototypeOf(target.materialize()); - }, - }); - return proxy; +export function wrapLazy(schema: LazyZodSchema): unknown { + return new Proxy(schema, lazyHandler); } /** diff --git a/libs/lazy-zod/src/lazy-z.ts b/libs/lazy-zod/src/lazy-z.ts index a56147930..9268737bd 100644 --- a/libs/lazy-zod/src/lazy-z.ts +++ b/libs/lazy-zod/src/lazy-z.ts @@ -1,4 +1,4 @@ -import { z as realZ } from 'zod'; +import { z as realZ, type ZodTypeAny } from 'zod'; import { HEAVY_FACTORIES } from './factories'; import { LazyZodSchema, wrapLazy } from './lazy-schema'; @@ -19,15 +19,20 @@ import { LazyZodSchema, wrapLazy } from './lazy-schema'; * target owns nothing, so no invariant applies; every trap forwards to * `namespace` instead. Exported for tests — the public entry point is `z`. */ -export function createLazyZ(namespace: typeof realZ): typeof realZ { +export type StandInFactory = (name: string, args: unknown[], build: (args: unknown[]) => ZodTypeAny) => unknown; + +const compoundStandIn: StandInFactory = (_name, args, build) => wrapLazy(new LazyZodSchema(() => build(args))); + +export function createLazyZ( + namespace: typeof realZ, + factories: ReadonlySet = HEAVY_FACTORIES, + standIn: StandInFactory = compoundStandIn, +): typeof realZ { return new Proxy({} as typeof realZ, { get(_target, key, receiver) { - if (typeof key === 'string' && HEAVY_FACTORIES.has(key)) { - const realFactory = (namespace as unknown as Record unknown>)[key]; - return (...args: unknown[]) => - wrapLazy( - new LazyZodSchema(() => realFactory.call(namespace, ...args) as ReturnType as never), - ); + if (typeof key === 'string' && factories.has(key)) { + const realFactory = (namespace as unknown as Record ZodTypeAny>)[key]; + return (...args: unknown[]) => standIn(key, args, (built) => realFactory.apply(namespace, built)); } return Reflect.get(namespace, key, receiver); }, diff --git a/libs/lazy-zod/src/utils.ts b/libs/lazy-zod/src/utils.ts index f14d3d091..4c5c4ad09 100644 --- a/libs/lazy-zod/src/utils.ts +++ b/libs/lazy-zod/src/utils.ts @@ -37,6 +37,8 @@ function nestedSchemas(field: unknown): unknown[] { return [field]; } +const CHILD_KEYS = ['innerType', 'element', 'valueType', 'keyType', 'left', 'right', 'catchall', 'rest', 'in', 'out']; + /** * Materialize a lazy schema and return the underlying real zod schema, * recursively materializing any lazy wrappers found in the schema tree @@ -82,12 +84,9 @@ function deepMaterialize(value: unknown, seen: WeakSet): unknown { if (Array.isArray(options)) { for (let i = 0; i < options.length; i++) options[i] = deepMaterialize(options[i], seen); } - if ('innerType' in def) def['innerType'] = deepMaterialize(def['innerType'], seen); - if ('element' in def) def['element'] = deepMaterialize(def['element'], seen); - if ('valueType' in def) def['valueType'] = deepMaterialize(def['valueType'], seen); - if ('keyType' in def) def['keyType'] = deepMaterialize(def['keyType'], seen); - if ('left' in def) def['left'] = deepMaterialize(def['left'], seen); - if ('right' in def) def['right'] = deepMaterialize(def['right'], seen); + for (const key of CHILD_KEYS) { + if (key in def) def[key] = deepMaterialize(def[key], seen); + } const items = def['items']; if (Array.isArray(items)) { for (let i = 0; i < items.length; i++) items[i] = deepMaterialize(items[i], seen); From 91b50ad90aa6e12eb1c518cb1398999a0a609ea3 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Mon, 2 Nov 2026 17:28:15 +0200 Subject: [PATCH 2/5] perf(sdk,auth,utils): build FrontMCP's own schemas with internalZ The schemas in sdk, auth and utils now come from internalZ, so a server builds a schema the first time it is used, leaves included. The recursive authorities policy schema stays on z, which keeps Zod's cycle support. A parity spec loads the three packages twice, once as shipped and once with internalZ replaced by z, and checks that every exported schema accepts and rejects the same inputs with the same issues and messages. --- .../authorities.schema.cycles.spec.ts | 14 ++ .../src/authorization/authorization.types.ts | 2 +- libs/auth/src/cimd/cimd.types.ts | 2 +- libs/auth/src/common/jwt.types.ts | 2 +- libs/auth/src/common/session.types.ts | 2 +- libs/auth/src/consent/consent.types.ts | 2 +- .../src/detection/auth-provider-detection.ts | 2 +- libs/auth/src/options/app-auth.schema.ts | 2 +- libs/auth/src/options/orchestrated.schema.ts | 2 +- libs/auth/src/options/public.schema.ts | 2 +- libs/auth/src/options/schema.ts | 2 +- libs/auth/src/options/shared.schemas.ts | 2 +- libs/auth/src/options/static.schema.ts | 2 +- libs/auth/src/options/transparent.schema.ts | 2 +- libs/auth/src/session/authorization-vault.ts | 2 +- libs/auth/src/session/authorization.store.ts | 2 +- .../session/encrypted-authorization-vault.ts | 2 +- .../src/session/transport-session.types.ts | 2 +- libs/auth/src/vault/auth-providers.types.ts | 2 +- .../internal-z-schema-parity.spec.ts | 170 ++++++++++++++++++ libs/sdk/src/agent/agent-builtin-tools.ts | 2 +- libs/sdk/src/agent/agent.instance.ts | 2 +- libs/sdk/src/agent/flows/call-agent.flow.ts | 2 +- libs/sdk/src/agent/index.ts | 2 +- libs/sdk/src/auth/flows/auth.verify.flow.ts | 2 +- libs/sdk/src/auth/flows/oauth.auth-ui.flow.ts | 2 +- .../src/auth/flows/oauth.authorize.flow.ts | 2 +- .../sdk/src/auth/flows/oauth.callback.flow.ts | 2 +- libs/sdk/src/auth/flows/oauth.connect.flow.ts | 2 +- .../flows/oauth.provider-callback.flow.ts | 2 +- .../sdk/src/auth/flows/oauth.register.flow.ts | 2 +- libs/sdk/src/auth/flows/oauth.token.flow.ts | 2 +- .../sdk/src/auth/flows/oauth.userinfo.flow.ts | 2 +- .../sdk/src/auth/flows/session.verify.flow.ts | 2 +- .../src/auth/flows/well-known.jwks.flow.ts | 2 +- ...l-known.oauth-authorization-server.flow.ts | 2 +- .../sdk/src/auth/flows/well-known.prm.flow.ts | 2 +- libs/sdk/src/builtin/config/config.plugin.ts | 2 +- libs/sdk/src/builtin/config/index.ts | 2 +- .../src/channel/flows/list-channels.flow.ts | 2 +- .../flows/send-channel-notification.flow.ts | 2 +- libs/sdk/src/channel/reply/reply.types.ts | 2 +- libs/sdk/src/common/common.schema.ts | 2 +- libs/sdk/src/common/entries/job.entry.ts | 2 +- libs/sdk/src/common/entries/tool.entry.ts | 2 +- libs/sdk/src/common/flow/flow.utils.ts | 2 +- .../src/common/metadata/adapter.metadata.ts | 2 +- .../sdk/src/common/metadata/agent.metadata.ts | 2 +- .../common/metadata/app-filter.metadata.ts | 2 +- libs/sdk/src/common/metadata/app.metadata.ts | 2 +- .../common/metadata/auth-provider.metadata.ts | 2 +- .../src/common/metadata/channel.metadata.ts | 2 +- libs/sdk/src/common/metadata/flow.metadata.ts | 2 +- .../src/common/metadata/front-mcp.metadata.ts | 2 +- libs/sdk/src/common/metadata/job.metadata.ts | 2 +- .../src/common/metadata/logger.metadata.ts | 2 +- libs/sdk/src/common/metadata/output-policy.ts | 2 +- .../src/common/metadata/plugin.metadata.ts | 2 +- .../src/common/metadata/prompt.metadata.ts | 2 +- .../src/common/metadata/provider.metadata.ts | 2 +- .../src/common/metadata/resource.metadata.ts | 2 +- .../sdk/src/common/metadata/skill.metadata.ts | 2 +- libs/sdk/src/common/metadata/tool.metadata.ts | 2 +- .../src/common/metadata/ui-serving-mode.ts | 2 +- .../src/common/metadata/workflow.metadata.ts | 2 +- .../common/schemas/annotated-class.schema.ts | 2 +- .../src/common/schemas/http-input.schema.ts | 2 +- .../src/common/schemas/http-output.schema.ts | 2 +- .../common/schemas/session-header.schema.ts | 2 +- .../src/common/types/auth/session.types.ts | 2 +- .../types/options/elicitation/schema.ts | 2 +- .../common/types/options/ext-apps/schema.ts | 2 +- .../src/common/types/options/health/schema.ts | 2 +- .../src/common/types/options/http/schema.ts | 2 +- .../common/types/options/logging/schema.ts | 2 +- .../common/types/options/metrics/schema.ts | 2 +- .../types/options/observability/schema.ts | 2 +- .../types/options/outbound-fetch/schema.ts | 2 +- .../common/types/options/pagination/schema.ts | 2 +- .../src/common/types/options/redis/schema.ts | 2 +- .../common/types/options/registry/schema.ts | 2 +- .../types/options/server-info/schema.ts | 2 +- .../common/types/options/session/schema.ts | 2 +- .../types/options/skills-http/schema.ts | 2 +- .../src/common/types/options/sqlite/schema.ts | 2 +- .../common/types/options/transport/schema.ts | 2 +- .../utils/decide-request-intent.utils.ts | 2 +- .../sdk/src/completion/flows/complete.flow.ts | 2 +- .../flows/elicitation-request.flow.ts | 2 +- .../flows/elicitation-result.flow.ts | 2 +- .../src/elicitation/helpers/elicit.helper.ts | 2 +- .../helpers/validate-elicitation-content.ts | 2 +- .../send-elicitation-result.tool.ts | 2 +- .../errors/authorization-required.error.ts | 2 +- libs/sdk/src/esm-loader/esm-auth.types.ts | 2 +- libs/sdk/src/esm-loader/esm-manifest.ts | 2 +- .../factories/esm-record-builders.ts | 2 +- libs/sdk/src/job/flows/execute-job.flow.ts | 2 +- libs/sdk/src/job/job.instance.ts | 2 +- libs/sdk/src/job/tools/execute-job.tool.ts | 2 +- libs/sdk/src/job/tools/get-job-status.tool.ts | 2 +- libs/sdk/src/job/tools/list-jobs.tool.ts | 2 +- libs/sdk/src/job/tools/register-job.tool.ts | 2 +- libs/sdk/src/job/tools/remove-job.tool.ts | 2 +- libs/sdk/src/logging/flows/set-level.flow.ts | 2 +- .../mcp-registry/flows/server-card.flow.ts | 2 +- .../flows/well-known.ai-catalog.flow.ts | 2 +- libs/sdk/src/prompt/flows/get-prompt.flow.ts | 2 +- .../sdk/src/prompt/flows/prompts-list.flow.ts | 2 +- .../src/resource/flows/read-resource.flow.ts | 2 +- .../flows/resource-templates-list.flow.ts | 2 +- .../src/resource/flows/resources-list.flow.ts | 2 +- .../resource/flows/subscribe-resource.flow.ts | 2 +- .../flows/unsubscribe-resource.flow.ts | 2 +- libs/sdk/src/scope/flows/http.request.flow.ts | 2 +- .../sdk/src/skill/flows/filter-skills.flow.ts | 2 +- .../src/skill/flows/http/llm-full-txt.flow.ts | 2 +- libs/sdk/src/skill/flows/http/llm-txt.flow.ts | 2 +- .../src/skill/flows/http/skills-api.flow.ts | 2 +- libs/sdk/src/skill/flows/load-skill.flow.ts | 2 +- .../sdk/src/skill/flows/search-skills.flow.ts | 2 +- libs/sdk/src/task/flows/tasks-cancel.flow.ts | 2 +- libs/sdk/src/task/flows/tasks-get.flow.ts | 2 +- libs/sdk/src/task/flows/tasks-list.flow.ts | 2 +- libs/sdk/src/task/flows/tasks-result.flow.ts | 2 +- libs/sdk/src/tool/flows/call-tool.flow.ts | 2 +- libs/sdk/src/tool/flows/tools-list.flow.ts | 2 +- libs/sdk/src/tool/tool.instance.ts | 2 +- libs/sdk/src/tool/tool.utils.ts | 2 +- .../flows/handle.mcp-20260728.flow.ts | 2 +- .../src/transport/flows/handle.sse.flow.ts | 2 +- .../flows/handle.stateless-http.flow.ts | 2 +- .../flows/handle.streamable-http.flow.ts | 2 +- .../src/transport/mcp-20260728/dispatcher.ts | 2 +- .../mcp-handlers/skills-mcp.types.ts | 2 +- .../workflow/tools/execute-workflow.tool.ts | 2 +- .../tools/get-workflow-status.tool.ts | 2 +- .../src/workflow/tools/list-workflows.tool.ts | 2 +- .../workflow/tools/register-workflow.tool.ts | 2 +- .../workflow/tools/remove-workflow.tool.ts | 2 +- .../src/crypto/key-persistence/schemas.ts | 2 +- .../src/crypto/secret-persistence/schema.ts | 2 +- libs/utils/src/env/browser-runtime-context.ts | 2 +- libs/utils/src/env/runtime-context.ts | 2 +- 144 files changed, 326 insertions(+), 142 deletions(-) create mode 100644 libs/auth/src/authorities/__tests__/authorities.schema.cycles.spec.ts create mode 100644 libs/sdk/src/__tests__/internal-z-schema-parity.spec.ts diff --git a/libs/auth/src/authorities/__tests__/authorities.schema.cycles.spec.ts b/libs/auth/src/authorities/__tests__/authorities.schema.cycles.spec.ts new file mode 100644 index 000000000..61859ce0f --- /dev/null +++ b/libs/auth/src/authorities/__tests__/authorities.schema.cycles.spec.ts @@ -0,0 +1,14 @@ +import { authoritiesPolicySchema } from '../authorities.schema'; + +describe('authoritiesPolicySchema with a reference cycle', () => { + it('accepts a policy that refers to itself, keeping the cycle in the result', () => { + const policy: Record = { roles: { any: ['admin'] } }; + policy['anyOf'] = [policy]; + + const result = authoritiesPolicySchema.safeParse(policy); + + expect(result.success).toBe(true); + const parsed = result.data as Record; + expect(parsed['anyOf'][0]).toBe(parsed); + }); +}); diff --git a/libs/auth/src/authorization/authorization.types.ts b/libs/auth/src/authorization/authorization.types.ts index 29a8293f3..dc7567f76 100644 --- a/libs/auth/src/authorization/authorization.types.ts +++ b/libs/auth/src/authorization/authorization.types.ts @@ -5,7 +5,7 @@ * These types are portable and can be used across different implementations. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { ProviderSnapshot } from '../session/session.types'; import type { TransportProtocol, TransportSession } from '../session/transport-session.types'; diff --git a/libs/auth/src/cimd/cimd.types.ts b/libs/auth/src/cimd/cimd.types.ts index 0d02fdb33..f5c8643bb 100644 --- a/libs/auth/src/cimd/cimd.types.ts +++ b/libs/auth/src/cimd/cimd.types.ts @@ -6,7 +6,7 @@ * * @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00 */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; // ============================================ // CLIENT METADATA DOCUMENT SCHEMA diff --git a/libs/auth/src/common/jwt.types.ts b/libs/auth/src/common/jwt.types.ts index bb2e6ace6..08896a728 100644 --- a/libs/auth/src/common/jwt.types.ts +++ b/libs/auth/src/common/jwt.types.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type RawZodShape } from './zod-utils'; diff --git a/libs/auth/src/common/session.types.ts b/libs/auth/src/common/session.types.ts index cae8c763c..11bb16970 100644 --- a/libs/auth/src/common/session.types.ts +++ b/libs/auth/src/common/session.types.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type RawZodShape } from './zod-utils'; diff --git a/libs/auth/src/consent/consent.types.ts b/libs/auth/src/consent/consent.types.ts index 603f10735..811f925d8 100644 --- a/libs/auth/src/consent/consent.types.ts +++ b/libs/auth/src/consent/consent.types.ts @@ -4,7 +4,7 @@ * Defines types for the tool consent flow that allows users to select * which MCP tools they want to expose to the LLM. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { consentConfigSchema } from '../options/shared.schemas'; diff --git a/libs/auth/src/detection/auth-provider-detection.ts b/libs/auth/src/detection/auth-provider-detection.ts index e667c79ca..5f14c062b 100644 --- a/libs/auth/src/detection/auth-provider-detection.ts +++ b/libs/auth/src/detection/auth-provider-detection.ts @@ -4,7 +4,7 @@ * Detects unique auth providers across nested apps and determines * if orchestrated mode is required at the parent scope level. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { AuthOptions } from '../options/schema'; import { staticAuthOptionsSchema } from '../options/static.schema'; diff --git a/libs/auth/src/options/app-auth.schema.ts b/libs/auth/src/options/app-auth.schema.ts index 81842eaf0..485700503 100644 --- a/libs/auth/src/options/app-auth.schema.ts +++ b/libs/auth/src/options/app-auth.schema.ts @@ -1,7 +1,7 @@ // options/app-auth.schema.ts // App-level auth options with standalone option -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type RawZodShape } from '../common/zod-utils'; import { localAuthSchema, remoteAuthSchema } from './orchestrated.schema'; diff --git a/libs/auth/src/options/orchestrated.schema.ts b/libs/auth/src/options/orchestrated.schema.ts index 4872f09f9..de55c4fb4 100644 --- a/libs/auth/src/options/orchestrated.schema.ts +++ b/libs/auth/src/options/orchestrated.schema.ts @@ -1,7 +1,7 @@ // options/orchestrated.schema.ts // Local and Remote auth modes (formerly orchestrated local/remote) -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../common/zod-utils'; import type { diff --git a/libs/auth/src/options/public.schema.ts b/libs/auth/src/options/public.schema.ts index 78705d856..1862df597 100644 --- a/libs/auth/src/options/public.schema.ts +++ b/libs/auth/src/options/public.schema.ts @@ -1,7 +1,7 @@ // options/public.schema.ts // Public mode - No authentication required -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { jsonWebKeySetSchema, jwkSchema } from '../common/jwt.types'; import type { RawZodShape } from '../common/zod-utils'; diff --git a/libs/auth/src/options/schema.ts b/libs/auth/src/options/schema.ts index 7cb33ac57..5a9e81e7f 100644 --- a/libs/auth/src/options/schema.ts +++ b/libs/auth/src/options/schema.ts @@ -1,7 +1,7 @@ // options/schema.ts // Unified auth options schema combining all modes -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { AuthOptionsInterface } from './interfaces'; import { localAuthSchema, remoteAuthSchema } from './orchestrated.schema'; diff --git a/libs/auth/src/options/shared.schemas.ts b/libs/auth/src/options/shared.schemas.ts index d46f70a97..2d85dd1b1 100644 --- a/libs/auth/src/options/shared.schemas.ts +++ b/libs/auth/src/options/shared.schemas.ts @@ -1,7 +1,7 @@ // options/shared.schemas.ts // Shared configuration schemas used across auth modes -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { jsonWebKeySetSchema, jwkSchema } from '../common/jwt.types'; import { redisConfigSchema, type RedisConfig } from '../session/transport-session.types'; diff --git a/libs/auth/src/options/static.schema.ts b/libs/auth/src/options/static.schema.ts index 7c0009cb8..5ffcb3af4 100644 --- a/libs/auth/src/options/static.schema.ts +++ b/libs/auth/src/options/static.schema.ts @@ -1,7 +1,7 @@ // options/static.schema.ts // Static mode — a fixed shared secret presented on every request. -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../common/zod-utils'; import type { StaticAuthOptionsInterface } from './interfaces'; diff --git a/libs/auth/src/options/transparent.schema.ts b/libs/auth/src/options/transparent.schema.ts index ffdc5191f..06b7542b6 100644 --- a/libs/auth/src/options/transparent.schema.ts +++ b/libs/auth/src/options/transparent.schema.ts @@ -1,7 +1,7 @@ // options/transparent.schema.ts // Transparent mode - Pass-through OAuth tokens from remote provider -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../common/zod-utils'; import type { TransparentAuthOptionsInterface } from './interfaces'; diff --git a/libs/auth/src/session/authorization-vault.ts b/libs/auth/src/session/authorization-vault.ts index 45bf615cd..d57a9eaa9 100644 --- a/libs/auth/src/session/authorization-vault.ts +++ b/libs/auth/src/session/authorization-vault.ts @@ -21,7 +21,7 @@ * - No incremental authorization support */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; // randomUUID can be imported from @frontmcp/utils if needed diff --git a/libs/auth/src/session/authorization.store.ts b/libs/auth/src/session/authorization.store.ts index f35c87c09..abecc34ff 100644 --- a/libs/auth/src/session/authorization.store.ts +++ b/libs/auth/src/session/authorization.store.ts @@ -6,7 +6,7 @@ * Supports both in-memory (dev/test) and Redis (production) backends. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { randomUUID, sha256Base64url } from '@frontmcp/utils'; /** diff --git a/libs/auth/src/session/encrypted-authorization-vault.ts b/libs/auth/src/session/encrypted-authorization-vault.ts index c4ff9c00f..abb622ffe 100644 --- a/libs/auth/src/session/encrypted-authorization-vault.ts +++ b/libs/auth/src/session/encrypted-authorization-vault.ts @@ -23,7 +23,7 @@ * ``` */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { AsyncLocalStorage, randomUUID } from '@frontmcp/utils'; import { EncryptionContextNotSetError, VaultLoadError, VaultNotFoundError } from '../errors/auth-internal.errors'; diff --git a/libs/auth/src/session/transport-session.types.ts b/libs/auth/src/session/transport-session.types.ts index 7fae19942..21b1d65bf 100644 --- a/libs/auth/src/session/transport-session.types.ts +++ b/libs/auth/src/session/transport-session.types.ts @@ -1,6 +1,6 @@ // session/transport-session.types.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; /** * Transport protocol types supported by MCP diff --git a/libs/auth/src/vault/auth-providers.types.ts b/libs/auth/src/vault/auth-providers.types.ts index c7c4bcb91..e9efce614 100644 --- a/libs/auth/src/vault/auth-providers.types.ts +++ b/libs/auth/src/vault/auth-providers.types.ts @@ -5,7 +5,7 @@ * These are portable types that can be used across different implementations. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { AuthorizationVault, Credential } from '../session'; diff --git a/libs/sdk/src/__tests__/internal-z-schema-parity.spec.ts b/libs/sdk/src/__tests__/internal-z-schema-parity.spec.ts new file mode 100644 index 000000000..58d932987 --- /dev/null +++ b/libs/sdk/src/__tests__/internal-z-schema-parity.spec.ts @@ -0,0 +1,170 @@ +/** + * Every schema exported by `@frontmcp/utils`, `@frontmcp/auth` and `@frontmcp/sdk` validates the same inputs, with + * the same issues and messages, whether its module builds it with `internalZ` or with the public `z`. + */ +import 'reflect-metadata'; + +import { isLazy } from '@frontmcp/lazy-zod'; + +type Exports = Record; +type Def = Record & { type: string }; +type ParseResult = { success: boolean; data?: unknown; error?: { issues: unknown[]; message: string } }; +type Parser = { + safeParse(input: unknown): ParseResult; + safeParseAsync(input: unknown): Promise; + _zod: { def: Def; innerType?: unknown }; + format?: string | null; + minLength?: number | null; +}; + +const ENTRY_POINTS = ['@frontmcp/utils', '@frontmcp/auth', '../index']; + +function loadEntryPoints(): Record { + return Object.fromEntries(ENTRY_POINTS.map((entry) => [entry, require(entry) as Exports])); +} + +let current: Record = {}; +jest.isolateModules(() => { + current = loadEntryPoints(); +}); + +let previous: Record = {}; +jest.isolateModules(() => { + jest.doMock('@frontmcp/lazy-zod', () => { + const actual = jest.requireActual('@frontmcp/lazy-zod'); + return { ...actual, internalZ: actual.z }; + }); + previous = loadEntryPoints(); +}); + +function isSchema(value: unknown): value is Parser { + return isLazy(value) || (value !== null && typeof value === 'object' && '_zod' in value); +} + +const STRING_FORMATS: Record = { + url: 'https://example.com/path', + email: 'user@example.com', + uuid: '123e4567-e89b-42d3-a456-426614174000', + datetime: '2020-01-01T00:00:00Z', +}; + +function sample(schema: unknown, depth: number): unknown { + if (depth > 6 || !isSchema(schema)) return undefined; + const parser = schema as Parser; + const def = parser._zod.def; + const child = (key: string): unknown => sample(def[key], depth + 1); + switch (def.type) { + case 'string': + return STRING_FORMATS[parser.format ?? ''] ?? 'sample'.padEnd(parser.minLength ?? 0, 'x'); + case 'number': + case 'int': + return 1; + case 'boolean': + return true; + case 'bigint': + return 1n; + case 'date': + return new Date(0); + case 'null': + return null; + case 'any': + case 'unknown': + return 'value'; + case 'literal': + return (def['values'] as unknown[])[0]; + case 'enum': + return Object.values(def['entries'] as object)[0]; + case 'array': + return [child('element')]; + case 'object': + return Object.fromEntries( + Object.entries(def['shape'] as Record).map(([key, value]) => [key, sample(value, depth + 1)]), + ); + case 'record': + return { key: child('valueType') }; + case 'tuple': + return (def['items'] as unknown[]).map((item) => sample(item, depth + 1)); + case 'union': + return sample((def['options'] as unknown[])[0], depth + 1); + case 'intersection': + return { ...(child('left') as object), ...(child('right') as object) }; + case 'optional': + case 'nullable': + case 'default': + case 'prefault': + case 'catch': + case 'readonly': + case 'nonoptional': + return child('innerType'); + case 'pipe': + return child('in'); + case 'lazy': + return sample(parser._zod.innerType, depth + 1); + case 'function': + return () => undefined; + default: + return undefined; + } +} + +function inputsFor(schema: Parser): unknown[] { + const base = sample(schema, 0); + const inputs: unknown[] = [base, undefined, null, 'text', 42, true, {}, []]; + if (base !== null && typeof base === 'object' && !Array.isArray(base)) { + for (const [key, value] of Object.entries(base)) { + const { [key]: _removed, ...rest } = base as Record; + inputs.push(rest, { ...base, [key]: typeof value === 'string' ? 42 : 'wrong' }); + } + } + return inputs; +} + +function comparable(value: unknown, seen = new WeakSet()): unknown { + if (typeof value === 'function') return '[function]'; + if (value === null || typeof value !== 'object' || seen.has(value)) return value; + seen.add(value); + if (Array.isArray(value)) return value.map((item) => comparable(item, seen)); + if (Object.getPrototypeOf(value) !== Object.prototype) return value; + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, comparable(item, seen)])); +} + +function describeResult(result: ParseResult): unknown { + if (result.success) return { success: true, data: comparable(result.data) }; + return { success: false, issues: comparable(result.error?.issues), message: result.error?.message }; +} + +async function outcome(schema: Parser, input: unknown): Promise { + try { + return describeResult(schema.safeParse(input)); + } catch (error) { + if ((error as Error).message.includes('parseAsync')) return describeResult(await schema.safeParseAsync(input)); + return { threw: (error as Error).message }; + } +} + +const cases = ENTRY_POINTS.flatMap((entry) => + Object.keys(previous[entry]) + .filter((name) => isSchema(previous[entry][name])) + .map((name) => ({ entry, name })), +); + +describe('exported schemas built with internalZ', () => { + it('finds the exported schemas of every entry point', () => { + for (const entry of ENTRY_POINTS) expect(cases.some((c) => c.entry === entry)).toBe(true); + }); + + it('compares the schemas internalZ builds with the ones the public z builds', () => { + const deferred = (loaded: Record) => + cases.filter(({ entry, name }) => isLazy(loaded[entry][name])).length; + expect(deferred(current)).toBeGreaterThan(deferred(previous)); + }); + + it.each(cases)('$entry $name validates the same inputs as before', async ({ entry, name }) => { + const before = previous[entry][name] as Parser; + const after = current[entry][name] as Parser; + expect(isSchema(after)).toBe(true); + for (const input of inputsFor(before)) { + expect(await outcome(after, input)).toEqual(await outcome(before, input)); + } + }); +}); diff --git a/libs/sdk/src/agent/agent-builtin-tools.ts b/libs/sdk/src/agent/agent-builtin-tools.ts index 05d9c1a2b..47acc9ea3 100644 --- a/libs/sdk/src/agent/agent-builtin-tools.ts +++ b/libs/sdk/src/agent/agent-builtin-tools.ts @@ -10,7 +10,7 @@ * and errors that apply to a client's call apply to the model's too. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type GetPromptResult, type ListPromptsResult, diff --git a/libs/sdk/src/agent/agent.instance.ts b/libs/sdk/src/agent/agent.instance.ts index 36ceba2ba..9f21c7b1f 100644 --- a/libs/sdk/src/agent/agent.instance.ts +++ b/libs/sdk/src/agent/agent.instance.ts @@ -2,7 +2,7 @@ import { AuthorityDeniedError, type AuthoritiesMetadata } from '@frontmcp/auth'; import { type Token } from '@frontmcp/di'; -import { toJSONSchema, z } from '@frontmcp/lazy-zod'; +import { toJSONSchema, internalZ as z } from '@frontmcp/lazy-zod'; import { type CallToolRequest, type CallToolResult, type TextContent, type Tool } from '@frontmcp/protocol'; import { ConfigService } from '../builtin/config'; diff --git a/libs/sdk/src/agent/flows/call-agent.flow.ts b/libs/sdk/src/agent/flows/call-agent.flow.ts index 4d07c0756..e35188470 100644 --- a/libs/sdk/src/agent/flows/call-agent.flow.ts +++ b/libs/sdk/src/agent/flows/call-agent.flow.ts @@ -2,7 +2,7 @@ import { AuthorityDeniedError, resolveRequiredScopes } from '@frontmcp/auth'; import { ExecutionTimeoutError, withTimeout, type SemaphoreTicket } from '@frontmcp/guard'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { CallToolRequestSchema, CallToolResultSchema, type AuthInfo } from '@frontmcp/protocol'; import { completionEventsOf, completionOutputText } from '../../channel/sources/completion-events'; diff --git a/libs/sdk/src/agent/index.ts b/libs/sdk/src/agent/index.ts index 171ef2089..c0bd76f96 100644 --- a/libs/sdk/src/agent/index.ts +++ b/libs/sdk/src/agent/index.ts @@ -7,7 +7,7 @@ * @example Creating an agent * ```typescript * import { Agent, AgentContext } from '@frontmcp/sdk'; - * import { z } from '@frontmcp/lazy-zod'; + * import { internalZ as z } from '@frontmcp/lazy-zod'; * * @Agent({ * name: 'research-agent', diff --git a/libs/sdk/src/auth/flows/auth.verify.flow.ts b/libs/sdk/src/auth/flows/auth.verify.flow.ts index 9a0b96487..e8b12286c 100644 --- a/libs/sdk/src/auth/flows/auth.verify.flow.ts +++ b/libs/sdk/src/auth/flows/auth.verify.flow.ts @@ -42,7 +42,7 @@ import { type TransparentVerifiedPayload, type VerifyResult, } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { LocalPrimaryAuth } from '../instances/instance.local-primary-auth'; diff --git a/libs/sdk/src/auth/flows/oauth.auth-ui.flow.ts b/libs/sdk/src/auth/flows/oauth.auth-ui.flow.ts index f96856503..c29bea2cc 100644 --- a/libs/sdk/src/auth/flows/oauth.auth-ui.flow.ts +++ b/libs/sdk/src/auth/flows/oauth.auth-ui.flow.ts @@ -21,7 +21,7 @@ * * @packageDocumentation */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { enforceGlobalRateLimit, diff --git a/libs/sdk/src/auth/flows/oauth.authorize.flow.ts b/libs/sdk/src/auth/flows/oauth.authorize.flow.ts index 36416de7c..2afc17395 100644 --- a/libs/sdk/src/auth/flows/oauth.authorize.flow.ts +++ b/libs/sdk/src/auth/flows/oauth.authorize.flow.ts @@ -37,7 +37,7 @@ import { type ProviderCard, type ProviderPkce, } from '@frontmcp/auth'; -import { z, type ZodError } from '@frontmcp/lazy-zod'; +import { internalZ as z, type ZodError } from '@frontmcp/lazy-zod'; import { generateCodeVerifier, randomUUID, sha256Base64url } from '@frontmcp/utils'; import { diff --git a/libs/sdk/src/auth/flows/oauth.callback.flow.ts b/libs/sdk/src/auth/flows/oauth.callback.flow.ts index 3ce178650..53bfc73ab 100644 --- a/libs/sdk/src/auth/flows/oauth.callback.flow.ts +++ b/libs/sdk/src/auth/flows/oauth.callback.flow.ts @@ -29,7 +29,7 @@ import { type PendingLoginState, type ProviderPkce, } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { base64urlEncode, generateCodeVerifier, diff --git a/libs/sdk/src/auth/flows/oauth.connect.flow.ts b/libs/sdk/src/auth/flows/oauth.connect.flow.ts index ab3fda4e0..3b59a4d27 100644 --- a/libs/sdk/src/auth/flows/oauth.connect.flow.ts +++ b/libs/sdk/src/auth/flows/oauth.connect.flow.ts @@ -30,7 +30,7 @@ import { type LoginConfig, type LoginFieldConfig, } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { enforceGlobalRateLimit, diff --git a/libs/sdk/src/auth/flows/oauth.provider-callback.flow.ts b/libs/sdk/src/auth/flows/oauth.provider-callback.flow.ts index c6fc0bde7..176c85d71 100644 --- a/libs/sdk/src/auth/flows/oauth.provider-callback.flow.ts +++ b/libs/sdk/src/auth/flows/oauth.provider-callback.flow.ts @@ -32,7 +32,7 @@ import { type ProviderTokens, type ProviderUserInfo, } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { base64urlEncode, generateCodeVerifier, diff --git a/libs/sdk/src/auth/flows/oauth.register.flow.ts b/libs/sdk/src/auth/flows/oauth.register.flow.ts index ab53607bd..1736ba407 100644 --- a/libs/sdk/src/auth/flows/oauth.register.flow.ts +++ b/libs/sdk/src/auth/flows/oauth.register.flow.ts @@ -32,7 +32,7 @@ */ import { isLoopbackRedirectUri, type RegisteredClient } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { base64urlEncode, randomBytes, randomUUID } from '@frontmcp/utils'; import { diff --git a/libs/sdk/src/auth/flows/oauth.token.flow.ts b/libs/sdk/src/auth/flows/oauth.token.flow.ts index 03c505cdd..57a5d1433 100644 --- a/libs/sdk/src/auth/flows/oauth.token.flow.ts +++ b/libs/sdk/src/auth/flows/oauth.token.flow.ts @@ -56,7 +56,7 @@ * That's it. No pages with complex consent screens are required; reuse your normal /oauth/authorize */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { randomUUID } from '@frontmcp/utils'; import { diff --git a/libs/sdk/src/auth/flows/oauth.userinfo.flow.ts b/libs/sdk/src/auth/flows/oauth.userinfo.flow.ts index ee634fe81..0791dd152 100644 --- a/libs/sdk/src/auth/flows/oauth.userinfo.flow.ts +++ b/libs/sdk/src/auth/flows/oauth.userinfo.flow.ts @@ -18,7 +18,7 @@ import 'reflect-metadata'; import { extractBearerToken } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { enforceGlobalRateLimit, diff --git a/libs/sdk/src/auth/flows/session.verify.flow.ts b/libs/sdk/src/auth/flows/session.verify.flow.ts index b311178e4..49a120b33 100644 --- a/libs/sdk/src/auth/flows/session.verify.flow.ts +++ b/libs/sdk/src/auth/flows/session.verify.flow.ts @@ -40,7 +40,7 @@ import { type ProviderVerifyRef, type VerifyResult, } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { getMachineId, randomUUID, sha256, sha256Hex, timingSafeEqual } from '@frontmcp/utils'; import { detectPlatformFromUserAgent } from '../../notification/notification.service'; diff --git a/libs/sdk/src/auth/flows/well-known.jwks.flow.ts b/libs/sdk/src/auth/flows/well-known.jwks.flow.ts index bb8b3d39b..f631fbd41 100644 --- a/libs/sdk/src/auth/flows/well-known.jwks.flow.ts +++ b/libs/sdk/src/auth/flows/well-known.jwks.flow.ts @@ -21,7 +21,7 @@ import { import 'reflect-metadata'; import { JwksService } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { OrchestratorJwksNotAvailableError } from '../../errors/auth-internal.errors'; diff --git a/libs/sdk/src/auth/flows/well-known.oauth-authorization-server.flow.ts b/libs/sdk/src/auth/flows/well-known.oauth-authorization-server.flow.ts index 47420daba..d5c2b0879 100644 --- a/libs/sdk/src/auth/flows/well-known.oauth-authorization-server.flow.ts +++ b/libs/sdk/src/auth/flows/well-known.oauth-authorization-server.flow.ts @@ -2,7 +2,7 @@ import 'reflect-metadata'; import { advertisedScopes, trimSlash } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { isProduction } from '@frontmcp/utils'; import { diff --git a/libs/sdk/src/auth/flows/well-known.prm.flow.ts b/libs/sdk/src/auth/flows/well-known.prm.flow.ts index a6a7f0f53..8781f83c3 100644 --- a/libs/sdk/src/auth/flows/well-known.prm.flow.ts +++ b/libs/sdk/src/auth/flows/well-known.prm.flow.ts @@ -2,7 +2,7 @@ import 'reflect-metadata'; import { resourceScopesFor, type ResourceScopeOptions } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { computeResource, diff --git a/libs/sdk/src/builtin/config/config.plugin.ts b/libs/sdk/src/builtin/config/config.plugin.ts index cc30f1a76..67e678c3f 100644 --- a/libs/sdk/src/builtin/config/config.plugin.ts +++ b/libs/sdk/src/builtin/config/config.plugin.ts @@ -27,7 +27,7 @@ import { loadEnvFiles, populateProcessEnv, processEnvEntries } from './providers * class MyServer {} * * // With typed schema (convict-style) - * import { z } from '@frontmcp/lazy-zod'; + * import { internalZ as z } from '@frontmcp/lazy-zod'; * * const configSchema = z.object({ * database: z.object({ diff --git a/libs/sdk/src/builtin/config/index.ts b/libs/sdk/src/builtin/config/index.ts index b7729eda7..18cf5fb47 100644 --- a/libs/sdk/src/builtin/config/index.ts +++ b/libs/sdk/src/builtin/config/index.ts @@ -11,7 +11,7 @@ import { ConfigService } from './providers/config.service'; * @example * ```typescript * import { FrontMcp, App, ConfigPlugin } from '@frontmcp/sdk'; - * import { z } from '@frontmcp/lazy-zod'; + * import { internalZ as z } from '@frontmcp/lazy-zod'; * * // Define configuration schema * const configSchema = z.object({ diff --git a/libs/sdk/src/channel/flows/list-channels.flow.ts b/libs/sdk/src/channel/flows/list-channels.flow.ts index 8677a61fb..68e23974e 100644 --- a/libs/sdk/src/channel/flows/list-channels.flow.ts +++ b/libs/sdk/src/channel/flows/list-channels.flow.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/channel/flows/list-channels.flow.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; diff --git a/libs/sdk/src/channel/flows/send-channel-notification.flow.ts b/libs/sdk/src/channel/flows/send-channel-notification.flow.ts index 9bfce10d7..431e17372 100644 --- a/libs/sdk/src/channel/flows/send-channel-notification.flow.ts +++ b/libs/sdk/src/channel/flows/send-channel-notification.flow.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/channel/flows/send-channel-notification.flow.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; import { InvalidInputError } from '../../errors'; diff --git a/libs/sdk/src/channel/reply/reply.types.ts b/libs/sdk/src/channel/reply/reply.types.ts index b19f1aa65..b44da21da 100644 --- a/libs/sdk/src/channel/reply/reply.types.ts +++ b/libs/sdk/src/channel/reply/reply.types.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/channel/reply/reply.types.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; /** * Input schema for the channel reply tool. diff --git a/libs/sdk/src/common/common.schema.ts b/libs/sdk/src/common/common.schema.ts index 1e8526f2e..4c2947fd0 100644 --- a/libs/sdk/src/common/common.schema.ts +++ b/libs/sdk/src/common/common.schema.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type ServerResponse } from '../common'; diff --git a/libs/sdk/src/common/entries/job.entry.ts b/libs/sdk/src/common/entries/job.entry.ts index f76c0ee0e..652ef440b 100644 --- a/libs/sdk/src/common/entries/job.entry.ts +++ b/libs/sdk/src/common/entries/job.entry.ts @@ -1,4 +1,4 @@ -import { toJSONSchema, z } from '@frontmcp/lazy-zod'; +import { toJSONSchema, internalZ as z } from '@frontmcp/lazy-zod'; import type ProviderRegistry from '../../provider/provider.registry'; import { type ToolInputOf, type ToolOutputOf } from '../decorators'; diff --git a/libs/sdk/src/common/entries/tool.entry.ts b/libs/sdk/src/common/entries/tool.entry.ts index c9e2e99fc..ca51dfdc8 100644 --- a/libs/sdk/src/common/entries/tool.entry.ts +++ b/libs/sdk/src/common/entries/tool.entry.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/common/entries/tool.entry.ts -import { toJSONSchema, z } from '@frontmcp/lazy-zod'; +import { toJSONSchema, internalZ as z } from '@frontmcp/lazy-zod'; import { type AuthInfo, type CallToolRequest, diff --git a/libs/sdk/src/common/flow/flow.utils.ts b/libs/sdk/src/common/flow/flow.utils.ts index ae1220d29..c60edbce1 100644 --- a/libs/sdk/src/common/flow/flow.utils.ts +++ b/libs/sdk/src/common/flow/flow.utils.ts @@ -1,7 +1,7 @@ // file: libs/sdk/src/common/flow/flow.utils.ts // Shared utilities for list flows (tools/list, resources/list, etc.) -import { z, type ZodType } from '@frontmcp/lazy-zod'; +import { internalZ as z, type ZodType } from '@frontmcp/lazy-zod'; import { InvalidInputError, InvalidMethodError } from '../../errors'; diff --git a/libs/sdk/src/common/metadata/adapter.metadata.ts b/libs/sdk/src/common/metadata/adapter.metadata.ts index cd8388906..974290241 100644 --- a/libs/sdk/src/common/metadata/adapter.metadata.ts +++ b/libs/sdk/src/common/metadata/adapter.metadata.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type RawZodShape } from '../types'; diff --git a/libs/sdk/src/common/metadata/agent.metadata.ts b/libs/sdk/src/common/metadata/agent.metadata.ts index dffb0e556..ce1dd96d1 100644 --- a/libs/sdk/src/common/metadata/agent.metadata.ts +++ b/libs/sdk/src/common/metadata/agent.metadata.ts @@ -7,7 +7,7 @@ import { type RateLimitConfig, type TimeoutConfig, } from '@frontmcp/guard'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { entryAvailabilitySchema, type EntryAvailability } from '@frontmcp/utils'; import { type AdapterType } from '../interfaces/adapter.interface'; diff --git a/libs/sdk/src/common/metadata/app-filter.metadata.ts b/libs/sdk/src/common/metadata/app-filter.metadata.ts index 6180ac226..44411a21f 100644 --- a/libs/sdk/src/common/metadata/app-filter.metadata.ts +++ b/libs/sdk/src/common/metadata/app-filter.metadata.ts @@ -4,7 +4,7 @@ * loaded from remote or ESM-based apps. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; /** * Glob-style name patterns per primitive type. diff --git a/libs/sdk/src/common/metadata/app.metadata.ts b/libs/sdk/src/common/metadata/app.metadata.ts index 77e50f97c..c10098361 100644 --- a/libs/sdk/src/common/metadata/app.metadata.ts +++ b/libs/sdk/src/common/metadata/app.metadata.ts @@ -1,5 +1,5 @@ import { authOptionsSchema } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { AuthInfo } from '@frontmcp/protocol'; import { isValidMcpUri } from '@frontmcp/utils'; diff --git a/libs/sdk/src/common/metadata/auth-provider.metadata.ts b/libs/sdk/src/common/metadata/auth-provider.metadata.ts index 7f6c4f6bc..3ff22fa17 100644 --- a/libs/sdk/src/common/metadata/auth-provider.metadata.ts +++ b/libs/sdk/src/common/metadata/auth-provider.metadata.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type RawZodShape } from '../types'; diff --git a/libs/sdk/src/common/metadata/channel.metadata.ts b/libs/sdk/src/common/metadata/channel.metadata.ts index 68de6ae6c..546b1ba99 100644 --- a/libs/sdk/src/common/metadata/channel.metadata.ts +++ b/libs/sdk/src/common/metadata/channel.metadata.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { entryAvailabilitySchema, type EntryAvailability } from '@frontmcp/utils'; import { type ToolType } from '../interfaces/tool.interface'; diff --git a/libs/sdk/src/common/metadata/flow.metadata.ts b/libs/sdk/src/common/metadata/flow.metadata.ts index 3a372c153..688ce7444 100644 --- a/libs/sdk/src/common/metadata/flow.metadata.ts +++ b/libs/sdk/src/common/metadata/flow.metadata.ts @@ -1,5 +1,5 @@ import { type Token } from '@frontmcp/di'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type ScopeEntry } from '../entries'; import { type HttpMethod, type ServerRequest } from '../interfaces'; diff --git a/libs/sdk/src/common/metadata/front-mcp.metadata.ts b/libs/sdk/src/common/metadata/front-mcp.metadata.ts index 623278b36..c4f578de4 100644 --- a/libs/sdk/src/common/metadata/front-mcp.metadata.ts +++ b/libs/sdk/src/common/metadata/front-mcp.metadata.ts @@ -1,5 +1,5 @@ import { guardConfigSchema, type GuardConfig } from '@frontmcp/guard'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { MAX_CONCURRENT_TASKS_PER_SESSION } from '../../task/task.types'; import type { AdapterType, AppType, PluginType, ProviderType, ResourceType, SkillType, ToolType } from '../interfaces'; diff --git a/libs/sdk/src/common/metadata/job.metadata.ts b/libs/sdk/src/common/metadata/job.metadata.ts index c5e7ef737..e7c7c6922 100644 --- a/libs/sdk/src/common/metadata/job.metadata.ts +++ b/libs/sdk/src/common/metadata/job.metadata.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type RawZodShape } from '../types'; import { type ToolInputType, type ToolOutputType } from './tool.metadata'; diff --git a/libs/sdk/src/common/metadata/logger.metadata.ts b/libs/sdk/src/common/metadata/logger.metadata.ts index fdf808494..2124ca6df 100644 --- a/libs/sdk/src/common/metadata/logger.metadata.ts +++ b/libs/sdk/src/common/metadata/logger.metadata.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type RawZodShape } from '../types'; diff --git a/libs/sdk/src/common/metadata/output-policy.ts b/libs/sdk/src/common/metadata/output-policy.ts index 7c50b0947..852a269ad 100644 --- a/libs/sdk/src/common/metadata/output-policy.ts +++ b/libs/sdk/src/common/metadata/output-policy.ts @@ -1,5 +1,5 @@ // file: libs/sdk/src/common/metadata/output-policy.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { isDevelopment } from '@frontmcp/utils'; /** diff --git a/libs/sdk/src/common/metadata/plugin.metadata.ts b/libs/sdk/src/common/metadata/plugin.metadata.ts index 27fb0e960..2257016ad 100644 --- a/libs/sdk/src/common/metadata/plugin.metadata.ts +++ b/libs/sdk/src/common/metadata/plugin.metadata.ts @@ -1,5 +1,5 @@ import { type Token } from '@frontmcp/di'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type AdapterType, diff --git a/libs/sdk/src/common/metadata/prompt.metadata.ts b/libs/sdk/src/common/metadata/prompt.metadata.ts index 54b669661..34b4ff1a3 100644 --- a/libs/sdk/src/common/metadata/prompt.metadata.ts +++ b/libs/sdk/src/common/metadata/prompt.metadata.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { IconSchema, type Icon } from '@frontmcp/protocol'; import { entryAvailabilitySchema, type EntryAvailability } from '@frontmcp/utils'; diff --git a/libs/sdk/src/common/metadata/provider.metadata.ts b/libs/sdk/src/common/metadata/provider.metadata.ts index 5a1b1b21d..e7c5e58b8 100644 --- a/libs/sdk/src/common/metadata/provider.metadata.ts +++ b/libs/sdk/src/common/metadata/provider.metadata.ts @@ -1,5 +1,5 @@ import { ProviderScope, type ProviderMetadata } from '@frontmcp/di'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type RawZodShape } from '../types'; diff --git a/libs/sdk/src/common/metadata/resource.metadata.ts b/libs/sdk/src/common/metadata/resource.metadata.ts index 98cdff3ed..e371aeffa 100644 --- a/libs/sdk/src/common/metadata/resource.metadata.ts +++ b/libs/sdk/src/common/metadata/resource.metadata.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { IconSchema, type Icon } from '@frontmcp/protocol'; import { entryAvailabilitySchema, isValidMcpUri, isValidMcpUriTemplate, type EntryAvailability } from '@frontmcp/utils'; diff --git a/libs/sdk/src/common/metadata/skill.metadata.ts b/libs/sdk/src/common/metadata/skill.metadata.ts index 276ca2e4c..04af600f5 100644 --- a/libs/sdk/src/common/metadata/skill.metadata.ts +++ b/libs/sdk/src/common/metadata/skill.metadata.ts @@ -1,5 +1,5 @@ import { getMetadata, isClass, type Type } from '@frontmcp/di'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { entryAvailabilitySchema, isValidMcpUri, type EntryAvailability } from '@frontmcp/utils'; import { InvalidInputError } from '../../errors/mcp.error'; diff --git a/libs/sdk/src/common/metadata/tool.metadata.ts b/libs/sdk/src/common/metadata/tool.metadata.ts index 457689d78..a540b12a5 100644 --- a/libs/sdk/src/common/metadata/tool.metadata.ts +++ b/libs/sdk/src/common/metadata/tool.metadata.ts @@ -6,7 +6,7 @@ import { type RateLimitConfig, type TimeoutConfig, } from '@frontmcp/guard'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { AudioContentSchema, EmbeddedResourceSchema, diff --git a/libs/sdk/src/common/metadata/ui-serving-mode.ts b/libs/sdk/src/common/metadata/ui-serving-mode.ts index a210e818e..ff5fc6568 100644 --- a/libs/sdk/src/common/metadata/ui-serving-mode.ts +++ b/libs/sdk/src/common/metadata/ui-serving-mode.ts @@ -1,5 +1,5 @@ // file: libs/sdk/src/common/metadata/ui-serving-mode.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { WidgetServingMode } from '@frontmcp/uipack/types'; /** diff --git a/libs/sdk/src/common/metadata/workflow.metadata.ts b/libs/sdk/src/common/metadata/workflow.metadata.ts index c39817ebe..7d4c32f7a 100644 --- a/libs/sdk/src/common/metadata/workflow.metadata.ts +++ b/libs/sdk/src/common/metadata/workflow.metadata.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type RawZodShape } from '../types'; import { type JobPermission, type JobRetryConfig } from './job.metadata'; diff --git a/libs/sdk/src/common/schemas/annotated-class.schema.ts b/libs/sdk/src/common/schemas/annotated-class.schema.ts index a48f99019..0f46b005d 100644 --- a/libs/sdk/src/common/schemas/annotated-class.schema.ts +++ b/libs/sdk/src/common/schemas/annotated-class.schema.ts @@ -1,5 +1,5 @@ import { type Type } from '@frontmcp/di'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { isPackageSpecifier } from '../../esm-loader/package-specifier'; import type { AdapterType, AgentType, PluginType, ProviderType } from '../interfaces'; diff --git a/libs/sdk/src/common/schemas/http-input.schema.ts b/libs/sdk/src/common/schemas/http-input.schema.ts index 2392e807e..69388b556 100644 --- a/libs/sdk/src/common/schemas/http-input.schema.ts +++ b/libs/sdk/src/common/schemas/http-input.schema.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; // Checked as `z.object({})` checks, but passed through as is: a passthrough object copies every key of a Node request. const httpObjectSchema = () => diff --git a/libs/sdk/src/common/schemas/http-output.schema.ts b/libs/sdk/src/common/schemas/http-output.schema.ts index ac465d31a..ccb104d6e 100644 --- a/libs/sdk/src/common/schemas/http-output.schema.ts +++ b/libs/sdk/src/common/schemas/http-output.schema.ts @@ -1,7 +1,7 @@ // output-schemas.ts // Use lazy-zod: this module defines many top-level discriminated-union // and object schemas. Lazy construction defers the cost to first `.parse()`. -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { JSONRPCErrorSchema, JSONRPCResponseSchema, diff --git a/libs/sdk/src/common/schemas/session-header.schema.ts b/libs/sdk/src/common/schemas/session-header.schema.ts index 8b7a713f1..14bf7e473 100644 --- a/libs/sdk/src/common/schemas/session-header.schema.ts +++ b/libs/sdk/src/common/schemas/session-header.schema.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; /** * Zod schema for validating mcp-session-id header format. diff --git a/libs/sdk/src/common/types/auth/session.types.ts b/libs/sdk/src/common/types/auth/session.types.ts index 96afc7b94..0ffe9237b 100644 --- a/libs/sdk/src/common/types/auth/session.types.ts +++ b/libs/sdk/src/common/types/auth/session.types.ts @@ -6,7 +6,7 @@ import { type SessionIdPayload, type UserClaim, } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; // Re-export non-conflicting session types from @frontmcp/auth export { aiPlatformTypeSchema, userClaimSchema, sessionIdPayloadSchema } from '@frontmcp/auth'; diff --git a/libs/sdk/src/common/types/options/elicitation/schema.ts b/libs/sdk/src/common/types/options/elicitation/schema.ts index 2efbf0720..9b555a9b1 100644 --- a/libs/sdk/src/common/types/options/elicitation/schema.ts +++ b/libs/sdk/src/common/types/options/elicitation/schema.ts @@ -3,7 +3,7 @@ // Elicitation configuration schema for @FrontMcp decorator. // Uses explicit interfaces from interfaces.ts for IDE autocomplete. -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../../common.types'; import { redisOptionsSchema } from '../redis'; diff --git a/libs/sdk/src/common/types/options/ext-apps/schema.ts b/libs/sdk/src/common/types/options/ext-apps/schema.ts index 03e011c49..d45865b62 100644 --- a/libs/sdk/src/common/types/options/ext-apps/schema.ts +++ b/libs/sdk/src/common/types/options/ext-apps/schema.ts @@ -1,7 +1,7 @@ // common/types/options/ext-apps/schema.ts // Zod schema for ext-apps configuration -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../../common.types'; import type { ExtAppsHostCapabilitiesInterface, ExtAppsOptionsInterface } from './interfaces'; diff --git a/libs/sdk/src/common/types/options/health/schema.ts b/libs/sdk/src/common/types/options/health/schema.ts index 7fc84e0aa..7753113a7 100644 --- a/libs/sdk/src/common/types/options/health/schema.ts +++ b/libs/sdk/src/common/types/options/health/schema.ts @@ -3,7 +3,7 @@ // Health and readiness endpoint configuration schema for @FrontMcp decorator. // Uses explicit interfaces from interfaces.ts for IDE autocomplete. -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../../common.types'; import type { HealthOptionsInterface, HealthReadyzOptionsInterface } from './interfaces'; diff --git a/libs/sdk/src/common/types/options/http/schema.ts b/libs/sdk/src/common/types/options/http/schema.ts index 9bdb96d43..888ed1ad6 100644 --- a/libs/sdk/src/common/types/options/http/schema.ts +++ b/libs/sdk/src/common/types/options/http/schema.ts @@ -2,7 +2,7 @@ // Zod schema for HTTP configuration. Uses lazy-zod so the schema tree is // not constructed at module load when nothing parses it. -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { getEnv } from '@frontmcp/utils'; import type { HttpMethod, ServerRequestHandler } from '../../../interfaces'; diff --git a/libs/sdk/src/common/types/options/logging/schema.ts b/libs/sdk/src/common/types/options/logging/schema.ts index 7436ccbf6..ff65674b2 100644 --- a/libs/sdk/src/common/types/options/logging/schema.ts +++ b/libs/sdk/src/common/types/options/logging/schema.ts @@ -1,7 +1,7 @@ // common/types/options/logging/schema.ts // Zod schema for logging configuration -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { getEnv, getRuntimeContext } from '@frontmcp/utils'; import { annotatedFrontMcpLoggerSchema } from '../../../schemas'; diff --git a/libs/sdk/src/common/types/options/metrics/schema.ts b/libs/sdk/src/common/types/options/metrics/schema.ts index 5da7cea9c..5c5918611 100644 --- a/libs/sdk/src/common/types/options/metrics/schema.ts +++ b/libs/sdk/src/common/types/options/metrics/schema.ts @@ -2,7 +2,7 @@ // // Configuration schema for the /metrics endpoint (issue #397). -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../../common.types'; import type { MetricsOptionsInterface, MetricsProcessOptionsInterface } from './interfaces'; diff --git a/libs/sdk/src/common/types/options/observability/schema.ts b/libs/sdk/src/common/types/options/observability/schema.ts index ccfd41d22..a309dac72 100644 --- a/libs/sdk/src/common/types/options/observability/schema.ts +++ b/libs/sdk/src/common/types/options/observability/schema.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; /** * Zod schema for observability configuration. diff --git a/libs/sdk/src/common/types/options/outbound-fetch/schema.ts b/libs/sdk/src/common/types/options/outbound-fetch/schema.ts index b48c2bee0..7f20bfebc 100644 --- a/libs/sdk/src/common/types/options/outbound-fetch/schema.ts +++ b/libs/sdk/src/common/types/options/outbound-fetch/schema.ts @@ -1,7 +1,7 @@ // common/types/options/outbound-fetch/schema.ts // Zod schema for outbound `this.fetch()` configuration -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../../common.types'; import type { OutboundFetchOptionsInterface } from './interfaces'; diff --git a/libs/sdk/src/common/types/options/pagination/schema.ts b/libs/sdk/src/common/types/options/pagination/schema.ts index de509110f..64f8e1b54 100644 --- a/libs/sdk/src/common/types/options/pagination/schema.ts +++ b/libs/sdk/src/common/types/options/pagination/schema.ts @@ -1,7 +1,7 @@ // common/types/options/pagination/schema.ts // Zod schema for pagination configuration -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../../common.types'; import type { PaginationOptionsInterface, ToolPaginationOptionsInterface } from './interfaces'; diff --git a/libs/sdk/src/common/types/options/redis/schema.ts b/libs/sdk/src/common/types/options/redis/schema.ts index cdcaf4743..e33745212 100644 --- a/libs/sdk/src/common/types/options/redis/schema.ts +++ b/libs/sdk/src/common/types/options/redis/schema.ts @@ -1,7 +1,7 @@ // common/types/options/redis/schema.ts // Zod schema for Redis/storage configuration -import { NEVER, z } from '@frontmcp/lazy-zod'; +import { NEVER, internalZ as z } from '@frontmcp/lazy-zod'; import { describeRedisUrlConflicts, mergeRedisUrlFields } from '@frontmcp/utils'; import type { PubsubOptionsInterface, RedisOptionsInterface } from './interfaces'; diff --git a/libs/sdk/src/common/types/options/registry/schema.ts b/libs/sdk/src/common/types/options/registry/schema.ts index f41c247d3..c383aefea 100644 --- a/libs/sdk/src/common/types/options/registry/schema.ts +++ b/libs/sdk/src/common/types/options/registry/schema.ts @@ -1,6 +1,6 @@ // common/types/options/registry/schema.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../../common.types'; import type { McpRegistryOptionsInterface, McpRegistryRepositoryInterface } from './interfaces'; diff --git a/libs/sdk/src/common/types/options/server-info/schema.ts b/libs/sdk/src/common/types/options/server-info/schema.ts index 6fbfa403f..622675d0a 100644 --- a/libs/sdk/src/common/types/options/server-info/schema.ts +++ b/libs/sdk/src/common/types/options/server-info/schema.ts @@ -1,7 +1,7 @@ // common/types/options/server-info/schema.ts // Zod schema for server info configuration -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { IconSchema } from '@frontmcp/protocol'; import { type RawZodShape } from '../../common.types'; diff --git a/libs/sdk/src/common/types/options/session/schema.ts b/libs/sdk/src/common/types/options/session/schema.ts index 2a07c5a2d..a0c4378bf 100644 --- a/libs/sdk/src/common/types/options/session/schema.ts +++ b/libs/sdk/src/common/types/options/session/schema.ts @@ -1,7 +1,7 @@ // common/types/options/session/schema.ts // Zod schema for session configuration -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { aiPlatformTypeSchema } from '../../auth/session.types'; import { type RawZodShape } from '../../common.types'; diff --git a/libs/sdk/src/common/types/options/skills-http/schema.ts b/libs/sdk/src/common/types/options/skills-http/schema.ts index 6a649cb30..a4c0295f2 100644 --- a/libs/sdk/src/common/types/options/skills-http/schema.ts +++ b/libs/sdk/src/common/types/options/skills-http/schema.ts @@ -1,7 +1,7 @@ // common/types/options/skills-http/schema.ts // Zod schema for skills HTTP configuration -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { ExternalSkillProviderBase } from '../../../../skill/providers/external-skill.provider'; import type { RawZodShape } from '../../common.types'; diff --git a/libs/sdk/src/common/types/options/sqlite/schema.ts b/libs/sdk/src/common/types/options/sqlite/schema.ts index d366a52f4..f76616c82 100644 --- a/libs/sdk/src/common/types/options/sqlite/schema.ts +++ b/libs/sdk/src/common/types/options/sqlite/schema.ts @@ -4,7 +4,7 @@ * Zod schema for SQLite storage options used in FrontMCP config. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { RawZodShape } from '../../common.types'; import type { SqliteOptionsInterface } from './interfaces'; diff --git a/libs/sdk/src/common/types/options/transport/schema.ts b/libs/sdk/src/common/types/options/transport/schema.ts index 015392377..e99e9a934 100644 --- a/libs/sdk/src/common/types/options/transport/schema.ts +++ b/libs/sdk/src/common/types/options/transport/schema.ts @@ -3,7 +3,7 @@ // Transport configuration schema with protocol presets and simplified structure. // Uses explicit interfaces from interfaces.ts for IDE autocomplete. -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { isServerless } from '@frontmcp/utils'; import { type RawZodShape } from '../../common.types'; diff --git a/libs/sdk/src/common/utils/decide-request-intent.utils.ts b/libs/sdk/src/common/utils/decide-request-intent.utils.ts index 0b5cea81a..7cbe10098 100644 --- a/libs/sdk/src/common/utils/decide-request-intent.utils.ts +++ b/libs/sdk/src/common/utils/decide-request-intent.utils.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type ServerRequest } from '../interfaces'; import { type LegacyProtocolFlags } from '../types/options/transport/schema'; diff --git a/libs/sdk/src/completion/flows/complete.flow.ts b/libs/sdk/src/completion/flows/complete.flow.ts index a59efc040..b2273280b 100644 --- a/libs/sdk/src/completion/flows/complete.flow.ts +++ b/libs/sdk/src/completion/flows/complete.flow.ts @@ -1,7 +1,7 @@ // file: libs/sdk/src/completion/flows/complete.flow.ts import { AuthorityDeniedError, resolveRequiredScopes, type AuthoritiesMetadata } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { CompleteRequestSchema, CompleteResultSchema, type AuthInfo } from '@frontmcp/protocol'; import { loadRemoteAppCapabilities } from '../../app/remote-capabilities.utils'; diff --git a/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts b/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts index a9d215b73..6a0890e19 100644 --- a/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts +++ b/libs/sdk/src/elicitation/flows/elicitation-request.flow.ts @@ -7,7 +7,7 @@ * @module elicitation/flows/elicitation-request.flow */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { randomUUID } from '@frontmcp/utils'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; diff --git a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts index face74cf3..5f3b5117c 100644 --- a/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts +++ b/libs/sdk/src/elicitation/flows/elicitation-result.flow.ts @@ -7,7 +7,7 @@ * @module elicitation/flows/elicitation-result.flow */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; import { ElicitationStoreNotInitializedError, InvalidInputError } from '../../errors'; diff --git a/libs/sdk/src/elicitation/helpers/elicit.helper.ts b/libs/sdk/src/elicitation/helpers/elicit.helper.ts index fc0eedf00..0fa69591f 100644 --- a/libs/sdk/src/elicitation/helpers/elicit.helper.ts +++ b/libs/sdk/src/elicitation/helpers/elicit.helper.ts @@ -7,7 +7,7 @@ * @module elicitation/helpers/elicit.helper */ -import { toJSONSchema, z, type ZodType } from '@frontmcp/lazy-zod'; +import { toJSONSchema, internalZ as z, type ZodType } from '@frontmcp/lazy-zod'; import { awaitOutsideRequest, randomUUID } from '@frontmcp/utils'; import { type FrontMcpContext } from '../../context'; diff --git a/libs/sdk/src/elicitation/helpers/validate-elicitation-content.ts b/libs/sdk/src/elicitation/helpers/validate-elicitation-content.ts index 044255a1e..8aafae369 100644 --- a/libs/sdk/src/elicitation/helpers/validate-elicitation-content.ts +++ b/libs/sdk/src/elicitation/helpers/validate-elicitation-content.ts @@ -7,7 +7,7 @@ * @module elicitation/helpers/validate-elicitation-content */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; /** * Validation result interface. diff --git a/libs/sdk/src/elicitation/send-elicitation-result.tool.ts b/libs/sdk/src/elicitation/send-elicitation-result.tool.ts index 0fbeed721..5a7f3f751 100644 --- a/libs/sdk/src/elicitation/send-elicitation-result.tool.ts +++ b/libs/sdk/src/elicitation/send-elicitation-result.tool.ts @@ -12,7 +12,7 @@ * 5. Original tool's elicit() returns the pre-resolved result immediately */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { CallToolResult } from '@frontmcp/protocol'; import { Tool, ToolContext } from '../common'; diff --git a/libs/sdk/src/errors/authorization-required.error.ts b/libs/sdk/src/errors/authorization-required.error.ts index 0c6f599e9..9cac8a137 100644 --- a/libs/sdk/src/errors/authorization-required.error.ts +++ b/libs/sdk/src/errors/authorization-required.error.ts @@ -11,7 +11,7 @@ * * Supports MCP elicit flow for clients that support it. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { PublicMcpError } from './mcp.error'; diff --git a/libs/sdk/src/esm-loader/esm-auth.types.ts b/libs/sdk/src/esm-loader/esm-auth.types.ts index 4cbceeb6a..1072752a6 100644 --- a/libs/sdk/src/esm-loader/esm-auth.types.ts +++ b/libs/sdk/src/esm-loader/esm-auth.types.ts @@ -3,7 +3,7 @@ * @description Authentication configuration for private npm registries and esm.sh CDN. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; /** * Authentication configuration for accessing private npm registries. diff --git a/libs/sdk/src/esm-loader/esm-manifest.ts b/libs/sdk/src/esm-loader/esm-manifest.ts index ce9498985..8ba50592e 100644 --- a/libs/sdk/src/esm-loader/esm-manifest.ts +++ b/libs/sdk/src/esm-loader/esm-manifest.ts @@ -4,7 +4,7 @@ * Defines the contract that npm packages must follow to be loadable by FrontMCP. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { isDecoratedAgentClass, diff --git a/libs/sdk/src/esm-loader/factories/esm-record-builders.ts b/libs/sdk/src/esm-loader/factories/esm-record-builders.ts index 89cf80bd5..3657cf1f8 100644 --- a/libs/sdk/src/esm-loader/factories/esm-record-builders.ts +++ b/libs/sdk/src/esm-loader/factories/esm-record-builders.ts @@ -7,7 +7,7 @@ */ import type { Type } from '@frontmcp/di'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { PromptKind, diff --git a/libs/sdk/src/job/flows/execute-job.flow.ts b/libs/sdk/src/job/flows/execute-job.flow.ts index 4e34db847..f2d3e955f 100644 --- a/libs/sdk/src/job/flows/execute-job.flow.ts +++ b/libs/sdk/src/job/flows/execute-job.flow.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/job/flows/execute-job.flow.ts import type { AuthoritiesContextBuilder } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Flow, diff --git a/libs/sdk/src/job/job.instance.ts b/libs/sdk/src/job/job.instance.ts index ea38a0ce6..92820efaf 100644 --- a/libs/sdk/src/job/job.instance.ts +++ b/libs/sdk/src/job/job.instance.ts @@ -1,5 +1,5 @@ import { type Token } from '@frontmcp/di'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { type EntryOwnerRef, diff --git a/libs/sdk/src/job/tools/execute-job.tool.ts b/libs/sdk/src/job/tools/execute-job.tool.ts index a0fade35b..1f4792f18 100644 --- a/libs/sdk/src/job/tools/execute-job.tool.ts +++ b/libs/sdk/src/job/tools/execute-job.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import { JobNotAuthorizedError } from '../../errors'; diff --git a/libs/sdk/src/job/tools/get-job-status.tool.ts b/libs/sdk/src/job/tools/get-job-status.tool.ts index 00d98cb4f..0e2610629 100644 --- a/libs/sdk/src/job/tools/get-job-status.tool.ts +++ b/libs/sdk/src/job/tools/get-job-status.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import { resolvePrincipal } from '../../common/utils/principal.utils'; diff --git a/libs/sdk/src/job/tools/list-jobs.tool.ts b/libs/sdk/src/job/tools/list-jobs.tool.ts index dcef6b7d4..6ecf1c65d 100644 --- a/libs/sdk/src/job/tools/list-jobs.tool.ts +++ b/libs/sdk/src/job/tools/list-jobs.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import { type JobEntry } from '../../common/entries/job.entry'; diff --git a/libs/sdk/src/job/tools/register-job.tool.ts b/libs/sdk/src/job/tools/register-job.tool.ts index d235acb36..63913e672 100644 --- a/libs/sdk/src/job/tools/register-job.tool.ts +++ b/libs/sdk/src/job/tools/register-job.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import type { ToolInputType, ToolOutputType } from '../../common/metadata/tool.metadata'; diff --git a/libs/sdk/src/job/tools/remove-job.tool.ts b/libs/sdk/src/job/tools/remove-job.tool.ts index b37f0ebbd..939b92089 100644 --- a/libs/sdk/src/job/tools/remove-job.tool.ts +++ b/libs/sdk/src/job/tools/remove-job.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import { JobNotAuthorizedError } from '../../errors'; diff --git a/libs/sdk/src/logging/flows/set-level.flow.ts b/libs/sdk/src/logging/flows/set-level.flow.ts index d49b1fcfd..ec45da034 100644 --- a/libs/sdk/src/logging/flows/set-level.flow.ts +++ b/libs/sdk/src/logging/flows/set-level.flow.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/logging/flows/set-level.flow.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { EmptyResultSchema, LoggingLevelSchema, SetLevelRequestSchema } from '@frontmcp/protocol'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; diff --git a/libs/sdk/src/mcp-registry/flows/server-card.flow.ts b/libs/sdk/src/mcp-registry/flows/server-card.flow.ts index 029548b2b..412fa20cb 100644 --- a/libs/sdk/src/mcp-registry/flows/server-card.flow.ts +++ b/libs/sdk/src/mcp-registry/flows/server-card.flow.ts @@ -1,6 +1,6 @@ /** GET /server-card: the SEP-2127 server card of the server's own streamable-HTTP endpoint. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { sha256Base64url } from '@frontmcp/utils'; import { diff --git a/libs/sdk/src/mcp-registry/flows/well-known.ai-catalog.flow.ts b/libs/sdk/src/mcp-registry/flows/well-known.ai-catalog.flow.ts index fa5601893..daf6396b9 100644 --- a/libs/sdk/src/mcp-registry/flows/well-known.ai-catalog.flow.ts +++ b/libs/sdk/src/mcp-registry/flows/well-known.ai-catalog.flow.ts @@ -1,6 +1,6 @@ /** GET /.well-known/ai-catalog.json at the literal root of the host: the AI Catalog listing the server card. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { enforceGlobalRateLimit, diff --git a/libs/sdk/src/prompt/flows/get-prompt.flow.ts b/libs/sdk/src/prompt/flows/get-prompt.flow.ts index 73358df09..29eb20b6b 100644 --- a/libs/sdk/src/prompt/flows/get-prompt.flow.ts +++ b/libs/sdk/src/prompt/flows/get-prompt.flow.ts @@ -1,7 +1,7 @@ // file: libs/sdk/src/prompt/flows/get-prompt.flow.ts import { AuthorityDeniedError, resolveRequiredScopes } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { GetPromptRequestSchema, GetPromptResultSchema, type AuthInfo } from '@frontmcp/protocol'; import { loadRemoteAppCapabilities } from '../../app/remote-capabilities.utils'; diff --git a/libs/sdk/src/prompt/flows/prompts-list.flow.ts b/libs/sdk/src/prompt/flows/prompts-list.flow.ts index d8310a518..566f958a4 100644 --- a/libs/sdk/src/prompt/flows/prompts-list.flow.ts +++ b/libs/sdk/src/prompt/flows/prompts-list.flow.ts @@ -12,7 +12,7 @@ import { import 'reflect-metadata'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { ListPromptsRequestSchema, ListPromptsResultSchema, type Prompt } from '@frontmcp/protocol'; import { isPubliclyListed, publicAccessFor } from '../../auth/public-access.utils'; diff --git a/libs/sdk/src/resource/flows/read-resource.flow.ts b/libs/sdk/src/resource/flows/read-resource.flow.ts index 399b67732..3b5690c70 100644 --- a/libs/sdk/src/resource/flows/read-resource.flow.ts +++ b/libs/sdk/src/resource/flows/read-resource.flow.ts @@ -1,7 +1,7 @@ // file: libs/sdk/src/resource/flows/read-resource.flow.ts import { AuthorityDeniedError, resolveRequiredScopes } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { ReadResourceRequestSchema, ReadResourceResultSchema, type AuthInfo } from '@frontmcp/protocol'; import { randomBytes } from '@frontmcp/utils'; diff --git a/libs/sdk/src/resource/flows/resource-templates-list.flow.ts b/libs/sdk/src/resource/flows/resource-templates-list.flow.ts index 0d059b5e7..930a15fda 100644 --- a/libs/sdk/src/resource/flows/resource-templates-list.flow.ts +++ b/libs/sdk/src/resource/flows/resource-templates-list.flow.ts @@ -12,7 +12,7 @@ import { import 'reflect-metadata'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { ListResourceTemplatesRequestSchema, ListResourceTemplatesResultSchema, diff --git a/libs/sdk/src/resource/flows/resources-list.flow.ts b/libs/sdk/src/resource/flows/resources-list.flow.ts index 0cbd01577..57f6a42f4 100644 --- a/libs/sdk/src/resource/flows/resources-list.flow.ts +++ b/libs/sdk/src/resource/flows/resources-list.flow.ts @@ -12,7 +12,7 @@ import { import 'reflect-metadata'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { ListResourcesRequestSchema, ListResourcesResultSchema, type Resource } from '@frontmcp/protocol'; import { callSurfaceOf, isOfferedOnSurface } from '../../common/availability'; diff --git a/libs/sdk/src/resource/flows/subscribe-resource.flow.ts b/libs/sdk/src/resource/flows/subscribe-resource.flow.ts index 951433fc7..69a8001bc 100644 --- a/libs/sdk/src/resource/flows/subscribe-resource.flow.ts +++ b/libs/sdk/src/resource/flows/subscribe-resource.flow.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/resource/flows/subscribe-resource.flow.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { EmptyResultSchema, SubscribeRequestSchema } from '@frontmcp/protocol'; import { loadRemoteAppCapabilities } from '../../app/remote-capabilities.utils'; diff --git a/libs/sdk/src/resource/flows/unsubscribe-resource.flow.ts b/libs/sdk/src/resource/flows/unsubscribe-resource.flow.ts index 49d9e3df3..bc3cbd9b1 100644 --- a/libs/sdk/src/resource/flows/unsubscribe-resource.flow.ts +++ b/libs/sdk/src/resource/flows/unsubscribe-resource.flow.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/resource/flows/unsubscribe-resource.flow.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { EmptyResultSchema, UnsubscribeRequestSchema } from '@frontmcp/protocol'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; diff --git a/libs/sdk/src/scope/flows/http.request.flow.ts b/libs/sdk/src/scope/flows/http.request.flow.ts index ce70c1790..3bb59243f 100644 --- a/libs/sdk/src/scope/flows/http.request.flow.ts +++ b/libs/sdk/src/scope/flows/http.request.flow.ts @@ -8,7 +8,7 @@ import { type TokenRefreshCallback, } from '@frontmcp/auth'; import { GuardStorageUnavailableError, type GuardManager } from '@frontmcp/guard'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { randomUUID } from '@frontmcp/utils'; import { sessionVerifyOutputSchema } from '../../auth/flows/session.verify.flow'; diff --git a/libs/sdk/src/skill/flows/filter-skills.flow.ts b/libs/sdk/src/skill/flows/filter-skills.flow.ts index de451b43c..e5503e2b2 100644 --- a/libs/sdk/src/skill/flows/filter-skills.flow.ts +++ b/libs/sdk/src/skill/flows/filter-skills.flow.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/skill/flows/filter-skills.flow.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions, type SkillEntry } from '../../common'; import { InvalidInputError } from '../../errors'; diff --git a/libs/sdk/src/skill/flows/http/llm-full-txt.flow.ts b/libs/sdk/src/skill/flows/http/llm-full-txt.flow.ts index 1272dc9e0..01fe1513b 100644 --- a/libs/sdk/src/skill/flows/http/llm-full-txt.flow.ts +++ b/libs/sdk/src/skill/flows/http/llm-full-txt.flow.ts @@ -5,7 +5,7 @@ * Returns full skill content with instructions and tool schemas. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { enforceGlobalRateLimit, diff --git a/libs/sdk/src/skill/flows/http/llm-txt.flow.ts b/libs/sdk/src/skill/flows/http/llm-txt.flow.ts index 4491ed77d..6c366d7e9 100644 --- a/libs/sdk/src/skill/flows/http/llm-txt.flow.ts +++ b/libs/sdk/src/skill/flows/http/llm-txt.flow.ts @@ -5,7 +5,7 @@ * Returns compact skill summaries in plain text format. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { enforceGlobalRateLimit, diff --git a/libs/sdk/src/skill/flows/http/skills-api.flow.ts b/libs/sdk/src/skill/flows/http/skills-api.flow.ts index 960efd1e0..fb8bf8275 100644 --- a/libs/sdk/src/skill/flows/http/skills-api.flow.ts +++ b/libs/sdk/src/skill/flows/http/skills-api.flow.ts @@ -5,7 +5,7 @@ * Provides JSON API for listing, searching, and loading skills. */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { enforceGlobalRateLimit, diff --git a/libs/sdk/src/skill/flows/load-skill.flow.ts b/libs/sdk/src/skill/flows/load-skill.flow.ts index 3750514a5..01a6e606e 100644 --- a/libs/sdk/src/skill/flows/load-skill.flow.ts +++ b/libs/sdk/src/skill/flows/load-skill.flow.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/skill/flows/load-skill.flow.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; import { InvalidInputError, PublicMcpError } from '../../errors'; diff --git a/libs/sdk/src/skill/flows/search-skills.flow.ts b/libs/sdk/src/skill/flows/search-skills.flow.ts index 862e3cacc..9ac41984f 100644 --- a/libs/sdk/src/skill/flows/search-skills.flow.ts +++ b/libs/sdk/src/skill/flows/search-skills.flow.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/skill/flows/search-skills.flow.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; import { extractToolNames } from '../../common/metadata/skill.metadata'; diff --git a/libs/sdk/src/task/flows/tasks-cancel.flow.ts b/libs/sdk/src/task/flows/tasks-cancel.flow.ts index d5aea1d54..fc3e8b709 100644 --- a/libs/sdk/src/task/flows/tasks-cancel.flow.ts +++ b/libs/sdk/src/task/flows/tasks-cancel.flow.ts @@ -10,7 +10,7 @@ * @module task/flows/tasks-cancel.flow */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { CancelTaskRequestSchema, CancelTaskResultSchema, type AuthInfo } from '@frontmcp/protocol'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; diff --git a/libs/sdk/src/task/flows/tasks-get.flow.ts b/libs/sdk/src/task/flows/tasks-get.flow.ts index 3f255f898..02be8a26c 100644 --- a/libs/sdk/src/task/flows/tasks-get.flow.ts +++ b/libs/sdk/src/task/flows/tasks-get.flow.ts @@ -7,7 +7,7 @@ * @module task/flows/tasks-get.flow */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { GetTaskRequestSchema, GetTaskResultSchema, type AuthInfo } from '@frontmcp/protocol'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; diff --git a/libs/sdk/src/task/flows/tasks-list.flow.ts b/libs/sdk/src/task/flows/tasks-list.flow.ts index 8047b332b..53803ce82 100644 --- a/libs/sdk/src/task/flows/tasks-list.flow.ts +++ b/libs/sdk/src/task/flows/tasks-list.flow.ts @@ -8,7 +8,7 @@ * @module task/flows/tasks-list.flow */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { ListTasksRequestSchema, ListTasksResultSchema, type AuthInfo } from '@frontmcp/protocol'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; diff --git a/libs/sdk/src/task/flows/tasks-result.flow.ts b/libs/sdk/src/task/flows/tasks-result.flow.ts index f0d7cf605..9d1040c62 100644 --- a/libs/sdk/src/task/flows/tasks-result.flow.ts +++ b/libs/sdk/src/task/flows/tasks-result.flow.ts @@ -14,7 +14,7 @@ * @module task/flows/tasks-result.flow */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { GetTaskPayloadRequestSchema, GetTaskPayloadResultSchema, type AuthInfo } from '@frontmcp/protocol'; import { Flow, FlowBase, FlowHooksOf, type FlowPlan, type FlowRunOptions } from '../../common'; diff --git a/libs/sdk/src/tool/flows/call-tool.flow.ts b/libs/sdk/src/tool/flows/call-tool.flow.ts index 0a3bed0b0..e4a3e2bb9 100644 --- a/libs/sdk/src/tool/flows/call-tool.flow.ts +++ b/libs/sdk/src/tool/flows/call-tool.flow.ts @@ -1,7 +1,7 @@ // tools/flows/call-tool.flow.ts import { AuthorityDeniedError, resolveRequiredScopes, signIncrementalAuthTicket } from '@frontmcp/auth'; import { ExecutionTimeoutError, withTimeout, type SemaphoreTicket } from '@frontmcp/guard'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { CallToolRequestSchema, CallToolResultSchema, type AuthInfo } from '@frontmcp/protocol'; import type { ToolResponseContent } from '@frontmcp/uipack/adapters'; import { diff --git a/libs/sdk/src/tool/flows/tools-list.flow.ts b/libs/sdk/src/tool/flows/tools-list.flow.ts index 1e4e2b87e..0aa656a1d 100644 --- a/libs/sdk/src/tool/flows/tools-list.flow.ts +++ b/libs/sdk/src/tool/flows/tools-list.flow.ts @@ -16,7 +16,7 @@ import { import 'reflect-metadata'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { ListToolsRequestSchema, ListToolsResultSchema, type AuthInfo } from '@frontmcp/protocol'; import type { AdapterPlatformType as AIPlatformType } from '@frontmcp/uipack/adapters'; import { isUIType, type UIType } from '@frontmcp/uipack/types'; diff --git a/libs/sdk/src/tool/tool.instance.ts b/libs/sdk/src/tool/tool.instance.ts index 84bb750f1..987648fac 100644 --- a/libs/sdk/src/tool/tool.instance.ts +++ b/libs/sdk/src/tool/tool.instance.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/tool/tool.instance.ts -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { CallToolRequest } from '@frontmcp/protocol'; import { diff --git a/libs/sdk/src/tool/tool.utils.ts b/libs/sdk/src/tool/tool.utils.ts index 8ee8364ea..31755bb44 100644 --- a/libs/sdk/src/tool/tool.utils.ts +++ b/libs/sdk/src/tool/tool.utils.ts @@ -1,6 +1,6 @@ // file: libs/sdk/src/tool/tool.utils.ts import { depsOfClass, depsOfFunc, getMetadata, isClass, type Token, type Type } from '@frontmcp/di'; -import { hasTransform, z, ZodBigInt, ZodBoolean, ZodDate, ZodNumber, ZodString } from '@frontmcp/lazy-zod'; +import { hasTransform, internalZ as z, ZodBigInt, ZodBoolean, ZodDate, ZodNumber, ZodString } from '@frontmcp/lazy-zod'; import { type AudioContent, type ContentBlock, diff --git a/libs/sdk/src/transport/flows/handle.mcp-20260728.flow.ts b/libs/sdk/src/transport/flows/handle.mcp-20260728.flow.ts index 19975e877..e669b02c6 100644 --- a/libs/sdk/src/transport/flows/handle.mcp-20260728.flow.ts +++ b/libs/sdk/src/transport/flows/handle.mcp-20260728.flow.ts @@ -15,7 +15,7 @@ * @see https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http */ import { isAnonymousSubject } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { MCP_20260728_META, type LoggingLevel, type SubscriptionFilter } from '@frontmcp/protocol'; import { runRequestExclusive } from '@frontmcp/utils'; diff --git a/libs/sdk/src/transport/flows/handle.sse.flow.ts b/libs/sdk/src/transport/flows/handle.sse.flow.ts index 391e2331a..9b526deba 100644 --- a/libs/sdk/src/transport/flows/handle.sse.flow.ts +++ b/libs/sdk/src/transport/flows/handle.sse.flow.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { createSessionId } from '../../auth/session/utils/session-id.utils'; import { diff --git a/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts b/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts index a7c2a41b5..a584d0563 100644 --- a/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts +++ b/libs/sdk/src/transport/flows/handle.stateless-http.flow.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { RequestSchema } from '@frontmcp/protocol'; import { diff --git a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts index 3b4ae25d2..ce113a45c 100644 --- a/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts +++ b/libs/sdk/src/transport/flows/handle.streamable-http.flow.ts @@ -1,5 +1,5 @@ import type { StoredSession } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { CallToolResultSchema, ElicitResultSchema, RequestSchema, type RequestId } from '@frontmcp/protocol'; import { createSessionId } from '../../auth/session/utils/session-id.utils'; diff --git a/libs/sdk/src/transport/mcp-20260728/dispatcher.ts b/libs/sdk/src/transport/mcp-20260728/dispatcher.ts index 79dd96811..b2aa8b0bf 100644 --- a/libs/sdk/src/transport/mcp-20260728/dispatcher.ts +++ b/libs/sdk/src/transport/mcp-20260728/dispatcher.ts @@ -8,7 +8,7 @@ * and result decoration. */ import { isAnonymousSubject } from '@frontmcp/auth'; -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { MCP_20260728_ERROR_CODES, MCP_20260728_REMOVED_METHODS, diff --git a/libs/sdk/src/transport/mcp-handlers/skills-mcp.types.ts b/libs/sdk/src/transport/mcp-handlers/skills-mcp.types.ts index 7a8f5c106..7f2e03e94 100644 --- a/libs/sdk/src/transport/mcp-handlers/skills-mcp.types.ts +++ b/libs/sdk/src/transport/mcp-handlers/skills-mcp.types.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; // ═══════════════════════════════════════════════════════════════════════════ // Skills Search Types diff --git a/libs/sdk/src/workflow/tools/execute-workflow.tool.ts b/libs/sdk/src/workflow/tools/execute-workflow.tool.ts index ef141032e..222b43d89 100644 --- a/libs/sdk/src/workflow/tools/execute-workflow.tool.ts +++ b/libs/sdk/src/workflow/tools/execute-workflow.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import { JobNotAuthorizedError } from '../../errors'; diff --git a/libs/sdk/src/workflow/tools/get-workflow-status.tool.ts b/libs/sdk/src/workflow/tools/get-workflow-status.tool.ts index fbfbdc93e..18014e4df 100644 --- a/libs/sdk/src/workflow/tools/get-workflow-status.tool.ts +++ b/libs/sdk/src/workflow/tools/get-workflow-status.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import { resolvePrincipal } from '../../common/utils/principal.utils'; diff --git a/libs/sdk/src/workflow/tools/list-workflows.tool.ts b/libs/sdk/src/workflow/tools/list-workflows.tool.ts index bf1b30e2b..0d73a519a 100644 --- a/libs/sdk/src/workflow/tools/list-workflows.tool.ts +++ b/libs/sdk/src/workflow/tools/list-workflows.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import { type WorkflowEntry } from '../../common/entries/workflow.entry'; diff --git a/libs/sdk/src/workflow/tools/register-workflow.tool.ts b/libs/sdk/src/workflow/tools/register-workflow.tool.ts index 4403bc3a4..40bf4d282 100644 --- a/libs/sdk/src/workflow/tools/register-workflow.tool.ts +++ b/libs/sdk/src/workflow/tools/register-workflow.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import { WorkflowKind } from '../../common/records/workflow.record'; diff --git a/libs/sdk/src/workflow/tools/remove-workflow.tool.ts b/libs/sdk/src/workflow/tools/remove-workflow.tool.ts index f917db9ff..761386faa 100644 --- a/libs/sdk/src/workflow/tools/remove-workflow.tool.ts +++ b/libs/sdk/src/workflow/tools/remove-workflow.tool.ts @@ -1,4 +1,4 @@ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { Tool, ToolContext } from '../../common'; import { JobNotAuthorizedError } from '../../errors'; diff --git a/libs/utils/src/crypto/key-persistence/schemas.ts b/libs/utils/src/crypto/key-persistence/schemas.ts index 207eb6aa8..b9df4a873 100644 --- a/libs/utils/src/crypto/key-persistence/schemas.ts +++ b/libs/utils/src/crypto/key-persistence/schemas.ts @@ -4,7 +4,7 @@ * @module @frontmcp/utils/key-persistence */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { AnyKeyData, AsymmetricKeyData, SecretKeyData } from './types'; diff --git a/libs/utils/src/crypto/secret-persistence/schema.ts b/libs/utils/src/crypto/secret-persistence/schema.ts index f055b8046..838b59445 100644 --- a/libs/utils/src/crypto/secret-persistence/schema.ts +++ b/libs/utils/src/crypto/secret-persistence/schema.ts @@ -4,7 +4,7 @@ * @module @frontmcp/utils/secret-persistence */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import type { SecretData, SecretValidationResult } from './types'; diff --git a/libs/utils/src/env/browser-runtime-context.ts b/libs/utils/src/env/browser-runtime-context.ts index 197e67290..7a7214454 100644 --- a/libs/utils/src/env/browser-runtime-context.ts +++ b/libs/utils/src/env/browser-runtime-context.ts @@ -6,7 +6,7 @@ * `os`/`provider`/`target`/`surface`). */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { getNodeEnv } from './browser-env'; diff --git a/libs/utils/src/env/runtime-context.ts b/libs/utils/src/env/runtime-context.ts index 92ef7b7c0..aa966458e 100644 --- a/libs/utils/src/env/runtime-context.ts +++ b/libs/utils/src/env/runtime-context.ts @@ -15,7 +15,7 @@ * - `surface` (per-call axis: mcp / cli / agent / job / http-trigger / webmcp) */ -import { z } from '@frontmcp/lazy-zod'; +import { internalZ as z } from '@frontmcp/lazy-zod'; import { getNodeEnv, isEdgeRuntime, isServerless } from '#env'; From c07fe82df96ca97bf1b909687595407ed9f15844 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Mon, 2 Nov 2026 17:39:24 +0200 Subject: [PATCH 3/5] fix(sdk): keep z in the JSDoc examples the import sweep rewrote --- libs/sdk/src/agent/index.ts | 2 +- libs/sdk/src/builtin/config/config.plugin.ts | 2 +- libs/sdk/src/builtin/config/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/sdk/src/agent/index.ts b/libs/sdk/src/agent/index.ts index c0bd76f96..171ef2089 100644 --- a/libs/sdk/src/agent/index.ts +++ b/libs/sdk/src/agent/index.ts @@ -7,7 +7,7 @@ * @example Creating an agent * ```typescript * import { Agent, AgentContext } from '@frontmcp/sdk'; - * import { internalZ as z } from '@frontmcp/lazy-zod'; + * import { z } from '@frontmcp/lazy-zod'; * * @Agent({ * name: 'research-agent', diff --git a/libs/sdk/src/builtin/config/config.plugin.ts b/libs/sdk/src/builtin/config/config.plugin.ts index 67e678c3f..cc30f1a76 100644 --- a/libs/sdk/src/builtin/config/config.plugin.ts +++ b/libs/sdk/src/builtin/config/config.plugin.ts @@ -27,7 +27,7 @@ import { loadEnvFiles, populateProcessEnv, processEnvEntries } from './providers * class MyServer {} * * // With typed schema (convict-style) - * import { internalZ as z } from '@frontmcp/lazy-zod'; + * import { z } from '@frontmcp/lazy-zod'; * * const configSchema = z.object({ * database: z.object({ diff --git a/libs/sdk/src/builtin/config/index.ts b/libs/sdk/src/builtin/config/index.ts index 18cf5fb47..b7729eda7 100644 --- a/libs/sdk/src/builtin/config/index.ts +++ b/libs/sdk/src/builtin/config/index.ts @@ -11,7 +11,7 @@ import { ConfigService } from './providers/config.service'; * @example * ```typescript * import { FrontMcp, App, ConfigPlugin } from '@frontmcp/sdk'; - * import { internalZ as z } from '@frontmcp/lazy-zod'; + * import { z } from '@frontmcp/lazy-zod'; * * // Define configuration schema * const configSchema = z.object({ From 237a383f69a58be68b289cd5e0164517cb74b2cf Mon Sep 17 00:00:00 2001 From: David Antoon Date: Mon, 2 Nov 2026 18:18:30 +0200 Subject: [PATCH 4/5] fix(lazy-zod): keep Zod's memoizer for schemas built inside an internalZ build, and convert merged schemas - A stand-in from z or lazyZ() that Zod builds while it builds one of internalZ's (.merge(), record() and discriminatedUnion() read their arguments) is built with Zod's memoizer, so a recursive schema of the caller's keeps its cycle support. The walk-less memoizer carries a registry symbol, so another copy of lazy-zod in the process recognizes it too. - forceMaterialize no longer assigns to the catchall getter that .merge() defines, which made toJSONSchema throw on every merged schema. - Tests pin the Zod memoizer internals the swap relies on and the fallback when they are gone, and compare chained internalZ schemas with Zod's own toJSONSchema. --- .../lazy-zod/src/__tests__/internal-z.spec.ts | 104 ++++++++++++++++-- .../src/__tests__/materialization.spec.ts | 8 ++ libs/lazy-zod/src/internal-z.ts | 24 +--- libs/lazy-zod/src/lazy-schema.ts | 3 +- libs/lazy-zod/src/memoizer.ts | 44 ++++++++ libs/lazy-zod/src/utils.ts | 3 +- 6 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 libs/lazy-zod/src/memoizer.ts diff --git a/libs/lazy-zod/src/__tests__/internal-z.spec.ts b/libs/lazy-zod/src/__tests__/internal-z.spec.ts index e5a0bc976..e4d061e01 100644 --- a/libs/lazy-zod/src/__tests__/internal-z.spec.ts +++ b/libs/lazy-zod/src/__tests__/internal-z.spec.ts @@ -192,6 +192,12 @@ const chainCases: ChainCase[] = [ valid: [{}, { a: 'x', d: 'y' }], invalid: [{ a: 1 }, { b: 1 }], }, + { + name: 'merge', + build: (Z) => Z.object({ a: Z.string() }).merge(Z.object({ b: Z.number().optional() })), + valid: [{ a: 'x' }, { a: 'x', b: 1 }], + invalid: [{ b: 1 }, { a: 'x', b: 'y' }], + }, { name: 'catchall', build: (Z) => Z.object({ a: Z.string() }).catchall(Z.number()), @@ -231,6 +237,17 @@ function expectSameResult(internal: realZ.ZodType, eager: realZ.ZodType, input: } } +function expectSameJSONSchema(build: (Z: Namespace) => realZ.ZodType): void { + let expected: unknown; + try { + expected = realZ.toJSONSchema(build(realZ)); + } catch (error) { + expect(() => toJSONSchema(build(internalZ))).toThrow((error as Error).message); + return; + } + expect(toJSONSchema(build(internalZ))).toEqual(expected); +} + describe('public z', () => { it('passes every leaf factory through as Zod’s own function', () => { for (const name of LEAF_FACTORIES) { @@ -280,15 +297,7 @@ describe('internalZ', () => { }); it('converts to the same JSON Schema', () => { - const eager = build(realZ); - let expected: unknown; - try { - expected = toJSONSchema(eager); - } catch (error) { - expect(() => toJSONSchema(build(internalZ))).toThrow((error as Error).message); - return; - } - expect(toJSONSchema(build(internalZ))).toEqual(expected); + expectSameJSONSchema(build); }); }); @@ -298,6 +307,10 @@ describe('internalZ', () => { const eager = build(realZ); for (const input of [...valid, ...invalid]) expectSameResult(internal, eager, input); }); + + it('converts to the same JSON Schema', () => { + expectSameJSONSchema(build); + }); }); it('builds leaf children with their parent, so the built schema holds Zod’s own leaves', () => { @@ -360,6 +373,79 @@ describe('internalZ', () => { expect(realZ.config().memoizer).toBe(before); }); + describe.each([ + [ + '.merge()', + (node: realZ.ZodType) => internalZ.object({ extra: internalZ.string().optional() }).merge(node as never), + ], + ['record()', (node: realZ.ZodType) => internalZ.record(internalZ.string(), node)], + [ + 'discriminatedUnion()', + (node: realZ.ZodType) => + internalZ.discriminatedUnion('kind', [internalZ.object({ kind: internalZ.literal('leaf') }), node as never]), + ], + ])('a schema of the caller’s that %s builds along with its own', (_method, embed) => { + it('keeps Zod’s memoizer, so it still parses a cyclic input', () => { + type Node = { kind: 'node'; next?: Node }; + const node: realZ.ZodType = z.object({ + kind: z.literal('node'), + get next(): realZ.ZodOptional> { + return node.optional(); + }, + }); + embed(node).safeParse({ kind: 'node' }); + + const cyclic: Node = { kind: 'node' }; + cyclic.next = cyclic; + const parsed = node.parse(cyclic); + expect(parsed.next).toBe(parsed); + }); + }); + + it('keeps Zod’s memoizer for a schema from another copy of lazy-zod, built along with its own', () => { + type Node = { kind: 'node'; next?: Node }; + let otherZ: Namespace = z; + jest.isolateModules(() => { + otherZ = jest.requireActual('../index').z; + }); + expect(otherZ).not.toBe(z); + const node: realZ.ZodType = otherZ.object({ + kind: otherZ.literal('node'), + get next(): realZ.ZodOptional> { + return node.optional(); + }, + }); + internalZ.record(internalZ.string(), node).safeParse({}); + + const cyclic: Node = { kind: 'node' }; + cyclic.next = cyclic; + const parsed = node.parse(cyclic); + expect(parsed.next).toBe(parsed); + }); + + it('builds around the memoizer Zod attaches to every container', () => { + realZ.object({}); + const memoizer = realZ.config().memoizer; + expect(memoizer).toBe(realZ.memoizer()); + expect(typeof memoizer?.attach).toBe('function'); + }); + + it('builds with whatever Zod does when Zod has no memoizer to swap', () => { + jest.isolateModules(() => { + jest.doMock('zod', () => { + const actual = jest.requireActual('zod'); + return { ...actual, z: { ...actual.z, memoizer: undefined, config: () => ({}) } }; + }); + const { internalZ: withoutMemoizer } = jest.requireActual('../internal-z'); + const schema = withoutMemoizer.object({ + name: withoutMemoizer.string(), + auth: withoutMemoizer.object({ issuer: withoutMemoizer.url() }).optional(), + }); + expect(schema.parse({ name: 'a' })).toEqual({ name: 'a' }); + expect(schema.safeParse({ name: 'a', auth: { issuer: 'x' } }).success).toBe(false); + }); + }); + it('answers introspection like the schema it stands for', () => { const base = internalZ.object({ kind: internalZ.literal('a'), size: internalZ.number().optional() }); const union = internalZ.discriminatedUnion('kind', [base, internalZ.object({ kind: internalZ.literal('b') })]); diff --git a/libs/lazy-zod/src/__tests__/materialization.spec.ts b/libs/lazy-zod/src/__tests__/materialization.spec.ts index b05b1de62..a53e85a17 100644 --- a/libs/lazy-zod/src/__tests__/materialization.spec.ts +++ b/libs/lazy-zod/src/__tests__/materialization.spec.ts @@ -1,5 +1,6 @@ import { z as realZ } from 'zod'; +import { toJSONSchema } from '../index'; import { LAZY_TARGET, lazyZ, type LazyZodSchema } from '../lazy-schema'; import { z } from '../lazy-z'; import { forceMaterialize, isLazy } from '../utils'; @@ -64,6 +65,13 @@ describe('materialization', () => { expect(real).toBeInstanceOf(realZ.ZodObject); }); + it('forceMaterialize leaves alone the catchall getter that .merge() defines', () => { + const merged = z.object({ a: z.string() }).merge(z.object({ b: z.number() })); + const eager = realZ.object({ a: realZ.string() }).merge(realZ.object({ b: realZ.number() })); + expect(toJSONSchema(merged)).toEqual(realZ.toJSONSchema(eager)); + expect(toJSONSchema(eager)).toEqual(realZ.toJSONSchema(eager)); + }); + it('forceMaterialize is a no-op for non-lazy schemas', () => { const raw = realZ.object({ a: realZ.string() }); expect(forceMaterialize(raw)).toBe(raw); diff --git a/libs/lazy-zod/src/internal-z.ts b/libs/lazy-zod/src/internal-z.ts index 3d7916cd6..fad0779ba 100644 --- a/libs/lazy-zod/src/internal-z.ts +++ b/libs/lazy-zod/src/internal-z.ts @@ -1,35 +1,13 @@ import { z as realZ, type ZodTypeAny } from 'zod'; -import type { $ZodMemoizer } from 'zod/v4/core'; import { HEAVY_FACTORIES, LEAF_FACTORIES } from './factories'; import { LAZY_TARGET, LazyZodSchema, wrapLazy } from './lazy-schema'; import { createLazyZ, type StandInFactory, type z } from './lazy-z'; +import { withoutRecursionWalk } from './memoizer'; import { isLazy } from './utils'; const BUILT_WITH_PARENT: unique symbol = Symbol('builtWithParent'); -const zodConfig = realZ.config(); -const defaultMemoizer = (realZ as { memoizer?: () => $ZodMemoizer }).memoizer; -const walklessMemoizers = new WeakMap<$ZodMemoizer, $ZodMemoizer>(); - -/** Zod's memoizer walks a container's whole subtree on its first parse, which would build every stand-in in it. */ -function withoutRecursionWalk(build: () => T): T { - const saved = zodConfig.memoizer; - const memoizer = saved ?? defaultMemoizer?.(); - if (!memoizer) return build(); - let walkless = walklessMemoizers.get(memoizer); - if (!walkless) { - walkless = Object.create(memoizer, { attach: { value: () => undefined } }) as $ZodMemoizer; - walklessMemoizers.set(memoizer, walkless); - } - zodConfig.memoizer = walkless; - try { - return build(); - } finally { - zodConfig.memoizer = saved; - } -} - class InternalLazySchema extends LazyZodSchema { readonly [BUILT_WITH_PARENT]: boolean; diff --git a/libs/lazy-zod/src/lazy-schema.ts b/libs/lazy-zod/src/lazy-schema.ts index 1055ec9c6..1f8e243af 100644 --- a/libs/lazy-zod/src/lazy-schema.ts +++ b/libs/lazy-zod/src/lazy-schema.ts @@ -1,6 +1,7 @@ import type { infer as zInfer, ZodTypeAny } from 'zod'; import { CHAIN_METHODS } from './factories'; +import { withRecursionWalk } from './memoizer'; /** * Branded symbol that identifies lazy schemas at runtime. @@ -47,7 +48,7 @@ export class LazyZodSchema { } protected build(factory: () => ZodTypeAny): ZodTypeAny { - return factory(); + return withRecursionWalk(factory); } /** True once the underlying schema has been constructed. */ diff --git a/libs/lazy-zod/src/memoizer.ts b/libs/lazy-zod/src/memoizer.ts new file mode 100644 index 000000000..8cc4c9f7f --- /dev/null +++ b/libs/lazy-zod/src/memoizer.ts @@ -0,0 +1,44 @@ +import { z as realZ } from 'zod'; +import type { $ZodMemoizer } from 'zod/v4/core'; + +/** A registry symbol, so every copy of lazy-zod in the process recognizes the others' walk-less memoizers. */ +const WALKLESS: unique symbol = Symbol.for('@frontmcp/lazy-zod.walklessMemoizer'); + +const zodConfig = realZ.config(); +const defaultMemoizer = (realZ as { memoizer?: () => $ZodMemoizer }).memoizer; +const walklessMemoizers = new WeakMap<$ZodMemoizer, $ZodMemoizer>(); + +function isWalkless(memoizer: $ZodMemoizer | undefined): memoizer is $ZodMemoizer { + return (memoizer as Record | undefined)?.[WALKLESS] === true; +} + +function buildWith(memoizer: $ZodMemoizer, build: () => T): T { + const saved = zodConfig.memoizer; + zodConfig.memoizer = memoizer; + try { + return build(); + } finally { + zodConfig.memoizer = saved; + } +} + +/** Zod's memoizer walks a container's whole subtree on its first parse, which would build every stand-in in it. */ +export function withoutRecursionWalk(build: () => T): T { + const memoizer = zodConfig.memoizer ?? defaultMemoizer?.(); + if (!memoizer || isWalkless(memoizer)) return build(); + let walkless = walklessMemoizers.get(memoizer); + if (!walkless) { + walkless = Object.create(memoizer, { + attach: { value: () => undefined }, + [WALKLESS]: { value: true }, + }) as $ZodMemoizer; + walklessMemoizers.set(memoizer, walkless); + } + return buildWith(walkless, build); +} + +/** Builds a schema that isn't FrontMCP's own, even inside a FrontMCP build, with Zod's memoizer and its cycle check. */ +export function withRecursionWalk(build: () => T): T { + const memoizer = zodConfig.memoizer; + return isWalkless(memoizer) ? buildWith(Object.getPrototypeOf(memoizer) as $ZodMemoizer, build) : build(); +} diff --git a/libs/lazy-zod/src/utils.ts b/libs/lazy-zod/src/utils.ts index 4c5c4ad09..e29ee201e 100644 --- a/libs/lazy-zod/src/utils.ts +++ b/libs/lazy-zod/src/utils.ts @@ -85,7 +85,8 @@ function deepMaterialize(value: unknown, seen: WeakSet): unknown { for (let i = 0; i < options.length; i++) options[i] = deepMaterialize(options[i], seen); } for (const key of CHILD_KEYS) { - if (key in def) def[key] = deepMaterialize(def[key], seen); + // `.merge()` defines `catchall` as a getter, which an assignment would throw on + if (key in def) Reflect.set(def, key, deepMaterialize(def[key], seen)); } const items = def['items']; if (Array.isArray(items)) { From df6d75857d93dc213b013a905584000a0ae84919 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Mon, 2 Nov 2026 18:18:43 +0200 Subject: [PATCH 5/5] test(sdk): compare exported schemas on cyclic inputs and on what consumers derive from them - A recursive exported schema also gets a sample that refers back to itself, so one moved to internalZ, which builds without Zod's cycle support, fails the spec. - Every exported schema is compared on what a consumer derives from it (.optional(), .array(), .or(), .extend(), .merge(), .partial(), .strict(), .pick(), .omit(), a spread shape, a discriminated union) and on what a consumer reads from it (type, class, instanceof, description, meta, optionality, shape, lazy-zod's and Zod's own toJSONSchema). --- .../internal-z-schema-parity.spec.ts | 155 ++++++++++++++++-- 1 file changed, 145 insertions(+), 10 deletions(-) diff --git a/libs/sdk/src/__tests__/internal-z-schema-parity.spec.ts b/libs/sdk/src/__tests__/internal-z-schema-parity.spec.ts index 58d932987..4bcb5fd3c 100644 --- a/libs/sdk/src/__tests__/internal-z-schema-parity.spec.ts +++ b/libs/sdk/src/__tests__/internal-z-schema-parity.spec.ts @@ -4,7 +4,7 @@ */ import 'reflect-metadata'; -import { isLazy } from '@frontmcp/lazy-zod'; +import { eagerZ, isLazy, toJSONSchema, z, ZodType } from '@frontmcp/lazy-zod'; type Exports = Record; type Def = Record & { type: string }; @@ -48,11 +48,14 @@ const STRING_FORMATS: Record = { datetime: '2020-01-01T00:00:00Z', }; -function sample(schema: unknown, depth: number): unknown { - if (depth > 6 || !isSchema(schema)) return undefined; +/** The objects a sample is still filling in, so a recursive schema's sample can refer back to one: a cyclic input. */ +type Cycles = { open: Map>; closed: number }; + +function sample(schema: unknown, depth: number, cycles?: Cycles): unknown { + if (depth > 10 || !isSchema(schema)) return undefined; const parser = schema as Parser; const def = parser._zod.def; - const child = (key: string): unknown => sample(def[key], depth + 1); + const child = (key: string): unknown => sample(def[key], depth + 1, cycles); switch (def.type) { case 'string': return STRING_FORMATS[parser.format ?? ''] ?? 'sample'.padEnd(parser.minLength ?? 0, 'x'); @@ -77,15 +80,13 @@ function sample(schema: unknown, depth: number): unknown { case 'array': return [child('element')]; case 'object': - return Object.fromEntries( - Object.entries(def['shape'] as Record).map(([key, value]) => [key, sample(value, depth + 1)]), - ); + return sampleObject(parser, depth, cycles); case 'record': return { key: child('valueType') }; case 'tuple': - return (def['items'] as unknown[]).map((item) => sample(item, depth + 1)); + return (def['items'] as unknown[]).map((item) => sample(item, depth + 1, cycles)); case 'union': - return sample((def['options'] as unknown[])[0], depth + 1); + return sampleUnion(def['options'] as unknown[], depth, cycles); case 'intersection': return { ...(child('left') as object), ...(child('right') as object) }; case 'optional': @@ -99,7 +100,7 @@ function sample(schema: unknown, depth: number): unknown { case 'pipe': return child('in'); case 'lazy': - return sample(parser._zod.innerType, depth + 1); + return sample(parser._zod.innerType, depth + 1, cycles); case 'function': return () => undefined; default: @@ -107,6 +108,37 @@ function sample(schema: unknown, depth: number): unknown { } } +function sampleObject(parser: Parser, depth: number, cycles: Cycles | undefined): Record { + const open = cycles?.open.get(parser._zod); + if (cycles && open) { + cycles.closed++; + return open; + } + const value: Record = {}; + cycles?.open.set(parser._zod, value); + for (const [key, field] of Object.entries(parser._zod.def['shape'] as Record)) { + value[key] = sample(field, depth + 1, cycles); + } + cycles?.open.delete(parser._zod); + return value; +} + +function sampleUnion(options: unknown[], depth: number, cycles: Cycles | undefined): unknown { + for (const option of cycles ? options : []) { + const closedBefore = cycles?.closed ?? 0; + const value = sample(option, depth + 1, cycles); + if ((cycles?.closed ?? 0) > closedBefore) return value; + } + return sample(options[0], depth + 1, cycles); +} + +/** A sample of a recursive schema that refers back to itself, or `undefined` for a schema with no cycle to close. */ +function cyclicSample(schema: Parser): unknown { + const cycles: Cycles = { open: new Map(), closed: 0 }; + const value = sample(schema, 0, cycles); + return cycles.closed > 0 ? value : undefined; +} + function inputsFor(schema: Parser): unknown[] { const base = sample(schema, 0); const inputs: unknown[] = [base, undefined, null, 'text', 42, true, {}, []]; @@ -116,6 +148,8 @@ function inputsFor(schema: Parser): unknown[] { inputs.push(rest, { ...base, [key]: typeof value === 'string' ? 42 : 'wrong' }); } } + const cyclic = cyclicSample(schema); + if (cyclic !== undefined) inputs.push(cyclic); return inputs; } @@ -142,6 +176,68 @@ async function outcome(schema: Parser, input: unknown): Promise { } } +function attempt(read: () => unknown): unknown { + try { + return { value: comparable(read()) }; + } catch (error) { + return { threw: (error as Error).message }; + } +} + +type Derived = Record Parser>; + +/** What a consumer builds from an exported schema: the derived schemas must validate like the ones `main` derives. */ +function derivedFrom(schema: Parser): Derived { + const derived: Derived = { + 'optional()': () => (schema as unknown as eagerZ.ZodType).optional() as unknown as Parser, + 'array()': () => (schema as unknown as eagerZ.ZodType).array() as unknown as Parser, + 'or()': () => (schema as unknown as eagerZ.ZodType).or(z.null()) as unknown as Parser, + 'z.object({ field })': () => z.object({ field: schema as unknown as eagerZ.ZodType }) as unknown as Parser, + }; + if (schema._zod.def.type !== 'object') return derived; + const object = schema as unknown as eagerZ.ZodObject; + const [firstKey] = Object.keys(object.shape); + const extra = { extra: z.string().optional() }; + const objectDerived: Derived = { + 'extend()': () => object.extend(extra) as unknown as Parser, + 'merge()': () => object.merge(z.object(extra)) as unknown as Parser, + 'partial()': () => object.partial() as unknown as Parser, + 'strict()': () => object.strict() as unknown as Parser, + 'z.object({ ...shape })': () => z.object({ ...object.shape, ...extra }) as unknown as Parser, + }; + if (firstKey !== undefined) { + objectDerived['pick()'] = () => object.pick({ [firstKey]: true }) as unknown as Parser; + objectDerived['omit()'] = () => object.omit({ [firstKey]: true }) as unknown as Parser; + } + const discriminator = Object.keys(object.shape).find((key) => object.shape[key]._zod.def.type === 'literal'); + if (discriminator !== undefined) { + objectDerived['z.discriminatedUnion()'] = () => + z.discriminatedUnion(discriminator, [ + object, + z.object({ [discriminator]: z.literal('another option') }), + ]) as unknown as Parser; + } + return { ...derived, ...objectDerived }; +} + +/** What a consumer reads from an exported schema without parsing with it. */ +function introspection(schema: Parser): Record { + const zodSchema = schema as unknown as eagerZ.ZodType; + const shape = schema._zod.def.type === 'object' ? (schema as unknown as eagerZ.ZodObject).shape : undefined; + return { + type: attempt(() => schema._zod.def.type), + className: attempt(() => zodSchema.constructor.name), + isZodType: attempt(() => schema instanceof ZodType), + description: attempt(() => zodSchema.description), + meta: attempt(() => zodSchema.meta()), + isOptional: attempt(() => zodSchema.isOptional()), + isNullable: attempt(() => zodSchema.isNullable()), + shape: attempt(() => shape && Object.entries(shape).map(([key, field]) => [key, field._zod.def.type])), + jsonSchema: attempt(() => toJSONSchema(zodSchema)), + inputJsonSchema: attempt(() => toJSONSchema(zodSchema, { io: 'input' })), + }; +} + const cases = ENTRY_POINTS.flatMap((entry) => Object.keys(previous[entry]) .filter((name) => isSchema(previous[entry][name])) @@ -167,4 +263,43 @@ describe('exported schemas built with internalZ', () => { expect(await outcome(after, input)).toEqual(await outcome(before, input)); } }); + + it('also gives each recursive schema a cyclic input, which only Zod’s memoizer parses', () => { + const recursive = cases.filter(({ entry, name }) => cyclicSample(previous[entry][name] as Parser) !== undefined); + expect(recursive.map(({ name }) => name)).toContain('authoritiesPolicySchema'); + }); + + it.each(cases)('$entry $name composes and converts like before', async ({ entry, name }) => { + const before = previous[entry][name] as Parser; + const after = current[entry][name] as Parser; + expect(introspection(after)).toEqual(introspection(before)); + + const zodJsonSchema = (schema: Parser) => attempt(() => eagerZ.toJSONSchema(schema as unknown as eagerZ.ZodType)); + const zodBefore = zodJsonSchema(before); + if (!('threw' in (zodBefore as object))) expect(zodJsonSchema(after)).toEqual(zodBefore); + + const derivedBefore = derivedFrom(before); + const derivedAfter = derivedFrom(after); + expect(Object.keys(derivedAfter)).toEqual(Object.keys(derivedBefore)); + const inputs = inputsFor(before); + for (const [label, derive] of Object.entries(derivedBefore)) { + const derivedOutcome = async (make: () => Parser, input: unknown) => { + try { + return await outcome(make(), input); + } catch (error) { + return { threw: (error as Error).message }; + } + }; + for (const input of [...inputs, [inputs[0]], { field: inputs[0] }]) { + expect({ label, outcome: await derivedOutcome(derivedAfter[label], input) }).toEqual({ + label, + outcome: await derivedOutcome(derive, input), + }); + } + expect({ label, jsonSchema: attempt(() => toJSONSchema(derivedAfter[label]() as never)) }).toEqual({ + label, + jsonSchema: attempt(() => toJSONSchema(derive() as never)), + }); + } + }); });