diff --git a/package-lock.json b/package-lock.json index c1eb7f03e3..6a23afa591 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,22 +74,6 @@ "ajv": ">=8" } }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", - "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, "node_modules/@apidevtools/openapi-schemas": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", @@ -105,23 +89,6 @@ "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", "license": "MIT" }, - "node_modules/@apidevtools/swagger-parser": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", - "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "14.0.1", - "@apidevtools/openapi-schemas": "^2.1.0", - "@apidevtools/swagger-methods": "^3.0.2", - "ajv": "^8.17.1", - "ajv-draft-04": "^1.0.0", - "call-me-maybe": "^1.0.2" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, "node_modules/@asamuzakjp/css-color": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.6.tgz", @@ -6961,6 +6928,141 @@ "win32" ] }, + "node_modules/@scalar/helpers": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.16.4.tgz", + "integrity": "sha512-9eILWzUcAkHEuYMMvijpSIy69t+Vkq+NudLB/7lgARFTWqXfQpIxW1L3VH7ogKyRie5+E4CNZ3PSvlw6IIfLGw==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/json-magic": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@scalar/json-magic/-/json-magic-0.17.0.tgz", + "integrity": "sha512-YmJAO9NoGCcU/vx7P4Dy9DGv+Vntp+ny3Fh99slHOOh/IZxvbdK5hWeZdzQ3x+ezab9XJNscFoX8H/P6W+H8lA==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.4", + "pathe": "^2.0.3", + "undici": "8.12.3", + "yaml": "^2.9.1" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@scalar/json-magic/node_modules/undici": { + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@scalar/json-schema-validator": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@scalar/json-schema-validator/-/json-schema-validator-0.1.17.tgz", + "integrity": "sha512-4Kmbvpng0V0kgPyqDWUZ1qstKjtDfz7seUCK1/inHjeQ+EJIcXPhoW47sOXf9yQBPbSjp42Wt8a7MZvizyoCPg==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.4", + "@scalar/types": "0.26.0", + "ajv": "^8.20.0", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^3.0.1", + "yaml": "^2.9.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-parser": { + "version": "0.29.19", + "resolved": "https://registry.npmjs.org/@scalar/openapi-parser/-/openapi-parser-0.29.19.tgz", + "integrity": "sha512-ajtjKM9sQQOxEKP/VCtLbM9kQCAa0M4XWKsfadVnPvga1p7t1wPtIW+uDph8sHe+nKyydqwYxlZ8Q6RluxhbXw==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.4", + "@scalar/json-magic": "0.17.0", + "@scalar/openapi-types": "0.9.8", + "@scalar/openapi-upgrader": "0.4.6", + "@scalar/openapi-validator": "0.1.17", + "@scalar/types": "0.26.0", + "yaml": "^2.9.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-types": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.9.8.tgz", + "integrity": "sha512-pGcpkZObsGBC7nRKYte+6gstoe0C4hp7B2UWQYjg5To1wE68s3OHVvin+muL3tn3ZAoahXeJAkn1+aEcMi802w==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-upgrader": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@scalar/openapi-upgrader/-/openapi-upgrader-0.4.6.tgz", + "integrity": "sha512-wkTbowGZAlDA5AhCAnBVft6y3veoE17P+okH/zrwM/BZWBSzPghGpa4BPTCg3+IzRmRP/8ZeaYO7LfxCDAEmcA==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.4", + "@scalar/openapi-types": "0.9.8" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/openapi-validator": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@scalar/openapi-validator/-/openapi-validator-0.1.17.tgz", + "integrity": "sha512-vBZZgD5k5mp5ii3kNdAti3bZZO0kEvUWYbr9E+Dkjdb67LAdgD5y4Ym0pNIik7prN6KUuUVUu1UMFGFvu1Udqw==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.4", + "@scalar/json-schema-validator": "0.1.17", + "@scalar/types": "0.26.0", + "yaml": "^2.9.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/types": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.26.0.tgz", + "integrity": "sha512-T78Dj1Q+W0E1RsMyPoydfR+L+p9IUIkntBCRYlWaRibM98jAxABRYGQoU/qDHX0PR1IAFdE+d7ZqslypZbJzcg==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.16.4", + "nanoid": "^5.1.16", + "type-fest": "^5.10.0", + "zod": "^4.6.7" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/types/node_modules/type-fest": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -9766,7 +9868,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "devOptional": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -19247,10 +19348,9 @@ } }, "node_modules/nanoid": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", - "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", - "devOptional": true, + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", "funding": [ { "type": "github", @@ -24083,6 +24183,18 @@ "node": ">= 6" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", @@ -26364,9 +26476,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -26505,10 +26617,9 @@ } }, "node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", - "devOptional": true, + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -26542,7 +26653,6 @@ "license": "Apache-2.0", "dependencies": { "@apideck/better-ajv-errors": "^0.3.6", - "@apidevtools/swagger-parser": "12.1.0", "@bufbuild/protobuf": "^2.16.0", "@connectrpc/connect": "^2.2.0", "@connectrpc/connect-node": "^2.2.0", @@ -26562,6 +26672,7 @@ "@rjsf/core": "6.8.0", "@rjsf/utils": "6.8.0", "@rjsf/validator-ajv8": "6.8.0", + "@scalar/openapi-parser": "0.29.19", "@sentry/electron": "^7.17.0", "@stoplight/spectral-core": "^1.22.0", "@stoplight/spectral-formats": "^1.8.2", diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 11e30c22d4..e40ac9265a 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -50,7 +50,6 @@ }, "dependencies": { "@apideck/better-ajv-errors": "^0.3.6", - "@apidevtools/swagger-parser": "12.1.0", "@bufbuild/protobuf": "^2.16.0", "@connectrpc/connect": "^2.2.0", "@connectrpc/connect-node": "^2.2.0", @@ -70,6 +69,7 @@ "@rjsf/core": "6.8.0", "@rjsf/utils": "6.8.0", "@rjsf/validator-ajv8": "6.8.0", + "@scalar/openapi-parser": "0.29.19", "@sentry/electron": "^7.17.0", "@stoplight/spectral-core": "^1.22.0", "@stoplight/spectral-formats": "^1.8.2", diff --git a/packages/insomnia/src/main/importers/importers/openapi-3.test.ts b/packages/insomnia/src/main/importers/importers/openapi-3.test.ts index af91d94d0e..dbc2fd65bb 100644 --- a/packages/insomnia/src/main/importers/importers/openapi-3.test.ts +++ b/packages/insomnia/src/main/importers/importers/openapi-3.test.ts @@ -495,4 +495,57 @@ describe('openapi-3', () => { expect(anyPetContentTypeHeader?.value).toBe('application/json'); }); }); + + describe('circular $ref', () => { + it('should import a schema that references itself without recursing forever', async () => { + const openApiDoc: OpenAPIV3.Document = { + openapi: '3.0.3', + info: { title: 'Tree API', version: '1.0.0' }, + servers: [{ url: 'https://api.example.com' }], + paths: { + '/nodes': { + post: { + requestBody: { + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/Node' }, + }, + }, + }, + responses: { 200: { description: 'ok' } }, + }, + }, + }, + components: { + schemas: { + Node: { + type: 'object', + properties: { + name: { type: 'string' }, + parent: { $ref: '#/components/schemas/Node' }, + children: { type: 'array', items: { $ref: '#/components/schemas/Node' } }, + }, + }, + }, + }, + }; + + const result = await convert(JSON.stringify(openApiDoc)); + expect(result).not.toBeNull(); + const request = result?.find(item => item._type === 'request'); + + const body = JSON.parse(request.body.text); + expect(body.name).toBe('string'); + expect(Array.isArray(body.children)).toBe(true); + // the cycle is cut off with null instead of expanding forever + let node = body; + let depth = 0; + while (node && depth < 10) { + node = node.parent; + depth++; + } + expect(node).toBeNull(); + expect(depth).toBeLessThan(10); + }); + }); }); diff --git a/packages/insomnia/src/main/importers/importers/openapi-3.ts b/packages/insomnia/src/main/importers/importers/openapi-3.ts index 57ec1d14ca..198cb3380d 100644 --- a/packages/insomnia/src/main/importers/importers/openapi-3.ts +++ b/packages/insomnia/src/main/importers/importers/openapi-3.ts @@ -1,7 +1,7 @@ import crypto from 'node:crypto'; import { parse as urlParse } from 'node:url'; -import SwaggerParser from '@apidevtools/swagger-parser'; +import { dereference, validate } from '@scalar/openapi-parser'; import type { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; import YAML from 'yaml'; @@ -554,7 +554,7 @@ const convertParameters = (parameters: OpenAPIV3.ParameterObject[] = []) => { * Returns example / default value of the parameter, if any of those are defined. If not, returns value based on parameter type. */ // @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion -const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => { +const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string, ancestors: unknown[] = []) => { const typeExamples = { 'string': () => 'string', 'string_email': () => 'user@example.com', @@ -571,7 +571,10 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => { if (properties) { for (const propertyName of Object.keys(properties)) { - example[propertyName] = generateParameterExample(properties[propertyName] as OpenAPIV3.SchemaObject); + example[propertyName] = generateParameterExample(properties[propertyName] as OpenAPIV3.SchemaObject, [ + ...ancestors, + schema, + ]); } } @@ -580,7 +583,7 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => { // @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion 'array': (schema: OpenAPIV2.ItemsObject) => { // @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion - const value = generateParameterExample(schema.items); + const value = generateParameterExample(schema.items, [...ancestors, schema]); if (schema.collectionFormat === 'csv') { return value; @@ -595,6 +598,11 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => { } if (schema instanceof Object) { + // dereferenced circular $refs resolve to the same object, so stop when a schema contains itself + if (ancestors.includes(schema)) { + return null; + } + const { type, format, @@ -626,7 +634,7 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => { if (allOf && Array.isArray(allOf)) { const mergedExample: Record = {}; for (const subSchema of allOf) { - const subExample = generateParameterExample(subSchema as OpenAPIV3.SchemaObject); + const subExample = generateParameterExample(subSchema as OpenAPIV3.SchemaObject, [...ancestors, schema]); if (subExample && typeof subExample === 'object' && !Array.isArray(subExample)) { Object.assign(mergedExample, subExample); } @@ -636,12 +644,12 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => { // Handle oneOf by using the first schema (validates against exactly one) if (oneOf && Array.isArray(oneOf) && oneOf.length > 0) { - return generateParameterExample(oneOf[0] as OpenAPIV3.SchemaObject); + return generateParameterExample(oneOf[0] as OpenAPIV3.SchemaObject, [...ancestors, schema]); } // Handle anyOf by using the first schema (validates against any/one or more) if (anyOf && Array.isArray(anyOf) && anyOf.length > 0) { - return generateParameterExample(anyOf[0] as OpenAPIV3.SchemaObject); + return generateParameterExample(anyOf[0] as OpenAPIV3.SchemaObject, [...ancestors, schema]); } // @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion @@ -810,11 +818,16 @@ export const convert: Converter = async rawData => { } try { - apiDocument = (await SwaggerParser.validate(apiDocument, { - dereference: { - circular: 'ignore', - }, - })) as OpenAPIV3.Document; + const input = apiDocument as unknown as Record; + const { valid, errors } = await validate(input); + if (!valid) { + throw errors; + } + const { schema, errors: dereferenceErrors } = await dereference(input); + if (dereferenceErrors?.length) { + throw dereferenceErrors; + } + apiDocument = schema as unknown as OpenAPIV3.Document; } catch (err) { console.log('[openapi-3] Import file validation failed', err); } diff --git a/packages/insomnia/src/main/importers/importers/swagger-2.ts b/packages/insomnia/src/main/importers/importers/swagger-2.ts index bf6e075371..fa1c1ef131 100644 --- a/packages/insomnia/src/main/importers/importers/swagger-2.ts +++ b/packages/insomnia/src/main/importers/importers/swagger-2.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto'; -import SwaggerParser from '@apidevtools/swagger-parser'; +import { dereference, validate } from '@scalar/openapi-parser'; import type { OpenAPIV2 } from 'openapi-types'; import YAML from 'yaml'; @@ -502,7 +502,15 @@ export const convert: Converter = async rawData => { } // Await here so we catch any exceptions try { - api = await SwaggerParser.validate(api); + const { valid, errors } = await validate(api); + if (!valid) { + throw errors; + } + const { schema, errors: dereferenceErrors } = await dereference(api); + if (dereferenceErrors?.length) { + throw dereferenceErrors; + } + api = schema as unknown as OpenAPIV2.Document; } catch (err) { // We already know it's a Swagger doc so we will try to import it anyway instead // of bailing out here.