openapi-contract-kit 0.0.3 → 0.0.5

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/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
3
+
4
+ ## [0.0.5] - 2026-09-12
5
+
6
+ - Tighten generated pragmatic email-format validation and modularize primitive and constraint renderers.
7
+
8
+ ## [0.0.4] - 2026-09-12
9
+
10
+ - Lowered the supported Node.js baseline to Node 24 and added CI coverage for Node 24 and 26.
4
11
 
5
12
  ## [0.0.3] - 2026-09-12
6
13
 
package/README.md CHANGED
@@ -19,7 +19,7 @@ JSON-first OpenAPI 3.1 contract generation and runtime validation for TypeScript
19
19
  pnpm add -D openapi-contract-kit
20
20
  ```
21
21
 
22
- Requires Node.js 26 and pnpm 12.3.4.
22
+ Requires Node.js 24 or newer and pnpm 12.3.4.
23
23
 
24
24
  ## Quick start
25
25
 
@@ -63,7 +63,7 @@ if (!result.ok) {
63
63
 
64
64
  Validation errors contain a path, keyword, and message. Successful validation returns the original input without mutating or cloning it.
65
65
 
66
- The focused fixture and acceptance tests cover structural validation: object shape, declared fields, primitive types, arrays, nullability, references, unions, and additional-property behavior. They do not require form-level checks such as email format, string length, patterns, or numeric ranges. Those constraints remain available when declared in consumer schemas.
66
+ The focused fixture and acceptance tests cover structural validation, primitive types, references, unions, additional-property behavior, and declared email format validation. String length, patterns, and numeric ranges remain available when declared in consumer schemas.
67
67
 
68
68
  ## Supported input
69
69
 
@@ -1,29 +1,10 @@
1
- function indent(lines, spaces = 2) {
2
- const prefix = ' '.repeat(spaces);
3
- return lines.map((line) => (line.length === 0 ? line : `${prefix}${line}`));
4
- }
5
- function typeExpression(type, value = 'value') {
6
- switch (type) {
7
- case 'array':
8
- return `Array.isArray(${value})`;
9
- case 'boolean':
10
- return `typeof ${value} === 'boolean'`;
11
- case 'integer':
12
- return `typeof ${value} === 'number' && Number.isFinite(${value}) && Number.isInteger(${value})`;
13
- case 'null':
14
- return `${value} === null`;
15
- case 'number':
16
- return `typeof ${value} === 'number' && Number.isFinite(${value})`;
17
- case 'object':
18
- return `typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value})`;
19
- case 'string':
20
- return `typeof ${value} === 'string'`;
21
- default: {
22
- const exhaustive = type;
23
- throw new Error(`Unsupported normalised schema type "${exhaustive}"`);
24
- }
25
- }
26
- }
1
+ import { renderArrayConstraints } from './validations/render-arrays.js';
2
+ import { renderNumberConstraints } from './validations/render-numbers.js';
3
+ import { renderObjectConstraints } from './validations/render-objects.js';
4
+ import { renderPrimitiveConstraints, typeExpression, } from './validations/render-primitives.js';
5
+ import { renderStringConstraints } from './validations/render-strings.js';
6
+ import { renderAllOf, renderUnionConstraint, } from './validations/render-unions.js';
7
+ import { indent } from './validations/render-utils.js';
27
8
  class MakerRenderer {
28
9
  #counter = 0;
29
10
  #functions = [];
@@ -155,168 +136,17 @@ export function make${name}(input: unknown): Result<${name}> {
155
136
  'return false;',
156
137
  ]), '}');
157
138
  }
158
- if (schema.constValue !== undefined) {
159
- lines.push(`if (!Object.is(value, ${JSON.stringify(schema.constValue)})) {`, ...indent([
160
- `errors.push({ path, keyword: 'const', message: 'Expected the documented constant value' });`,
161
- ]), '}');
162
- }
163
- if (schema.enumValues !== null) {
164
- const values = schema.enumValues
165
- .map((value) => JSON.stringify(value))
166
- .join(', ');
167
- lines.push(`if (![${values}].some((candidate) => Object.is(candidate, value))) {`, ...indent([
168
- `errors.push({ path, keyword: 'enum', message: 'Expected a documented enum value' });`,
169
- ]), '}');
170
- }
171
- for (const child of children.allOf) {
172
- lines.push(`${child}(value, path, errors);`);
173
- }
174
- this.#renderUnionConstraint(lines, 'anyOf', children.anyOf, false);
175
- this.#renderUnionConstraint(lines, 'oneOf', children.oneOf, true);
176
- this.#renderStringConstraints(lines, schema);
177
- this.#renderNumberConstraints(lines, schema);
178
- this.#renderArrayConstraints(lines, children.items);
179
- this.#renderObjectConstraints(lines, schema, children);
139
+ renderPrimitiveConstraints(lines, schema.constValue, schema.enumValues);
140
+ renderAllOf(lines, children.allOf);
141
+ renderUnionConstraint(lines, 'anyOf', children.anyOf, false);
142
+ renderUnionConstraint(lines, 'oneOf', children.oneOf, true);
143
+ renderStringConstraints(lines, schema);
144
+ renderNumberConstraints(lines, schema);
145
+ renderArrayConstraints(lines, children.items);
146
+ renderObjectConstraints(lines, schema, children);
180
147
  lines.push('return errors.length === errorCount;');
181
148
  return lines;
182
149
  }
183
- #renderUnionConstraint(lines, keyword, branches, isExclusive) {
184
- if (branches.length === 0) {
185
- return;
186
- }
187
- const variable = `${keyword}Matches`;
188
- lines.push(`let ${variable} = 0;`);
189
- for (const [index, branch] of branches.entries()) {
190
- const errorsName = `${keyword}Errors${index}`;
191
- lines.push(`const ${errorsName}: ValidationIssue[] = [];`, `if (${branch}(value, path, ${errorsName})) {`, ...indent([`${variable} += 1;`]), '}');
192
- }
193
- const invalidExpression = isExclusive
194
- ? `${variable} !== 1`
195
- : `${variable} === 0`;
196
- const expectation = isExclusive ? 'exactly one' : 'at least one';
197
- lines.push(`if (${invalidExpression}) {`, ...indent([
198
- `errors.push({ path, keyword: '${keyword}', message: 'Expected ${expectation} matching branch' });`,
199
- ]), '}');
200
- }
201
- #renderStringConstraints(lines, schema) {
202
- if (schema.format === null &&
203
- schema.maxLength === null &&
204
- schema.minLength === null &&
205
- schema.pattern === null) {
206
- return;
207
- }
208
- lines.push(`if (typeof value === 'string') {`);
209
- const checks = [];
210
- if (schema.minLength !== null) {
211
- checks.push(`if (Array.from(value).length < ${schema.minLength}) {`, ...indent([
212
- `errors.push({ path, keyword: 'minLength', message: 'String is shorter than ${schema.minLength} characters' });`,
213
- ]), '}');
214
- }
215
- if (schema.maxLength !== null) {
216
- checks.push(`if (Array.from(value).length > ${schema.maxLength}) {`, ...indent([
217
- `errors.push({ path, keyword: 'maxLength', message: 'String is longer than ${schema.maxLength} characters' });`,
218
- ]), '}');
219
- }
220
- if (schema.pattern !== null) {
221
- checks.push(`if (!new RegExp(${JSON.stringify(schema.pattern)}, 'u').test(value)) {`, ...indent([
222
- `errors.push({ path, keyword: 'pattern', message: 'String does not match the documented pattern' });`,
223
- ]), '}');
224
- }
225
- if (schema.format === 'email') {
226
- checks.push(`if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/u.test(value)) {`, ...indent([
227
- `errors.push({ path, keyword: 'format', message: 'Expected email format' });`,
228
- ]), '}');
229
- }
230
- lines.push(...indent(checks), '}');
231
- }
232
- #renderNumberConstraints(lines, schema) {
233
- if (schema.exclusiveMaximum === null &&
234
- schema.exclusiveMinimum === null &&
235
- schema.maximum === null &&
236
- schema.minimum === null) {
237
- return;
238
- }
239
- lines.push(`if (typeof value === 'number' && Number.isFinite(value)) {`);
240
- const checks = [];
241
- const addCheck = (expression, keyword, message) => {
242
- checks.push(`if (${expression}) {`, ...indent([
243
- `errors.push({ path, keyword: '${keyword}', message: ${JSON.stringify(message)} });`,
244
- ]), '}');
245
- };
246
- if (schema.minimum !== null) {
247
- addCheck(`value < ${schema.minimum}`, 'minimum', `Number must be at least ${schema.minimum}`);
248
- }
249
- if (schema.maximum !== null) {
250
- addCheck(`value > ${schema.maximum}`, 'maximum', `Number must be at most ${schema.maximum}`);
251
- }
252
- if (schema.exclusiveMinimum !== null) {
253
- addCheck(`value <= ${schema.exclusiveMinimum}`, 'exclusiveMinimum', `Number must be greater than ${schema.exclusiveMinimum}`);
254
- }
255
- if (schema.exclusiveMaximum !== null) {
256
- addCheck(`value >= ${schema.exclusiveMaximum}`, 'exclusiveMaximum', `Number must be less than ${schema.exclusiveMaximum}`);
257
- }
258
- lines.push(...indent(checks), '}');
259
- }
260
- #renderArrayConstraints(lines, itemValidator) {
261
- if (itemValidator === null) {
262
- return;
263
- }
264
- lines.push('if (Array.isArray(value)) {', ...indent([
265
- 'for (let index = 0; index < value.length; index += 1) {',
266
- ...indent(['const item = value[index];']),
267
- ...indent([`${itemValidator}(item, [...path, index], errors);`]),
268
- '}',
269
- ]), '}');
270
- }
271
- #renderObjectConstraints(lines, schema, children) {
272
- if (children.properties.length === 0 &&
273
- children.additionalProperties === null &&
274
- schema.additionalProperties !== false) {
275
- return;
276
- }
277
- lines.push(`if (typeof value === 'object' && value !== null && !Array.isArray(value)) {`);
278
- const checks = [];
279
- for (const property of children.properties) {
280
- const hasProperty = `Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(property.name)})`;
281
- const propertyPath = `[...path, ${JSON.stringify(property.name)}]`;
282
- if (property.required) {
283
- checks.push(`if (!${hasProperty}) {`, ...indent([
284
- `errors.push({ path: ${propertyPath}, keyword: 'required', message: 'Required property is missing' });`,
285
- ]), '} else {', ...indent([
286
- `${property.functionName}(Reflect.get(value, ${JSON.stringify(property.name)}), ${propertyPath}, errors);`,
287
- ]), '}');
288
- }
289
- else {
290
- checks.push(`if (${hasProperty}) {`, ...indent([
291
- `${property.functionName}(Reflect.get(value, ${JSON.stringify(property.name)}), ${propertyPath}, errors);`,
292
- ]), '}');
293
- }
294
- }
295
- if (schema.additionalProperties === false ||
296
- children.additionalProperties !== null) {
297
- const propertyNames = children.properties.map(({ name }) => name);
298
- checks.push('for (const key of Object.keys(value)) {');
299
- const isAdditionalProperty = propertyNames.length === 0
300
- ? 'true'
301
- : `![${propertyNames
302
- .map((name) => JSON.stringify(name))
303
- .join(', ')}].includes(key)`;
304
- const additionalChecks = [`if (${isAdditionalProperty}) {`];
305
- if (schema.additionalProperties === false) {
306
- additionalChecks.push(...indent([
307
- `errors.push({ path: [...path, key], keyword: 'additionalProperties', message: 'Unknown property is not allowed' });`,
308
- ]));
309
- }
310
- else if (children.additionalProperties !== null) {
311
- additionalChecks.push(...indent([
312
- `${children.additionalProperties}(Reflect.get(value, key), [...path, key], errors);`,
313
- ]));
314
- }
315
- additionalChecks.push('}');
316
- checks.push(...indent(additionalChecks), '}');
317
- }
318
- lines.push(...indent(checks), '}');
319
- }
320
150
  }
321
151
  export function emitSchemaMakers(model, config) {
322
152
  const files = new Map();
@@ -0,0 +1 @@
1
+ export declare function renderArrayConstraints(lines: string[], itemValidator: string | null): void;
@@ -0,0 +1,5 @@
1
+ export function renderArrayConstraints(lines, itemValidator) {
2
+ if (itemValidator === null)
3
+ return;
4
+ lines.push('if (Array.isArray(value)) {', ' for (let index = 0; index < value.length; index += 1) {', ' const item = value[index];', ` ${itemValidator}(item, [...path, index], errors);`, ' }', '}');
5
+ }
@@ -0,0 +1,2 @@
1
+ import type { StandardSchema } from '../types.js';
2
+ export declare function renderNumberConstraints(lines: string[], schema: StandardSchema): void;
@@ -0,0 +1,24 @@
1
+ import { indent } from './render-utils.js';
2
+ export function renderNumberConstraints(lines, schema) {
3
+ if (schema.exclusiveMaximum === null &&
4
+ schema.exclusiveMinimum === null &&
5
+ schema.maximum === null &&
6
+ schema.minimum === null)
7
+ return;
8
+ lines.push("if (typeof value === 'number' && Number.isFinite(value)) {");
9
+ const checks = [];
10
+ const addCheck = (expression, keyword, message) => {
11
+ checks.push(`if (${expression}) {`, ...indent([
12
+ `errors.push({ path, keyword: '${keyword}', message: ${JSON.stringify(message)} });`,
13
+ ]), '}');
14
+ };
15
+ if (schema.minimum !== null)
16
+ addCheck(`value < ${schema.minimum}`, 'minimum', `Number must be at least ${schema.minimum}`);
17
+ if (schema.maximum !== null)
18
+ addCheck(`value > ${schema.maximum}`, 'maximum', `Number must be at most ${schema.maximum}`);
19
+ if (schema.exclusiveMinimum !== null)
20
+ addCheck(`value <= ${schema.exclusiveMinimum}`, 'exclusiveMinimum', `Number must be greater than ${schema.exclusiveMinimum}`);
21
+ if (schema.exclusiveMaximum !== null)
22
+ addCheck(`value >= ${schema.exclusiveMaximum}`, 'exclusiveMaximum', `Number must be less than ${schema.exclusiveMaximum}`);
23
+ lines.push(...indent(checks), '}');
24
+ }
@@ -0,0 +1,3 @@
1
+ import type { StandardSchema } from '../types.js';
2
+ import type { ChildFunctions } from './types.js';
3
+ export declare function renderObjectConstraints(lines: string[], schema: StandardSchema, children: ChildFunctions): void;
@@ -0,0 +1,39 @@
1
+ import { indent } from './render-utils.js';
2
+ export function renderObjectConstraints(lines, schema, children) {
3
+ if (children.properties.length === 0 &&
4
+ children.additionalProperties === null &&
5
+ schema.additionalProperties !== false)
6
+ return;
7
+ lines.push("if (typeof value === 'object' && value !== null && !Array.isArray(value)) {");
8
+ const checks = [];
9
+ for (const property of children.properties) {
10
+ const hasProperty = `Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(property.name)})`;
11
+ const propertyPath = `[...path, ${JSON.stringify(property.name)}]`;
12
+ const validation = `${property.functionName}(Reflect.get(value, ${JSON.stringify(property.name)}), ${propertyPath}, errors);`;
13
+ if (property.required)
14
+ checks.push(`if (!${hasProperty}) {`, ...indent([
15
+ `errors.push({ path: ${propertyPath}, keyword: 'required', message: 'Required property is missing' });`,
16
+ ]), '} else {', ...indent([validation]), '}');
17
+ else
18
+ checks.push(`if (${hasProperty}) {`, ...indent([validation]), '}');
19
+ }
20
+ if (schema.additionalProperties === false ||
21
+ children.additionalProperties !== null) {
22
+ const propertyNames = children.properties.map(({ name }) => name);
23
+ const isAdditional = propertyNames.length === 0
24
+ ? 'true'
25
+ : `![${propertyNames.map((name) => JSON.stringify(name)).join(', ')}].includes(key)`;
26
+ const additional = [`if (${isAdditional}) {`];
27
+ if (schema.additionalProperties === false)
28
+ additional.push(...indent([
29
+ `errors.push({ path: [...path, key], keyword: 'additionalProperties', message: 'Unknown property is not allowed' });`,
30
+ ]));
31
+ else if (children.additionalProperties !== null)
32
+ additional.push(...indent([
33
+ `${children.additionalProperties}(Reflect.get(value, key), [...path, key], errors);`,
34
+ ]));
35
+ additional.push('}');
36
+ checks.push('for (const key of Object.keys(value)) {', ...indent(additional), '}');
37
+ }
38
+ lines.push(...indent(checks), '}');
39
+ }
@@ -0,0 +1,3 @@
1
+ import type { JsonPrimitive, SchemaType } from '../types.js';
2
+ export declare function typeExpression(type: SchemaType, value?: string): string;
3
+ export declare function renderPrimitiveConstraints(lines: string[], constValue: JsonPrimitive | undefined, enumValues: readonly JsonPrimitive[] | null): void;
@@ -0,0 +1,36 @@
1
+ import { indent } from './render-utils.js';
2
+ export function typeExpression(type, value = 'value') {
3
+ switch (type) {
4
+ case 'array':
5
+ return `Array.isArray(${value})`;
6
+ case 'boolean':
7
+ return `typeof ${value} === 'boolean'`;
8
+ case 'integer':
9
+ return `typeof ${value} === 'number' && Number.isFinite(${value}) && Number.isInteger(${value})`;
10
+ case 'null':
11
+ return `${value} === null`;
12
+ case 'number':
13
+ return `typeof ${value} === 'number' && Number.isFinite(${value})`;
14
+ case 'object':
15
+ return `typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value})`;
16
+ case 'string':
17
+ return `typeof ${value} === 'string'`;
18
+ default: {
19
+ const exhaustive = type;
20
+ throw new Error(`Unsupported normalised schema type "${exhaustive}"`);
21
+ }
22
+ }
23
+ }
24
+ export function renderPrimitiveConstraints(lines, constValue, enumValues) {
25
+ if (constValue !== undefined) {
26
+ lines.push(`if (!Object.is(value, ${JSON.stringify(constValue)})) {`, ...indent([
27
+ `errors.push({ path, keyword: 'const', message: 'Expected the documented constant value' });`,
28
+ ]), '}');
29
+ }
30
+ if (enumValues !== null) {
31
+ const values = enumValues.map((value) => JSON.stringify(value)).join(', ');
32
+ lines.push(`if (![${values}].some((candidate) => Object.is(candidate, value))) {`, ...indent([
33
+ `errors.push({ path, keyword: 'enum', message: 'Expected a documented enum value' });`,
34
+ ]), '}');
35
+ }
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { StandardSchema } from '../types.js';
2
+ export declare function renderStringConstraints(lines: string[], schema: StandardSchema): void;
@@ -0,0 +1,29 @@
1
+ import { indent } from './render-utils.js';
2
+ const EMAIL_FORMAT_PATTERN = "^[A-Za-z0-9!#$%&'*+\\/=?^_`{|}~-]+(?:\\.[A-Za-z0-9!#$%&'*+\\/=?^_`{|}~-]+)*@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\\.[A-Za-z]{2,63}$";
3
+ export function renderStringConstraints(lines, schema) {
4
+ if (schema.format === null &&
5
+ schema.maxLength === null &&
6
+ schema.minLength === null &&
7
+ schema.pattern === null)
8
+ return;
9
+ lines.push("if (typeof value === 'string') {");
10
+ const checks = [];
11
+ if (schema.minLength !== null)
12
+ checks.push(`if (Array.from(value).length < ${schema.minLength}) {`, ...indent([
13
+ `errors.push({ path, keyword: 'minLength', message: 'String is shorter than ${schema.minLength} characters' });`,
14
+ ]), '}');
15
+ if (schema.maxLength !== null)
16
+ checks.push(`if (Array.from(value).length > ${schema.maxLength}) {`, ...indent([
17
+ `errors.push({ path, keyword: 'maxLength', message: 'String is longer than ${schema.maxLength} characters' });`,
18
+ ]), '}');
19
+ if (schema.pattern !== null)
20
+ checks.push(`if (!new RegExp(${JSON.stringify(schema.pattern)}, 'u').test(value)) {`, ...indent([
21
+ `errors.push({ path, keyword: 'pattern', message: 'String does not match the documented pattern' });`,
22
+ ]), '}');
23
+ if (schema.format === 'email') {
24
+ checks.push(`if (!new RegExp(${JSON.stringify(EMAIL_FORMAT_PATTERN)}).test(value)) {`, ...indent([
25
+ `errors.push({ path, keyword: 'format', message: 'Expected email format' });`,
26
+ ]), '}');
27
+ }
28
+ lines.push(...indent(checks), '}');
29
+ }
@@ -0,0 +1,2 @@
1
+ export declare function renderAllOf(lines: string[], children: readonly string[]): void;
2
+ export declare function renderUnionConstraint(lines: string[], keyword: 'anyOf' | 'oneOf', branches: readonly string[], isExclusive: boolean): void;
@@ -0,0 +1,19 @@
1
+ export function renderAllOf(lines, children) {
2
+ for (const child of children)
3
+ lines.push(`${child}(value, path, errors);`);
4
+ }
5
+ export function renderUnionConstraint(lines, keyword, branches, isExclusive) {
6
+ if (branches.length === 0)
7
+ return;
8
+ const variable = `${keyword}Matches`;
9
+ lines.push(`let ${variable} = 0;`);
10
+ for (const [index, branch] of branches.entries()) {
11
+ const errorsName = `${keyword}Errors${index}`;
12
+ lines.push(`const ${errorsName}: ValidationIssue[] = [];`, `if (${branch}(value, path, ${errorsName})) {`, ` ${variable} += 1;`, '}');
13
+ }
14
+ const invalidExpression = isExclusive
15
+ ? `${variable} !== 1`
16
+ : `${variable} === 0`;
17
+ const expectation = isExclusive ? 'exactly one' : 'at least one';
18
+ lines.push(`if (${invalidExpression}) {`, ` errors.push({ path, keyword: '${keyword}', message: 'Expected ${expectation} matching branch' });`, '}');
19
+ }
@@ -0,0 +1 @@
1
+ export declare function indent(lines: readonly string[], spaces?: number): string[];
@@ -0,0 +1,4 @@
1
+ export function indent(lines, spaces = 2) {
2
+ const prefix = ' '.repeat(spaces);
3
+ return lines.map((line) => (line.length === 0 ? line : `${prefix}${line}`));
4
+ }
@@ -0,0 +1,12 @@
1
+ import type { SchemaProperty } from '../types.js';
2
+ export type ChildFunctions = {
3
+ readonly additionalProperties: string | null;
4
+ readonly allOf: readonly string[];
5
+ readonly anyOf: readonly string[];
6
+ readonly items: string | null;
7
+ readonly oneOf: readonly string[];
8
+ readonly properties: readonly (SchemaProperty & {
9
+ readonly functionName: string;
10
+ })[];
11
+ readonly reference: string | null;
12
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "openapi-contract-kit",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Generate type-safe TypeScript contracts and runtime validators from JSON OpenAPI 3.1 documents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "engines": {
8
- "node": ">=26"
8
+ "node": ">=24.0.0"
9
9
  },
10
10
  "main": "./dist/src/index.js",
11
11
  "types": "./dist/src/index.d.ts",
@@ -32,7 +32,7 @@
32
32
  ],
33
33
  "devDependencies": {
34
34
  "@eslint/js": "^10.0.1",
35
- "@types/node": "^26.5.1",
35
+ "@types/node": "^24.0.0",
36
36
  "eslint": "^10.10.0",
37
37
  "prettier": "^3.9.6",
38
38
  "typescript": "^5.9.3",