diff --git a/package-lock.json b/package-lock.json index 882ea853..ca9bd433 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24593,7 +24593,7 @@ "helmet": "^8.3.0", "https-proxy-agent": "^7.0.6", "ioredis": "^5.11.1", - "js-yaml": "^4.3.2", + "js-yaml": "^5.4.1", "mongodb": "^7.6.0", "mssql": "^12.7.2", "mysql2": "^3.24.4", @@ -24814,6 +24814,28 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "packages/backend/node_modules/js-yaml": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz", + "integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, "packages/backend/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", diff --git a/packages/backend/package.json b/packages/backend/package.json index acc2893c..83cfabd8 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -62,7 +62,7 @@ "helmet": "^8.3.0", "https-proxy-agent": "^7.0.6", "ioredis": "^5.11.1", - "js-yaml": "^4.3.2", + "js-yaml": "^5.4.1", "mongodb": "^7.6.0", "mssql": "^12.7.2", "mysql2": "^3.24.4", diff --git a/packages/backend/src/connectors/parsers/openapi.parser.spec.ts b/packages/backend/src/connectors/parsers/openapi.parser.spec.ts index 94aebe30..06e41ce3 100644 --- a/packages/backend/src/connectors/parsers/openapi.parser.spec.ts +++ b/packages/backend/src/connectors/parsers/openapi.parser.spec.ts @@ -25,6 +25,33 @@ describe('OpenApiParser', () => { expect(tools).toHaveLength(0); }); + it('should parse a YAML spec with anchors and merge keys', async () => { + const yamlSpec = [ + 'openapi: 3.0.0', + 'info: { title: Test API, version: 1.0.0 }', + 'x-common: &paging', + ' in: query', + ' schema: { type: integer }', + 'paths:', + ' /points:', + ' get:', + ' operationId: listPoints', + ' parameters:', + ' - <<: *paging', + ' name: y', + ' description: Vertical offset', + ' responses:', + " '200': { description: OK }", + ].join('\n'); + const tools = await parser.parse(yamlSpec); + expect(tools).toHaveLength(1); + const params = tools[0].parameters as any; + // The merged keys land on the parameter, and `y` stays a name, not `true`. + expect(params.properties.y).toBeDefined(); + expect(params.properties.y.type).toBe('integer'); + expect(tools[0].endpointMapping.queryParams).toEqual(expect.objectContaining({ y: '$y' })); + }); + // ── GET endpoints ────────────────────────────────────────────────────── it('should parse a simple GET endpoint', async () => { diff --git a/packages/backend/src/connectors/parsers/openapi.parser.ts b/packages/backend/src/connectors/parsers/openapi.parser.ts index 9b52c144..2608e801 100644 --- a/packages/backend/src/connectors/parsers/openapi.parser.ts +++ b/packages/backend/src/connectors/parsers/openapi.parser.ts @@ -4,10 +4,16 @@ import { capToolName } from './tool-name.util'; const SwaggerParser = require('swagger-parser'); import axios from 'axios'; -const yaml = require('js-yaml') as { load: (s: string) => unknown }; +import { CORE_SCHEMA, load as loadYaml, mergeTag, timestampTag } from 'js-yaml'; import { assertSafeOutboundUrl } from '../../common/ssrf.util'; import { normalizeOpenApi31 } from './openapi-3.1-normalizer'; +// js-yaml 5 loads with the bare YAML 1.2 core schema. Real-world specs use +// `<<: *anchor` merge keys, which it would keep as a literal "<<" property, +// so add merge (and timestamps) back: the same result js-yaml 4 gave. Not +// YAML11_SCHEMA: its 1.1 booleans turn a property named `y` or `on` into `true`. +const SPEC_YAML_SCHEMA = CORE_SCHEMA.withTags(mergeTag, timestampTag); + export interface ParsedTool { name: string; /** @@ -169,7 +175,7 @@ export class OpenApiParser { return JSON.parse(input); } try { - return yaml.load(input); + return loadYaml(input, { schema: SPEC_YAML_SCHEMA }); } catch (yamlErr) { // Last-resort: try JSON anyway so the original error surfaces if the // input is genuinely malformed.