@veryfront/ext-schema-zod 0.1.1185 → 0.1.1189

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,6 +78,30 @@ register("SchemaValidator", createZodAdapter());
78
78
 
79
79
  `toJsonSchema(schema)` converts any `Schema<T>` to a JSON Schema object for OpenAPI or tool-call definitions.
80
80
 
81
+ `compileJsonSchema(schema)` compiles raw JSON Schema with strict validation,
82
+ standard formats, and no coercion, default insertion, or removal of additional
83
+ properties.
84
+
85
+ | Boundary | Limit |
86
+ | --------------------------------- | --------------------------------------------- |
87
+ | Schema depth | 64 levels |
88
+ | Schema nodes | 8,192 |
89
+ | Serialized schema | 512 KiB |
90
+ | Schema string | 256 KiB |
91
+ | Schema property name | 4 KiB UTF-8 |
92
+ | Code-generating collection fanout | 256 entries |
93
+ | Nested compilation work | 24,000 units |
94
+ | Validation input | 128 levels, 100,000 nodes, 4 MiB serialized |
95
+ | Compiled-validator cache | 128 entries and 2 MiB aggregate source weight |
96
+
97
+ Schemas and inputs must be data-only JSON. Accessors, symbol or hidden keys,
98
+ custom object prototypes, cycles, sparse arrays, and revoked proxies are
99
+ rejected without invoking property getters, setters, iterators, or `toJSON`.
100
+ Inspecting a Proxy necessarily invokes its reflection traps; a trap may run or
101
+ throw before the value is rejected. Successful validation returns the
102
+ adapter-owned input snapshot. Cache eviction is least-recently used when either
103
+ cache limit is exceeded.
104
+
81
105
  ## Running Tests
82
106
 
83
107
  ```sh
package/esm/adapter.d.ts CHANGED
@@ -13,10 +13,12 @@ import type { SchemaValidator } from "veryfront/extensions/schema";
13
13
  /**
14
14
  * Build a zod-backed `SchemaValidator` instance.
15
15
  *
16
- * Stateless safe to call once at extension setup and pass the returned
17
- * value to `ctx.provide("SchemaValidator", …)`. Tests that need to register
18
- * the validator without going through full extension bootstrap can call this
19
- * factory directly.
16
+ * Adapter instances snapshot schemas into plain JSON and retain validators in
17
+ * an entry- and weight-bounded LRU cache. Each unique validator owns an isolated
18
+ * Ajv compiler, so unrelated `$id` values cannot collide or accumulate in a
19
+ * process-wide registry. It is therefore safe to call this once at extension setup and pass the returned value to
20
+ * `ctx.provide("SchemaValidator", …)`. Tests that need to register the
21
+ * validator without full extension bootstrap can call this factory directly.
20
22
  */
21
23
  export declare function createZodAdapter(): SchemaValidator;
22
24
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAKV,eAAe,EAIhB,MAAM,6BAA6B,CAAC;AAiHrC;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,IAAI,eAAe,CA8FlD"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,OAAO,KAAK,EASV,eAAe,EAIhB,MAAM,6BAA6B,CAAC;AA4vBrC;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,IAAI,eAAe,CAiGlD"}
package/esm/adapter.js CHANGED
@@ -9,8 +9,15 @@
9
9
  *
10
10
  * @module extensions/ext-schema-zod/adapter
11
11
  */
12
+ import { Ajv as AjvDraft7 } from "ajv";
13
+ import { Ajv2019 } from "ajv/dist/2019.js";
14
+ import { Ajv2020 } from "ajv/dist/2020.js";
15
+ import addFormatsModule from "ajv-formats";
12
16
  import { z } from "zod";
13
- import { isOptionalSchema, zodToJsonSchema } from "./json-schema.js";
17
+ import { isOptionalSchema, recordStaticJsonSchemaDefault, zodToJsonSchema } from "./json-schema.js";
18
+ // Deno exposes the CommonJS default as a callable value at runtime while the
19
+ // package declaration describes the imported value as a module namespace.
20
+ const addFormats = addFormatsModule;
14
21
  /** Unwrap our opaque Schema<T> back to the underlying zod schema. */
15
22
  function toZod(schema) {
16
23
  return schema.__zod;
@@ -27,7 +34,13 @@ function wrap(zs) {
27
34
  optional: () => wrap(zs.optional()),
28
35
  nullable: () => wrap(zs.nullable()),
29
36
  nullish: () => wrap(zs.nullish()),
30
- default: (value) => wrap(anyZs.default(value)),
37
+ default: (value) => {
38
+ const defaulted = anyZs.default(value);
39
+ if (typeof value !== "function") {
40
+ recordStaticJsonSchemaDefault(defaulted, value);
41
+ }
42
+ return wrap(defaulted);
43
+ },
31
44
  describe: (description) => wrap(zs.describe(description)),
32
45
  refine: (check, message) => wrap(zs.refine(check, message)),
33
46
  superRefine: (check) => wrap(
@@ -94,25 +107,545 @@ function wrap(zs) {
94
107
  function toZodShape(shape) {
95
108
  const out = {};
96
109
  for (const [key, value] of Object.entries(shape)) {
97
- out[key] = toZod(value);
110
+ defineOwnDataProperty(out, key, toZod(value));
98
111
  }
99
112
  return out;
100
113
  }
114
+ function defineOwnDataProperty(target, key, value) {
115
+ // A data descriptor preserves keys such as `__proto__` as ordinary data
116
+ // without invoking inherited setters in runtimes that expose them.
117
+ Object.defineProperty(target, key, {
118
+ value,
119
+ enumerable: true,
120
+ configurable: true,
121
+ writable: true,
122
+ });
123
+ }
101
124
  const coerce = {
102
125
  string: () => wrap(z.coerce.string()),
103
126
  number: () => wrap(z.coerce.number()),
104
127
  boolean: () => wrap(z.coerce.boolean()),
105
128
  date: () => wrap(z.coerce.date()),
106
129
  };
130
+ const JSON_SCHEMA_VALIDATOR_CACHE_SIZE = 128;
131
+ const JSON_SCHEMA_VALIDATOR_CACHE_MAX_WEIGHT = 2 * 1024 * 1024;
132
+ // Raw schemas can originate in extensions and tool metadata, so snapshot them
133
+ // through a deliberately bounded JSON boundary before handing them to Ajv.
134
+ // These ceilings are comfortably above practical tool schemas while keeping a
135
+ // single compilation from consuming unbounded stack, CPU, or memory.
136
+ const JSON_SCHEMA_MAX_DEPTH = 64;
137
+ const JSON_SCHEMA_MAX_NODES = 8_192;
138
+ const JSON_SCHEMA_MAX_SERIALIZED_BYTES = 512 * 1024;
139
+ const JSON_SCHEMA_MAX_STRING_BYTES = 256 * 1024;
140
+ const JSON_SCHEMA_MAX_KEY_BYTES = 4 * 1024;
141
+ const JSON_SCHEMA_MAX_COMBINATOR_FANOUT = 256;
142
+ const JSON_SCHEMA_MAX_COMPILATION_WORK = 24_000;
143
+ const JSON_INSTANCE_MAX_DEPTH = 128;
144
+ const JSON_INSTANCE_MAX_NODES = 100_000;
145
+ const JSON_INSTANCE_MAX_SERIALIZED_BYTES = 4 * 1024 * 1024;
146
+ const JSON_INSTANCE_MAX_STRING_BYTES = 1024 * 1024;
147
+ const JSON_INSTANCE_MAX_KEY_BYTES = 16 * 1024;
148
+ const JSON_UTF8_ENCODER = new TextEncoder();
149
+ const JSON_SCHEMA_LIMITS = {
150
+ maxDepth: JSON_SCHEMA_MAX_DEPTH,
151
+ maxNodes: JSON_SCHEMA_MAX_NODES,
152
+ maxSerializedBytes: JSON_SCHEMA_MAX_SERIALIZED_BYTES,
153
+ maxStringBytes: JSON_SCHEMA_MAX_STRING_BYTES,
154
+ maxKeyBytes: JSON_SCHEMA_MAX_KEY_BYTES,
155
+ };
156
+ const JSON_INSTANCE_LIMITS = {
157
+ maxDepth: JSON_INSTANCE_MAX_DEPTH,
158
+ maxNodes: JSON_INSTANCE_MAX_NODES,
159
+ maxSerializedBytes: JSON_INSTANCE_MAX_SERIALIZED_BYTES,
160
+ maxStringBytes: JSON_INSTANCE_MAX_STRING_BYTES,
161
+ maxKeyBytes: JSON_INSTANCE_MAX_KEY_BYTES,
162
+ };
163
+ const JSON_SCHEMA_COMPILER_OPTIONS = {
164
+ strict: true,
165
+ allErrors: true,
166
+ addUsedSchema: false,
167
+ allowUnionTypes: true,
168
+ coerceTypes: false,
169
+ ownProperties: true,
170
+ useDefaults: false,
171
+ removeAdditional: false,
172
+ };
173
+ const DRAFT_7_META_SCHEMA_URI = "https://json-schema.org/draft-07/schema";
174
+ const AJV_DRAFT_7_META_SCHEMA_URI = "http://json-schema.org/draft-07/schema#";
175
+ function normalizeMetaSchemaUri(uri) {
176
+ return uri.trim().replace(/#$/, "").replace(/^http:/, "https:");
177
+ }
178
+ function createJsonSchemaCompiler(schema) {
179
+ const declaredDraft = typeof schema.$schema === "string"
180
+ ? normalizeMetaSchemaUri(schema.$schema)
181
+ : "https://json-schema.org/draft/2020-12/schema";
182
+ const compiler = declaredDraft === DRAFT_7_META_SCHEMA_URI
183
+ ? new AjvDraft7(JSON_SCHEMA_COMPILER_OPTIONS)
184
+ : declaredDraft === "https://json-schema.org/draft/2019-09/schema"
185
+ ? new Ajv2019(JSON_SCHEMA_COMPILER_OPTIONS)
186
+ : declaredDraft === "https://json-schema.org/draft/2020-12/schema"
187
+ ? new Ajv2020(JSON_SCHEMA_COMPILER_OPTIONS)
188
+ : undefined;
189
+ if (!compiler) {
190
+ throw new Error(`Unsupported JSON Schema draft: ${schema.$schema}`);
191
+ }
192
+ addFormats(compiler);
193
+ return compiler;
194
+ }
195
+ function schemaForCompilation(schema) {
196
+ if (typeof schema.$schema !== "string" ||
197
+ normalizeMetaSchemaUri(schema.$schema) !== DRAFT_7_META_SCHEMA_URI) {
198
+ return schema;
199
+ }
200
+ // Ajv's Draft 7 compiler registers the canonical HTTP identifier. Compile
201
+ // an isolated root snapshot using that identifier while leaving the
202
+ // caller-owned and cache-key snapshots unchanged.
203
+ return {
204
+ ...schema,
205
+ $schema: AJV_DRAFT_7_META_SCHEMA_URI,
206
+ };
207
+ }
208
+ function boundedUtf8Length(value, limit, label) {
209
+ // Every UTF-16 code unit contributes at least one UTF-8 byte. This cheap
210
+ // guard avoids allocating an encoded copy once the value is already known
211
+ // to exceed the byte ceiling.
212
+ if (value.length > limit) {
213
+ throw new TypeError(`JSON Schema ${label} exceeds the ${limit}-byte limit`);
214
+ }
215
+ const byteLength = JSON_UTF8_ENCODER.encode(value).byteLength;
216
+ if (byteLength > limit) {
217
+ throw new TypeError(`JSON Schema ${label} exceeds the ${limit}-byte limit`);
218
+ }
219
+ return byteLength;
220
+ }
221
+ function serializedJsonTokenByteLength(value) {
222
+ const serialized = JSON.stringify(value);
223
+ if (serialized === undefined) {
224
+ throw new TypeError("JSON Schema must contain only JSON values");
225
+ }
226
+ return JSON_UTF8_ENCODER.encode(serialized).byteLength;
227
+ }
228
+ class JsonSchemaCanonicalizer {
229
+ limits;
230
+ activeAncestors = new Set();
231
+ stack = [];
232
+ canonicalRoot;
233
+ rootAssigned = false;
234
+ nodeCount = 0;
235
+ serializedBytes = 0;
236
+ constructor(limits) {
237
+ this.limits = limits;
238
+ }
239
+ canonicalize(value) {
240
+ this.stack.push({ kind: "visit", value, depth: 0 });
241
+ while (this.stack.length > 0) {
242
+ const frame = this.stack.pop();
243
+ if (!frame)
244
+ break;
245
+ if (frame.kind === "exit") {
246
+ this.activeAncestors.delete(frame.value);
247
+ }
248
+ else {
249
+ this.visit(frame);
250
+ }
251
+ }
252
+ if (!this.rootAssigned) {
253
+ throw new TypeError("JSON Schema must contain a JSON value");
254
+ }
255
+ return {
256
+ value: this.canonicalRoot,
257
+ nodeCount: this.nodeCount,
258
+ serializedBytes: this.serializedBytes,
259
+ };
260
+ }
261
+ visit(frame) {
262
+ this.consumeNode(frame.depth);
263
+ if (this.visitScalar(frame))
264
+ return;
265
+ const current = frame.value;
266
+ if (typeof current !== "object" || current === null) {
267
+ throw new TypeError("JSON Schema must contain only JSON values");
268
+ }
269
+ if (this.activeAncestors.has(current)) {
270
+ throw new TypeError("JSON Schema must not contain cycles");
271
+ }
272
+ if (Array.isArray(current)) {
273
+ this.visitArray(frame, current);
274
+ }
275
+ else {
276
+ this.visitObject(frame, current);
277
+ }
278
+ }
279
+ visitScalar(frame) {
280
+ const current = frame.value;
281
+ if (current === null || typeof current === "boolean") {
282
+ this.assign(frame, current);
283
+ this.addSerializedBytes(serializedJsonTokenByteLength(current));
284
+ return true;
285
+ }
286
+ if (typeof current === "string") {
287
+ boundedUtf8Length(current, this.limits.maxStringBytes, "string");
288
+ this.assign(frame, current);
289
+ this.addSerializedBytes(serializedJsonTokenByteLength(current));
290
+ return true;
291
+ }
292
+ if (typeof current === "number") {
293
+ if (!Number.isFinite(current)) {
294
+ throw new TypeError("JSON Schema numbers must be finite");
295
+ }
296
+ this.assign(frame, current);
297
+ this.addSerializedBytes(serializedJsonTokenByteLength(current));
298
+ return true;
299
+ }
300
+ if (typeof current !== "object") {
301
+ throw new TypeError("JSON Schema must contain only JSON values");
302
+ }
303
+ return false;
304
+ }
305
+ visitArray(frame, current) {
306
+ const lengthDescriptor = Reflect.getOwnPropertyDescriptor(current, "length");
307
+ const length = lengthDescriptor && "value" in lengthDescriptor
308
+ ? lengthDescriptor.value
309
+ : undefined;
310
+ if (typeof length !== "number" ||
311
+ !Number.isSafeInteger(length) ||
312
+ length < 0) {
313
+ throw new TypeError("JSON Schema arrays must have a non-negative integer data length");
314
+ }
315
+ if (length > this.limits.maxNodes) {
316
+ throw new TypeError(`JSON Schema exceeds the maximum node count of ${this.limits.maxNodes}`);
317
+ }
318
+ const ownKeys = Reflect.ownKeys(current);
319
+ if (ownKeys.some((key) => typeof key === "symbol") ||
320
+ ownKeys.length !== length + 1 ||
321
+ !ownKeys.includes("length")) {
322
+ throw new TypeError("JSON Schema arrays must be dense JSON arrays without extra properties");
323
+ }
324
+ this.addSerializedBytes(2 + Math.max(0, length - 1));
325
+ const descriptors = [];
326
+ for (let index = 0; index < length; index++) {
327
+ const descriptor = Reflect.getOwnPropertyDescriptor(current, String(index));
328
+ if (!descriptor || !("value" in descriptor) || descriptor.enumerable !== true) {
329
+ throw new TypeError("JSON Schema arrays must be dense data-only JSON arrays without accessors");
330
+ }
331
+ descriptors.push(descriptor);
332
+ }
333
+ const canonical = new Array(length);
334
+ this.assign(frame, canonical);
335
+ this.activeAncestors.add(current);
336
+ this.stack.push({ kind: "exit", value: current });
337
+ for (let index = descriptors.length - 1; index >= 0; index--) {
338
+ const descriptor = descriptors[index];
339
+ if (!descriptor) {
340
+ throw new TypeError("JSON Schema array snapshot is internally inconsistent");
341
+ }
342
+ this.stack.push({
343
+ kind: "visit",
344
+ value: descriptor.value,
345
+ depth: frame.depth + 1,
346
+ parent: canonical,
347
+ key: index,
348
+ });
349
+ }
350
+ }
351
+ visitObject(frame, current) {
352
+ const prototype = Object.getPrototypeOf(current);
353
+ if (prototype !== Object.prototype && prototype !== null) {
354
+ throw new TypeError("JSON Schema objects must be plain JSON objects");
355
+ }
356
+ const ownKeys = Reflect.ownKeys(current);
357
+ if (ownKeys.length > this.limits.maxNodes) {
358
+ throw new TypeError(`JSON Schema exceeds the maximum node count of ${this.limits.maxNodes}`);
359
+ }
360
+ if (ownKeys.some((key) => typeof key === "symbol")) {
361
+ throw new TypeError("JSON Schema objects must not contain symbol keys");
362
+ }
363
+ this.addSerializedBytes(2 + Math.max(0, ownKeys.length - 1));
364
+ const entries = ownKeys.map((key) => {
365
+ boundedUtf8Length(key, this.limits.maxKeyBytes, "key");
366
+ this.addSerializedBytes(serializedJsonTokenByteLength(key) + 1);
367
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
368
+ if (!descriptor || !("value" in descriptor)) {
369
+ throw new TypeError("JSON Schema objects must be data-only and must not contain accessors");
370
+ }
371
+ if (descriptor.enumerable !== true) {
372
+ throw new TypeError("JSON Schema objects must not contain non-enumerable properties");
373
+ }
374
+ return { key, value: descriptor.value };
375
+ }).sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0);
376
+ const canonical = {};
377
+ this.assign(frame, canonical);
378
+ this.activeAncestors.add(current);
379
+ this.stack.push({ kind: "exit", value: current });
380
+ for (let index = entries.length - 1; index >= 0; index--) {
381
+ const entry = entries[index];
382
+ if (!entry) {
383
+ throw new TypeError("JSON Schema object snapshot is internally inconsistent");
384
+ }
385
+ this.stack.push({
386
+ kind: "visit",
387
+ value: entry.value,
388
+ depth: frame.depth + 1,
389
+ parent: canonical,
390
+ key: entry.key,
391
+ });
392
+ }
393
+ }
394
+ consumeNode(depth) {
395
+ if (depth > this.limits.maxDepth) {
396
+ throw new TypeError(`JSON Schema exceeds the maximum depth of ${this.limits.maxDepth}`);
397
+ }
398
+ this.nodeCount++;
399
+ if (this.nodeCount > this.limits.maxNodes) {
400
+ throw new TypeError(`JSON Schema exceeds the maximum node count of ${this.limits.maxNodes}`);
401
+ }
402
+ }
403
+ addSerializedBytes(amount) {
404
+ this.serializedBytes += amount;
405
+ if (this.serializedBytes > this.limits.maxSerializedBytes) {
406
+ throw new TypeError(`JSON Schema exceeds the ${this.limits.maxSerializedBytes}-byte serialized limit`);
407
+ }
408
+ }
409
+ assign(frame, canonical) {
410
+ if (frame.parent === undefined) {
411
+ this.canonicalRoot = canonical;
412
+ this.rootAssigned = true;
413
+ return;
414
+ }
415
+ if (Array.isArray(frame.parent)) {
416
+ frame.parent[frame.key] = canonical;
417
+ return;
418
+ }
419
+ defineOwnDataProperty(frame.parent, frame.key, canonical);
420
+ }
421
+ }
422
+ function canonicalizeJsonValue(value, limits) {
423
+ return new JsonSchemaCanonicalizer(limits).canonicalize(value);
424
+ }
425
+ const COMBINATOR_KEYWORDS = new Set(["allOf", "anyOf", "oneOf"]);
426
+ const CODE_GENERATING_MAP_KEYWORDS = new Map([
427
+ ["properties", 4],
428
+ ["patternProperties", 8],
429
+ ["dependencies", 4],
430
+ ["dependentRequired", 4],
431
+ ["dependentSchemas", 4],
432
+ ]);
433
+ const RETAINED_SCHEMA_MAP_KEYWORDS = new Set(["$defs", "definitions"]);
434
+ function isJsonObject(value) {
435
+ return value !== null && typeof value === "object" && !Array.isArray(value);
436
+ }
437
+ /**
438
+ * Estimate generated-validator work before Ajv sees a schema. Node and byte
439
+ * limits alone do not capture wide combinators, large enums, or property maps,
440
+ * all of which expand generated code and retained compiler state.
441
+ */
442
+ function estimateCompilationWork(schema) {
443
+ const stack = [schema];
444
+ let work = 0;
445
+ const addWork = (amount) => {
446
+ work += amount;
447
+ if (work > JSON_SCHEMA_MAX_COMPILATION_WORK) {
448
+ throw new TypeError(`JSON Schema exceeds the compilation work limit of ${JSON_SCHEMA_MAX_COMPILATION_WORK}`);
449
+ }
450
+ };
451
+ const addBoundedCollectionWork = (keyword, length, multiplier) => {
452
+ if (length > JSON_SCHEMA_MAX_COMBINATOR_FANOUT) {
453
+ throw new TypeError(`JSON Schema ${keyword} exceeds the code-generation collection limit of ${JSON_SCHEMA_MAX_COMBINATOR_FANOUT}`);
454
+ }
455
+ addWork(length * multiplier);
456
+ };
457
+ while (stack.length > 0) {
458
+ const value = stack.pop();
459
+ if (!value || typeof value !== "object")
460
+ continue;
461
+ if (Array.isArray(value)) {
462
+ for (let index = value.length - 1; index >= 0; index--)
463
+ stack.push(value[index]);
464
+ continue;
465
+ }
466
+ const schemaObject = value;
467
+ const propertyCount = isJsonObject(schemaObject.properties)
468
+ ? Object.keys(schemaObject.properties).length
469
+ : 0;
470
+ const patternPropertyCount = isJsonObject(schemaObject.patternProperties)
471
+ ? Object.keys(schemaObject.patternProperties).length
472
+ : 0;
473
+ if (propertyCount > 0 && patternPropertyCount > 0) {
474
+ // In Ajv strict mode every pattern is checked against every named
475
+ // property during compilation, so account for the cross-product.
476
+ addWork(propertyCount * patternPropertyCount);
477
+ }
478
+ for (const [keyword, keywordValue] of Object.entries(schemaObject)) {
479
+ addWork(1);
480
+ if (COMBINATOR_KEYWORDS.has(keyword) && Array.isArray(keywordValue)) {
481
+ addBoundedCollectionWork(keyword, keywordValue.length, 8);
482
+ }
483
+ else if ((keyword === "prefixItems" || keyword === "items") && Array.isArray(keywordValue)) {
484
+ addBoundedCollectionWork(keyword, keywordValue.length, 2);
485
+ }
486
+ else if ((keyword === "enum" || keyword === "required" || keyword === "type") &&
487
+ Array.isArray(keywordValue)) {
488
+ addBoundedCollectionWork(keyword, keywordValue.length, keyword === "required" ? 4 : 2);
489
+ }
490
+ else if (isJsonObject(keywordValue) && CODE_GENERATING_MAP_KEYWORDS.has(keyword)) {
491
+ const entryCount = Object.keys(keywordValue).length;
492
+ addBoundedCollectionWork(keyword, entryCount, CODE_GENERATING_MAP_KEYWORDS.get(keyword));
493
+ if (keyword === "dependentRequired" || keyword === "dependencies") {
494
+ for (const dependencyValue of Object.values(keywordValue)) {
495
+ if (Array.isArray(dependencyValue)) {
496
+ addBoundedCollectionWork(`${keyword} entry`, dependencyValue.length, 4);
497
+ }
498
+ }
499
+ }
500
+ }
501
+ else if (isJsonObject(keywordValue) && RETAINED_SCHEMA_MAP_KEYWORDS.has(keyword)) {
502
+ addWork(Object.keys(keywordValue).length * 2);
503
+ }
504
+ else if (keyword === "pattern" && typeof keywordValue === "string") {
505
+ addWork(Math.ceil(keywordValue.length / 8));
506
+ }
507
+ stack.push(keywordValue);
508
+ }
509
+ }
510
+ return work;
511
+ }
512
+ function snapshotJsonSchema(schema) {
513
+ const snapshot = canonicalizeJsonValue(schema, JSON_SCHEMA_LIMITS);
514
+ const canonical = snapshot.value;
515
+ if (canonical === null || typeof canonical !== "object" || Array.isArray(canonical)) {
516
+ throw new TypeError("JSON Schema root must be a plain object");
517
+ }
518
+ const key = JSON.stringify(canonical);
519
+ if (key === undefined)
520
+ throw new TypeError("JSON Schema must be JSON serializable");
521
+ const compilationWork = estimateCompilationWork(canonical);
522
+ const cacheWeight = snapshot.serializedBytes + snapshot.nodeCount * 64 + compilationWork * 32;
523
+ return {
524
+ key,
525
+ schema: canonical,
526
+ nodeCount: snapshot.nodeCount,
527
+ serializedBytes: snapshot.serializedBytes,
528
+ compilationWork,
529
+ cacheWeight,
530
+ };
531
+ }
532
+ function copyValidationIssue(error) {
533
+ let params;
534
+ try {
535
+ params = structuredClone(error.params);
536
+ }
537
+ catch {
538
+ params = { ...error.params };
539
+ }
540
+ return {
541
+ instancePath: error.instancePath,
542
+ schemaPath: error.schemaPath,
543
+ keyword: error.keyword,
544
+ params,
545
+ ...(error.message === undefined ? {} : { message: error.message }),
546
+ };
547
+ }
548
+ function validationFailure(errors) {
549
+ return {
550
+ success: false,
551
+ errors: (errors ?? []).map(copyValidationIssue),
552
+ };
553
+ }
554
+ function validationSuccess(input) {
555
+ return { success: true, value: input };
556
+ }
557
+ const DATA_ONLY_INPUT_FAILURE = Object.freeze({
558
+ success: false,
559
+ errors: Object.freeze([
560
+ Object.freeze({
561
+ instancePath: "",
562
+ schemaPath: "",
563
+ keyword: "veryfrontDataOnly",
564
+ params: Object.freeze({}),
565
+ message: "Input must be a bounded, data-only JSON value",
566
+ }),
567
+ ]),
568
+ });
569
+ function snapshotJsonInstance(input) {
570
+ try {
571
+ return canonicalizeJsonValue(input, JSON_INSTANCE_LIMITS);
572
+ }
573
+ catch {
574
+ // Accessors, throwing/revoked Proxies, cycles, custom prototypes, and
575
+ // oversized inputs are ordinary validation failures at this boundary.
576
+ return undefined;
577
+ }
578
+ }
579
+ function errorObjectsFromUnknown(error) {
580
+ if (!error || typeof error !== "object" || !("errors" in error)) {
581
+ return undefined;
582
+ }
583
+ return Array.isArray(error.errors) ? error.errors : undefined;
584
+ }
585
+ function isPromiseLike(value) {
586
+ return !!value &&
587
+ (typeof value === "object" || typeof value === "function") &&
588
+ "then" in value &&
589
+ typeof value.then === "function";
590
+ }
591
+ function compileJsonSchemaValidator(schema) {
592
+ const validate = createJsonSchemaCompiler(schema).compile(schemaForCompilation(schema));
593
+ return (input) => {
594
+ const inputSnapshot = snapshotJsonInstance(input);
595
+ if (!inputSnapshot)
596
+ return DATA_ONLY_INPUT_FAILURE;
597
+ const acceptedInput = inputSnapshot.value;
598
+ const outcome = validate(acceptedInput);
599
+ if (isPromiseLike(outcome)) {
600
+ return Promise.resolve(outcome).then(() => validationSuccess(acceptedInput), (error) => {
601
+ const errors = errorObjectsFromUnknown(error);
602
+ if (errors)
603
+ return validationFailure(errors);
604
+ throw error;
605
+ });
606
+ }
607
+ return outcome ? validationSuccess(acceptedInput) : validationFailure(validate.errors);
608
+ };
609
+ }
610
+ function createJsonSchemaCompilationCache() {
611
+ const cache = new Map();
612
+ let cacheWeight = 0;
613
+ return (schema) => {
614
+ const snapshot = snapshotJsonSchema(schema);
615
+ const cached = cache.get(snapshot.key);
616
+ if (cached) {
617
+ cache.delete(snapshot.key);
618
+ cache.set(snapshot.key, cached);
619
+ return cached.validator;
620
+ }
621
+ const compiled = compileJsonSchemaValidator(snapshot.schema);
622
+ while (cache.size >= JSON_SCHEMA_VALIDATOR_CACHE_SIZE ||
623
+ cacheWeight + snapshot.cacheWeight > JSON_SCHEMA_VALIDATOR_CACHE_MAX_WEIGHT) {
624
+ const oldestKey = cache.keys().next().value;
625
+ if (oldestKey === undefined)
626
+ break;
627
+ const oldest = cache.get(oldestKey);
628
+ cache.delete(oldestKey);
629
+ if (oldest)
630
+ cacheWeight -= oldest.weight;
631
+ }
632
+ cache.set(snapshot.key, { validator: compiled, weight: snapshot.cacheWeight });
633
+ cacheWeight += snapshot.cacheWeight;
634
+ return compiled;
635
+ };
636
+ }
107
637
  /**
108
638
  * Build a zod-backed `SchemaValidator` instance.
109
639
  *
110
- * Stateless safe to call once at extension setup and pass the returned
111
- * value to `ctx.provide("SchemaValidator", …)`. Tests that need to register
112
- * the validator without going through full extension bootstrap can call this
113
- * factory directly.
640
+ * Adapter instances snapshot schemas into plain JSON and retain validators in
641
+ * an entry- and weight-bounded LRU cache. Each unique validator owns an isolated
642
+ * Ajv compiler, so unrelated `$id` values cannot collide or accumulate in a
643
+ * process-wide registry. It is therefore safe to call this once at extension setup and pass the returned value to
644
+ * `ctx.provide("SchemaValidator", …)`. Tests that need to register the
645
+ * validator without full extension bootstrap can call this factory directly.
114
646
  */
115
647
  export function createZodAdapter() {
648
+ const compileJsonSchema = createJsonSchemaCompilationCache();
116
649
  return {
117
650
  string: () => wrap(z.string()),
118
651
  number: () => wrap(z.number()),
@@ -153,6 +686,7 @@ export function createZodAdapter() {
153
686
  custom: (check, message) => wrap(z.custom(check, message)),
154
687
  coerce,
155
688
  validate: (schema, data) => schema.safeParse(data),
689
+ compileJsonSchema,
156
690
  toJsonSchema: (schema) => zodToJsonSchema(toZod(schema)),
157
691
  isOptional: (schema) => isOptionalSchema(toZod(schema)),
158
692
  };
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Zod-to-JSON-Schema converter used by the `SchemaValidator` adapter.
3
3
  *
4
- * Operates directly on zod's internal `_def` shape (covers both v3 and v4)
5
- * because zod ships no first-class JSON-Schema export. Confined to
6
- * `extensions/ext-schema-zod/` so the rest of the codebase never has to import zod
4
+ * Operates directly on zod's internal `_def` shape to preserve the adapter's
5
+ * stable v3 and v4 compatibility behavior. It is confined to
6
+ * `extensions/ext-schema-zod/` so the rest of the codebase never imports zod
7
7
  * to learn what a tool's input schema looks like.
8
8
  *
9
9
  * @module extensions/ext-schema-zod/json-schema
10
10
  */
11
11
  import type { z } from "zod";
12
12
  import type { JsonSchema } from "veryfront/extensions/schema";
13
+ /** Record a literal default without evaluating Zod's possibly dynamic getter. */
14
+ export declare function recordStaticJsonSchemaDefault(schema: z.ZodTypeAny, value: unknown): void;
13
15
  export declare function zodToJsonSchema(schema: z.ZodTypeAny): JsonSchema;
14
16
  export declare function isOptionalSchema(schema: z.ZodTypeAny): boolean;
15
17
  //# sourceMappingURL=json-schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../src/json-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAgD9D,wBAAgB,eAAe,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,UAAU,CAEhE;AAgBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,OAAO,CAE9D"}
1
+ {"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../src/json-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAqO9D,iFAAiF;AACjF,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAOxF;AAmLD,wBAAgB,eAAe,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,UAAU,CAMhE;AAgBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,OAAO,CAE9D"}
@@ -1,21 +1,122 @@
1
1
  /**
2
2
  * Zod-to-JSON-Schema converter used by the `SchemaValidator` adapter.
3
3
  *
4
- * Operates directly on zod's internal `_def` shape (covers both v3 and v4)
5
- * because zod ships no first-class JSON-Schema export. Confined to
6
- * `extensions/ext-schema-zod/` so the rest of the codebase never has to import zod
4
+ * Operates directly on zod's internal `_def` shape to preserve the adapter's
5
+ * stable v3 and v4 compatibility behavior. It is confined to
6
+ * `extensions/ext-schema-zod/` so the rest of the codebase never imports zod
7
7
  * to learn what a tool's input schema looks like.
8
8
  *
9
9
  * @module extensions/ext-schema-zod/json-schema
10
10
  */
11
+ const MAX_CONVERSION_DEPTH = 128;
12
+ const MAX_CONVERSION_NODES = 100_000;
13
+ function assertConversionDepth(depth) {
14
+ if (depth > MAX_CONVERSION_DEPTH) {
15
+ throw new RangeError(`Zod schema exceeds the maximum conversion depth of ${MAX_CONVERSION_DEPTH}`);
16
+ }
17
+ }
18
+ const STATIC_DEFAULT_VALUE = Symbol("veryfront.staticJsonSchemaDefault");
11
19
  const LITERAL_TYPE_MAP = {
12
20
  string: "string",
13
21
  number: "number",
14
22
  boolean: "boolean",
15
23
  };
16
24
  function getLiteralType(value) {
25
+ if (value === null)
26
+ return "null";
17
27
  return LITERAL_TYPE_MAP[typeof value];
18
28
  }
29
+ function finiteStringRecordKeys(schema, context) {
30
+ const activeSchemas = new WeakSet();
31
+ const keys = [];
32
+ const stack = [{
33
+ kind: "visit",
34
+ schema,
35
+ depth: context.depth,
36
+ }];
37
+ let pendingVisitCount = 1;
38
+ let visitedNodes = 0;
39
+ const finish = (result) => {
40
+ context.nodeCount += visitedNodes;
41
+ return result;
42
+ };
43
+ const assertCapacity = (additionalNodes) => {
44
+ if (context.nodeCount +
45
+ visitedNodes +
46
+ pendingVisitCount +
47
+ additionalNodes >
48
+ MAX_CONVERSION_NODES) {
49
+ throw new RangeError(`Zod schema exceeds the maximum conversion node count of ${MAX_CONVERSION_NODES}`);
50
+ }
51
+ };
52
+ while (stack.length > 0) {
53
+ const frame = stack.pop();
54
+ if (!frame)
55
+ break;
56
+ if (frame.kind === "exit") {
57
+ activeSchemas.delete(frame.schema);
58
+ continue;
59
+ }
60
+ pendingVisitCount--;
61
+ assertConversionDepth(frame.depth);
62
+ visitedNodes++;
63
+ if (context.nodeCount + visitedNodes > MAX_CONVERSION_NODES) {
64
+ throw new RangeError(`Zod schema exceeds the maximum conversion node count of ${MAX_CONVERSION_NODES}`);
65
+ }
66
+ if (activeSchemas.has(frame.schema))
67
+ return finish(undefined);
68
+ const tag = getTypeTag(frame.schema);
69
+ const def = getDef(frame.schema);
70
+ if (tag === "ZodLiteral" || tag === "literal") {
71
+ const literal = def.value ?? (Array.isArray(def.values) ? def.values[0] : def.values);
72
+ if (typeof literal !== "string")
73
+ return finish(undefined);
74
+ keys.push(literal);
75
+ continue;
76
+ }
77
+ if (tag === "ZodEnum" || tag === "enum") {
78
+ const rawValues = Array.isArray(def.values)
79
+ ? def.values
80
+ : def.entries
81
+ ? Object.values(def.entries)
82
+ : [];
83
+ assertCapacity(rawValues.length);
84
+ for (const value of rawValues) {
85
+ visitedNodes++;
86
+ if (typeof value !== "string")
87
+ return finish(undefined);
88
+ keys.push(value);
89
+ }
90
+ continue;
91
+ }
92
+ if (tag !== "ZodUnion" &&
93
+ tag !== "ZodDiscriminatedUnion" &&
94
+ tag !== "union") {
95
+ return finish(undefined);
96
+ }
97
+ const options = def.options ?? [];
98
+ const optionCount = options instanceof Map ? options.size : options.length;
99
+ assertCapacity(optionCount);
100
+ if (optionCount > 0 && frame.depth + 1 > MAX_CONVERSION_DEPTH) {
101
+ throw new RangeError(`Zod schema exceeds the maximum conversion depth of ${MAX_CONVERSION_DEPTH}`);
102
+ }
103
+ const optionArray = options instanceof Map ? Array.from(options.values()) : options;
104
+ activeSchemas.add(frame.schema);
105
+ stack.push({ kind: "exit", schema: frame.schema });
106
+ pendingVisitCount += optionCount;
107
+ for (let index = optionArray.length - 1; index >= 0; index--) {
108
+ const option = optionArray[index];
109
+ if (!option)
110
+ return finish(undefined);
111
+ stack.push({
112
+ kind: "visit",
113
+ schema: option,
114
+ depth: frame.depth + 1,
115
+ });
116
+ }
117
+ }
118
+ return finish(Array.from(new Set(keys)));
119
+ }
19
120
  function getDef(schema) {
20
121
  return schema._def;
21
122
  }
@@ -24,8 +125,217 @@ function getTypeTag(schema) {
24
125
  const def = getDef(schema);
25
126
  return def.typeName ?? def.type;
26
127
  }
128
+ function originatesFromCustomSchema(schema) {
129
+ const seen = new WeakSet();
130
+ let current = schema;
131
+ let depth = 0;
132
+ while (!seen.has(current)) {
133
+ assertConversionDepth(depth);
134
+ seen.add(current);
135
+ const tag = getTypeTag(current);
136
+ if (tag === "custom" || tag === "ZodCustom")
137
+ return true;
138
+ if (tag !== "pipe" && tag !== "ZodEffects")
139
+ return false;
140
+ const def = getDef(current);
141
+ const input = def.schema ?? def.in;
142
+ if (!input || input === current)
143
+ return false;
144
+ current = input;
145
+ depth++;
146
+ }
147
+ return false;
148
+ }
149
+ function representedPipeSchema(def) {
150
+ const input = def.schema ?? def.in;
151
+ return input && originatesFromCustomSchema(input) && def.out ? def.out : input;
152
+ }
153
+ /** Record a literal default without evaluating Zod's possibly dynamic getter. */
154
+ export function recordStaticJsonSchemaDefault(schema, value) {
155
+ Object.defineProperty(getDef(schema), STATIC_DEFAULT_VALUE, {
156
+ configurable: true,
157
+ enumerable: true,
158
+ value: { value },
159
+ writable: false,
160
+ });
161
+ }
162
+ function getStaticJsonSchemaDefault(def) {
163
+ const marked = def[STATIC_DEFAULT_VALUE];
164
+ if (!marked || typeof marked !== "object" || !("value" in marked))
165
+ return undefined;
166
+ return marked;
167
+ }
168
+ function getCheckDefinitions(def) {
169
+ return (def.checks ?? []).flatMap((check) => {
170
+ if (!check || typeof check !== "object")
171
+ return [];
172
+ const internal = check._zod?.def;
173
+ const definition = internal ?? check;
174
+ return definition && typeof definition === "object" ? [definition] : [];
175
+ });
176
+ }
177
+ function finiteNumber(value) {
178
+ return typeof value === "number" && Number.isFinite(value);
179
+ }
180
+ function addConjunctiveConstraints(json, keyword, values) {
181
+ const uniqueValues = Array.from(new Set(values));
182
+ if (uniqueValues.length === 0)
183
+ return;
184
+ if (uniqueValues.length === 1) {
185
+ json[keyword] = uniqueValues[0];
186
+ return;
187
+ }
188
+ json.allOf = [
189
+ ...(json.allOf ?? []),
190
+ ...uniqueValues.map((value) => ({ [keyword]: value })),
191
+ ];
192
+ }
193
+ const STRING_FORMAT_MAP = {
194
+ email: "email",
195
+ url: "uri",
196
+ uuid: "uuid",
197
+ datetime: "date-time",
198
+ };
199
+ function convertString(def) {
200
+ const json = { type: "string" };
201
+ const patterns = [];
202
+ const formats = [];
203
+ for (const check of getCheckDefinitions(def)) {
204
+ if (check.check === "min_length" && finiteNumber(check.minimum)) {
205
+ json.minLength = Math.max(json.minLength ?? 0, check.minimum);
206
+ continue;
207
+ }
208
+ if (check.check === "max_length" && finiteNumber(check.maximum)) {
209
+ json.maxLength = Math.min(json.maxLength ?? Number.POSITIVE_INFINITY, check.maximum);
210
+ continue;
211
+ }
212
+ const format = check.check === "string_format" ? check.format : check.kind;
213
+ if (format === "min" && finiteNumber(check.value)) {
214
+ json.minLength = Math.max(json.minLength ?? 0, check.value);
215
+ }
216
+ else if (format === "max" && finiteNumber(check.value)) {
217
+ json.maxLength = Math.min(json.maxLength ?? Number.POSITIVE_INFINITY, check.value);
218
+ }
219
+ else if (format === "regex") {
220
+ const regex = check.pattern ?? check.regex;
221
+ if (regex && regex.flags.length === 0)
222
+ patterns.push(regex.source);
223
+ }
224
+ else if (format && STRING_FORMAT_MAP[format]) {
225
+ formats.push(STRING_FORMAT_MAP[format]);
226
+ }
227
+ }
228
+ addConjunctiveConstraints(json, "pattern", patterns);
229
+ addConjunctiveConstraints(json, "format", formats);
230
+ return json;
231
+ }
232
+ function tighterLowerBoundary(current, candidate) {
233
+ if (!current || candidate.value > current.value)
234
+ return candidate;
235
+ if (candidate.value < current.value)
236
+ return current;
237
+ return { value: current.value, exclusive: current.exclusive || candidate.exclusive };
238
+ }
239
+ function tighterUpperBoundary(current, candidate) {
240
+ if (!current || candidate.value < current.value)
241
+ return candidate;
242
+ if (candidate.value > current.value)
243
+ return current;
244
+ return { value: current.value, exclusive: current.exclusive || candidate.exclusive };
245
+ }
246
+ function convertNumber(def) {
247
+ let integer = false;
248
+ let lower;
249
+ let upper;
250
+ for (const check of getCheckDefinitions(def)) {
251
+ if ((check.check === "number_format" && check.format === "safeint") ||
252
+ check.kind === "int") {
253
+ integer = true;
254
+ lower = tighterLowerBoundary(lower, {
255
+ value: -Number.MAX_SAFE_INTEGER,
256
+ exclusive: false,
257
+ });
258
+ upper = tighterUpperBoundary(upper, {
259
+ value: Number.MAX_SAFE_INTEGER,
260
+ exclusive: false,
261
+ });
262
+ continue;
263
+ }
264
+ if (check.check === "greater_than" && finiteNumber(check.value)) {
265
+ lower = tighterLowerBoundary(lower, {
266
+ value: check.value,
267
+ exclusive: check.inclusive !== true,
268
+ });
269
+ }
270
+ else if (check.check === "less_than" && finiteNumber(check.value)) {
271
+ upper = tighterUpperBoundary(upper, {
272
+ value: check.value,
273
+ exclusive: check.inclusive !== true,
274
+ });
275
+ }
276
+ else if (check.kind === "min" && finiteNumber(check.value)) {
277
+ lower = tighterLowerBoundary(lower, {
278
+ value: check.value,
279
+ exclusive: check.inclusive === false,
280
+ });
281
+ }
282
+ else if (check.kind === "max" && finiteNumber(check.value)) {
283
+ upper = tighterUpperBoundary(upper, {
284
+ value: check.value,
285
+ exclusive: check.inclusive === false,
286
+ });
287
+ }
288
+ }
289
+ const json = { type: integer ? "integer" : "number" };
290
+ if (lower) {
291
+ if (lower.exclusive)
292
+ json.exclusiveMinimum = lower.value;
293
+ else
294
+ json.minimum = lower.value;
295
+ }
296
+ if (upper) {
297
+ if (upper.exclusive)
298
+ json.exclusiveMaximum = upper.value;
299
+ else
300
+ json.maximum = upper.value;
301
+ }
302
+ return json;
303
+ }
304
+ function arrayLimit(value) {
305
+ if (finiteNumber(value))
306
+ return value;
307
+ if (value && typeof value === "object" && finiteNumber(value.value))
308
+ return value.value;
309
+ return undefined;
310
+ }
311
+ function applyArrayLimits(json, def) {
312
+ const minimums = [];
313
+ const maximums = [];
314
+ const legacyMinimum = arrayLimit(def.minLength);
315
+ const legacyMaximum = arrayLimit(def.maxLength);
316
+ if (legacyMinimum !== undefined)
317
+ minimums.push(legacyMinimum);
318
+ if (legacyMaximum !== undefined)
319
+ maximums.push(legacyMaximum);
320
+ for (const check of getCheckDefinitions(def)) {
321
+ if (check.check === "min_length" && finiteNumber(check.minimum)) {
322
+ minimums.push(check.minimum);
323
+ }
324
+ else if (check.check === "max_length" && finiteNumber(check.maximum)) {
325
+ maximums.push(check.maximum);
326
+ }
327
+ }
328
+ if (minimums.length > 0)
329
+ json.minItems = Math.max(...minimums);
330
+ if (maximums.length > 0)
331
+ json.maxItems = Math.min(...maximums);
332
+ }
27
333
  export function zodToJsonSchema(schema) {
28
- return convertSchema(schema, { seen: new WeakSet() });
334
+ return convertSchema(schema, {
335
+ seen: new WeakSet(),
336
+ depth: 0,
337
+ nodeCount: 0,
338
+ });
29
339
  }
30
340
  function convertSchema(schema, context) {
31
341
  // Guard against invalid schemas (can happen with different zod instances in npm bundle)
@@ -45,11 +355,18 @@ export function isOptionalSchema(schema) {
45
355
  function convert(schema, context) {
46
356
  if (context.seen.has(schema))
47
357
  return {};
358
+ assertConversionDepth(context.depth);
359
+ context.nodeCount += 1;
360
+ if (context.nodeCount > MAX_CONVERSION_NODES) {
361
+ throw new RangeError(`Zod schema exceeds the maximum conversion node count of ${MAX_CONVERSION_NODES}`);
362
+ }
48
363
  context.seen.add(schema);
364
+ context.depth += 1;
49
365
  try {
50
366
  return convertInner(schema, context);
51
367
  }
52
368
  finally {
369
+ context.depth -= 1;
53
370
  context.seen.delete(schema);
54
371
  }
55
372
  }
@@ -59,13 +376,23 @@ function convertInner(schema, context) {
59
376
  switch (tag) {
60
377
  case "ZodString":
61
378
  case "string":
62
- return { type: "string" };
379
+ return convertString(def);
63
380
  case "ZodNumber":
64
381
  case "number":
65
- return { type: "number" };
382
+ return convertNumber(def);
66
383
  case "ZodBoolean":
67
384
  case "boolean":
68
385
  return { type: "boolean" };
386
+ case "ZodNull":
387
+ case "null":
388
+ return { type: "null" };
389
+ case "ZodUnknown":
390
+ case "unknown":
391
+ case "ZodAny":
392
+ case "any":
393
+ case "ZodCustom":
394
+ case "custom":
395
+ return {};
69
396
  case "ZodBigInt":
70
397
  case "bigint":
71
398
  return { type: "integer" };
@@ -95,7 +422,12 @@ function convertInner(schema, context) {
95
422
  const required = [];
96
423
  for (const [key, value] of Object.entries(shape ?? {})) {
97
424
  const zodSchema = value;
98
- properties[key] = convertSchema(zodSchema, context);
425
+ Object.defineProperty(properties, key, {
426
+ configurable: true,
427
+ enumerable: true,
428
+ value: convertSchema(zodSchema, context),
429
+ writable: true,
430
+ });
99
431
  if (!isOptionalSchema(zodSchema))
100
432
  required.push(key);
101
433
  }
@@ -111,9 +443,12 @@ function convertInner(schema, context) {
111
443
  case "array": {
112
444
  // v3: _def.type (item schema), v4: _def.element (item schema)
113
445
  const itemType = def.element ?? def.type;
114
- if (!itemType || typeof itemType === "string")
115
- return { type: "array" };
116
- return { type: "array", items: convertSchema(itemType, context) };
446
+ const json = { type: "array" };
447
+ if (itemType && typeof itemType !== "string") {
448
+ json.items = convertSchema(itemType, context);
449
+ }
450
+ applyArrayLimits(json, def);
451
+ return json;
117
452
  }
118
453
  case "ZodTuple":
119
454
  case "tuple": {
@@ -137,7 +472,37 @@ function convertInner(schema, context) {
137
472
  const valueSchema = def.valueType ?? def.element;
138
473
  if (!valueSchema)
139
474
  return { type: "object" };
140
- return { type: "object", additionalProperties: convertSchema(valueSchema, context) };
475
+ const valueJsonSchema = convertSchema(valueSchema, context);
476
+ const finiteKeys = def.keyType && finiteStringRecordKeys(def.keyType, context);
477
+ if (finiteKeys) {
478
+ const properties = {};
479
+ for (const key of finiteKeys) {
480
+ Object.defineProperty(properties, key, {
481
+ configurable: true,
482
+ enumerable: true,
483
+ value: valueJsonSchema,
484
+ writable: true,
485
+ });
486
+ }
487
+ return {
488
+ type: "object",
489
+ properties,
490
+ required: finiteKeys,
491
+ additionalProperties: false,
492
+ };
493
+ }
494
+ const json = {
495
+ type: "object",
496
+ additionalProperties: valueJsonSchema,
497
+ };
498
+ if (def.keyType) {
499
+ const propertyNames = convertSchema(def.keyType, context);
500
+ if (Object.keys(propertyNames).length !== 1 ||
501
+ propertyNames.type !== "string") {
502
+ json.propertyNames = propertyNames;
503
+ }
504
+ }
505
+ return json;
141
506
  }
142
507
  case "ZodDefault":
143
508
  case "default": {
@@ -145,22 +510,19 @@ function convertInner(schema, context) {
145
510
  if (!innerType)
146
511
  return { type: "object" };
147
512
  const inner = convertSchema(innerType, context);
148
- const defaultValue = typeof def.defaultValue === "function"
149
- ? def.defaultValue()
150
- : def.defaultValue;
151
- if (typeof inner === "object" && !("anyOf" in inner) && defaultValue !== undefined) {
152
- inner.default = defaultValue;
153
- }
513
+ const staticDefault = getStaticJsonSchemaDefault(def);
514
+ if (staticDefault)
515
+ inner.default = staticDefault.value;
154
516
  return inner;
155
517
  }
156
518
  case "ZodLazy":
157
519
  case "lazy":
158
- return def.getter ? convert(def.getter(), context) : { type: "object" };
520
+ return def.getter ? convertSchema(def.getter(), context) : { type: "object" };
159
521
  case "ZodEffects":
160
522
  case "pipe": {
161
523
  // v3: ZodEffects wraps schema in _def.schema
162
524
  // v4: pipe wraps in _def.in (input schema)
163
- const innerSchema = def.schema ?? def.in;
525
+ const innerSchema = representedPipeSchema(def);
164
526
  return innerSchema ? convert(innerSchema, context) : { type: "object" };
165
527
  }
166
528
  default:
@@ -184,10 +546,16 @@ function getObjectAdditionalProperties(def, context) {
184
546
  return convertSchema(def.catchall, context);
185
547
  }
186
548
  function unwrapSchema(schema) {
549
+ const seen = new WeakSet();
187
550
  let current = schema;
188
551
  let nullable = false;
189
552
  let optional = false;
553
+ let depth = 0;
190
554
  while (true) {
555
+ assertConversionDepth(depth);
556
+ if (seen.has(current))
557
+ return { schema: current, nullable, optional };
558
+ seen.add(current);
191
559
  const tag = getTypeTag(current);
192
560
  const def = getDef(current);
193
561
  switch (tag) {
@@ -195,17 +563,20 @@ function unwrapSchema(schema) {
195
563
  case "nullable":
196
564
  nullable = true;
197
565
  current = (def.innerType ?? def.schema);
566
+ depth++;
198
567
  break;
199
568
  case "ZodOptional":
200
569
  case "optional":
201
570
  optional = true;
202
571
  current = (def.innerType ?? def.schema);
572
+ depth++;
203
573
  break;
204
574
  case "ZodEffects":
205
575
  case "pipe":
206
- current = def.schema ?? def.in ?? current;
576
+ current = representedPipeSchema(def) ?? current;
207
577
  if (current === schema)
208
578
  return { schema: current, nullable, optional };
579
+ depth++;
209
580
  break;
210
581
  default:
211
582
  return { schema: current, nullable, optional };
@@ -213,8 +584,14 @@ function unwrapSchema(schema) {
213
584
  }
214
585
  }
215
586
  function hasDefaultSchema(schema) {
587
+ const seen = new WeakSet();
216
588
  let current = schema;
589
+ let depth = 0;
217
590
  while (true) {
591
+ assertConversionDepth(depth);
592
+ if (seen.has(current))
593
+ return false;
594
+ seen.add(current);
218
595
  const tag = getTypeTag(current);
219
596
  const def = getDef(current);
220
597
  switch (tag) {
@@ -231,6 +608,7 @@ function hasDefaultSchema(schema) {
231
608
  if (!inner || inner === current)
232
609
  return false;
233
610
  current = inner;
611
+ depth++;
234
612
  break;
235
613
  }
236
614
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veryfront/ext-schema-zod",
3
- "version": "0.1.1185",
3
+ "version": "0.1.1189",
4
4
  "description": "Veryfront first-party extension package for ext-schema-zod",
5
5
  "keywords": [
6
6
  "veryfront",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "scripts": {},
29
29
  "engines": {
30
- "node": ">=18.0.0"
30
+ "node": ">=22.3.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
@@ -42,10 +42,12 @@
42
42
  "capabilities": []
43
43
  },
44
44
  "dependencies": {
45
+ "ajv": "8.18.0",
46
+ "ajv-formats": "3.0.1",
45
47
  "zod": "4.3.6"
46
48
  },
47
49
  "peerDependencies": {
48
- "veryfront": "^0.1.1185"
50
+ "veryfront": "^0.1.1189"
49
51
  },
50
52
  "type": "module",
51
53
  "types": "./esm/index.d.ts",