@readme/openapi-parser
Advanced tools
+266
-42
@@ -110,4 +110,4 @@ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; | ||
| var _openapischemas = require('@readme/openapi-schemas'); | ||
| var _ajvdraft04 = require('ajv-draft-04'); var _ajvdraft042 = _interopRequireDefault(_ajvdraft04); | ||
| var _2020js = require('ajv/dist/2020.js'); var _2020js2 = _interopRequireDefault(_2020js); | ||
| var _ajvdraft04 = require('ajv-draft-04'); var _ajvdraft042 = _interopRequireDefault(_ajvdraft04); | ||
@@ -165,3 +165,3 @@ // src/lib/hasInvalidPaths.ts | ||
| } | ||
| function validateSchema(api, options = {}) { | ||
| function validateSchema(api, options = {}, suppressedInstancePaths = []) { | ||
| if (hasInvalidPaths(api)) { | ||
@@ -208,3 +208,8 @@ return { | ||
| let additionalErrors = 0; | ||
| let reducedErrors = reduceAjvErrors(ajv.errors); | ||
| let reducedErrors = reduceAjvErrors(ajv.errors).filter((err) => { | ||
| return !suppressedInstancePaths.some((path) => err.instancePath === path || err.instancePath.startsWith(`${path}/`)); | ||
| }); | ||
| if (!reducedErrors.length) { | ||
| return { valid: true, warnings: [], specification: specificationName }; | ||
| } | ||
| if (reducedErrors.length >= LARGE_SPEC_ERROR_CAP) { | ||
@@ -244,5 +249,10 @@ try { | ||
| // src/validators/spec/index.ts | ||
| var SpecificationValidator = (_class = class {constructor() { _class.prototype.__init.call(this);_class.prototype.__init2.call(this); } | ||
| var SpecificationValidator = (_class = class {constructor() { _class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this); } | ||
| __init() {this.errors = []} | ||
| __init2() {this.warnings = []} | ||
| /** | ||
| * Instance paths flagged by `runPreSchemaChecks()`. Used to suppress AJV `oneOf` noise that | ||
| * the pre-schema validator has already produced a clearer error or warning for. | ||
| */ | ||
| __init3() {this.flaggedInstancePaths = []} | ||
| reportError(message) { | ||
@@ -254,2 +264,7 @@ this.errors.push({ message }); | ||
| } | ||
| flagInstancePath(path) { | ||
| if (!this.flaggedInstancePaths.includes(path)) { | ||
| this.flaggedInstancePaths.push(path); | ||
| } | ||
| } | ||
| }, _class); | ||
@@ -266,2 +281,5 @@ | ||
| } | ||
| runPreSchemaChecks() { | ||
| this.checkSecuritySchemes(); | ||
| } | ||
| run() { | ||
@@ -482,2 +500,115 @@ const operationIds = []; | ||
| /** | ||
| * Validates security schemes in `components.securitySchemes` against their declared `type`. | ||
| * | ||
| * AJV uses a `oneOf` schema to validate security schemes, so when a scheme is malformed AJV | ||
| * fails every branch and produces overwhelming, unhelpful errors. This pre-AJV pass surfaces | ||
| * a single targeted error per problem. | ||
| * | ||
| * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object} | ||
| */ | ||
| checkSecuritySchemes() { | ||
| const securitySchemes = _optionalChain([this, 'access', _14 => _14.api, 'access', _15 => _15.components, 'optionalAccess', _16 => _16.securitySchemes]); | ||
| if (!securitySchemes) { | ||
| return; | ||
| } | ||
| const schemeTypeProps = { | ||
| apiKey: { | ||
| required: ["name", "in"], | ||
| foreign: { scheme: "http", bearerFormat: "http", flows: "oauth2", openIdConnectUrl: "openIdConnect" } | ||
| }, | ||
| http: { | ||
| required: ["scheme"], | ||
| foreign: { name: "apiKey", in: "apiKey", flows: "oauth2", openIdConnectUrl: "openIdConnect" } | ||
| }, | ||
| oauth2: { | ||
| required: ["flows"], | ||
| foreign: { | ||
| name: "apiKey", | ||
| in: "apiKey", | ||
| scheme: "http", | ||
| bearerFormat: "http", | ||
| openIdConnectUrl: "openIdConnect" | ||
| } | ||
| }, | ||
| openIdConnect: { | ||
| required: ["openIdConnectUrl"], | ||
| foreign: { name: "apiKey", in: "apiKey", scheme: "http", bearerFormat: "http", flows: "oauth2" } | ||
| }, | ||
| mutualTLS: { | ||
| required: [], | ||
| foreign: { | ||
| name: "apiKey", | ||
| in: "apiKey", | ||
| scheme: "http", | ||
| bearerFormat: "http", | ||
| flows: "oauth2", | ||
| openIdConnectUrl: "openIdConnect" | ||
| } | ||
| } | ||
| }; | ||
| Object.keys(securitySchemes).forEach((name) => { | ||
| const scheme = securitySchemes[name]; | ||
| if ("$ref" in scheme) { | ||
| return; | ||
| } | ||
| const schemeId = `/components/securitySchemes/${name}`; | ||
| const reportIssue = (message) => this.reportSecuritySchemeIssue(message, schemeId); | ||
| if (!("type" in scheme) || !scheme.type) { | ||
| reportIssue( | ||
| `\`${schemeId}\` is missing required property \`type\`. Must be one of: \`apiKey\`, \`http\`, \`oauth2\`, \`openIdConnect\`, \`mutualTLS\`.` | ||
| ); | ||
| return; | ||
| } | ||
| const type = scheme.type; | ||
| if (type === "basic") { | ||
| reportIssue( | ||
| `\`${schemeId}\` uses \`type: basic\`, which is a Swagger 2.0 value. In OpenAPI 3.x use \`type: http\` with \`scheme: basic\` instead.` | ||
| ); | ||
| return; | ||
| } | ||
| if (!(type in schemeTypeProps)) { | ||
| reportIssue( | ||
| `\`${schemeId}\` has an invalid \`type\`: \`${type}\`. Must be one of: \`apiKey\`, \`http\`, \`oauth2\`, \`openIdConnect\`, \`mutualTLS\`.` | ||
| ); | ||
| return; | ||
| } | ||
| const config = schemeTypeProps[type]; | ||
| config.required.forEach((prop) => { | ||
| if (!(prop in scheme)) { | ||
| reportIssue(`\`${schemeId}\` (\`type: ${type}\`) is missing required property \`${prop}\`.`); | ||
| } | ||
| }); | ||
| Object.entries(config.foreign).forEach(([prop, ownerType]) => { | ||
| if (prop in scheme) { | ||
| reportIssue( | ||
| `\`${schemeId}\` (\`type: ${type}\`) includes \`${prop}\`, which is only valid for \`type: ${ownerType}\` schemes.` | ||
| ); | ||
| } | ||
| }); | ||
| if (type === "apiKey" && typeof scheme.in === "string") { | ||
| const validIn = ["query", "header", "cookie"]; | ||
| if (!validIn.includes(scheme.in)) { | ||
| reportIssue( | ||
| `\`${schemeId}\` has an invalid \`in\` value: \`${scheme.in}\`. Must be one of: \`query\`, \`header\`, \`cookie\`.` | ||
| ); | ||
| } | ||
| } | ||
| if (type === "oauth2" && scheme.flows && typeof scheme.flows === "object") { | ||
| if (Object.keys(scheme.flows).length === 0) { | ||
| reportIssue( | ||
| `\`${schemeId}\` has empty \`flows\`. At least one grant type is required: \`implicit\`, \`password\`, \`clientCredentials\`, or \`authorizationCode\`.` | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| reportSecuritySchemeIssue(message, schemeId) { | ||
| this.flagInstancePath(schemeId); | ||
| if (this.rules["invalid-security-scheme-properties"] === "warning") { | ||
| this.reportWarning(message); | ||
| } else { | ||
| this.reportError(message); | ||
| } | ||
| } | ||
| /** | ||
| * Checks the given parameter list for duplicates. | ||
@@ -516,2 +647,5 @@ * | ||
| } | ||
| runPreSchemaChecks() { | ||
| this.checkSecurityDefinitions(); | ||
| } | ||
| run() { | ||
@@ -733,19 +867,5 @@ const operationIds = []; | ||
| validateRequiredPropertiesExist(schema, schemaId) { | ||
| function collectProperties(schemaObj, props) { | ||
| if (schemaObj.properties) { | ||
| Object.keys(schemaObj.properties).forEach((property) => { | ||
| if (schemaObj.properties.hasOwnProperty(property)) { | ||
| props[property] = schemaObj.properties[property]; | ||
| } | ||
| }); | ||
| } | ||
| if (schemaObj.allOf) { | ||
| schemaObj.allOf.forEach((parent) => { | ||
| collectProperties(parent, props); | ||
| }); | ||
| } | ||
| } | ||
| if (schema.required && Array.isArray(schema.required)) { | ||
| const props = {}; | ||
| collectProperties(schema, props); | ||
| this.collectProperties(schema, props); | ||
| schema.required.forEach((requiredProperty) => { | ||
@@ -764,2 +884,62 @@ if (!props[requiredProperty]) { | ||
| /** | ||
| * Recursively collects all properties of a schema and its ancestors. They are added to the | ||
| * supplied `props` object. | ||
| * | ||
| */ | ||
| collectProperties(schemaObj, props) { | ||
| if (schemaObj.properties) { | ||
| Object.keys(schemaObj.properties).forEach((property) => { | ||
| if (schemaObj.properties.hasOwnProperty(property)) { | ||
| props[property] = schemaObj.properties[property]; | ||
| } | ||
| }); | ||
| } | ||
| if (schemaObj.allOf) { | ||
| schemaObj.allOf.forEach((parent) => { | ||
| this.collectProperties(parent, props); | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Validates security definitions against their declared `type`. | ||
| * | ||
| * AJV uses `oneOf` to validate `securityDefinitions`, so when a definition is malformed AJV | ||
| * fails every branch and produces overwhelming, unhelpful errors. This pre-AJV pass surfaces | ||
| * a single targeted error per problem. | ||
| * | ||
| * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#security-scheme-object} | ||
| */ | ||
| checkSecurityDefinitions() { | ||
| const securityDefinitions = this.api.securityDefinitions; | ||
| if (!securityDefinitions) { | ||
| return; | ||
| } | ||
| const validApiKeyIn = ["query", "header"]; | ||
| Object.keys(securityDefinitions).forEach((name) => { | ||
| const definition = securityDefinitions[name]; | ||
| const definitionId = `/securityDefinitions/${name}`; | ||
| const reportIssue = (message) => this.reportSecuritySchemeIssue(message, definitionId); | ||
| const type = definition.type; | ||
| if (type === "http") { | ||
| reportIssue( | ||
| `\`${definitionId}\` uses \`type: http\`, which is an OpenAPI 3.x value. In Swagger 2.0 use \`type: basic\` for HTTP Basic auth.` | ||
| ); | ||
| return; | ||
| } | ||
| if (type === "apiKey" && typeof definition.in === "string" && !validApiKeyIn.includes(definition.in)) { | ||
| reportIssue( | ||
| `\`${definitionId}\` has an invalid \`in\` value: \`${definition.in}\`. Swagger 2.0 only supports \`query\` or \`header\`.` | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
| reportSecuritySchemeIssue(message, definitionId) { | ||
| this.flagInstancePath(definitionId); | ||
| if (this.rules["invalid-security-scheme-properties"] === "warning") { | ||
| this.reportWarning(message); | ||
| } else { | ||
| this.reportError(message); | ||
| } | ||
| } | ||
| /** | ||
| * Checks the given parameter list for duplicates. | ||
@@ -811,2 +991,33 @@ * | ||
| } | ||
| function validateSpecPreSchema(api, rules) { | ||
| let validator; | ||
| const specificationName = getSpecificationName(api); | ||
| if (_chunkLU5KN3DHcjs.isOpenAPI.call(void 0, api)) { | ||
| validator = new OpenAPISpecificationValidator(api, rules.openapi); | ||
| } else { | ||
| validator = new SwaggerSpecificationValidator(api, rules.swagger); | ||
| } | ||
| validator.runPreSchemaChecks(); | ||
| const flaggedInstancePaths = validator.flaggedInstancePaths; | ||
| if (!validator.errors.length) { | ||
| return { | ||
| flaggedInstancePaths, | ||
| result: { | ||
| valid: true, | ||
| warnings: validator.warnings, | ||
| specification: specificationName | ||
| } | ||
| }; | ||
| } | ||
| return { | ||
| flaggedInstancePaths, | ||
| result: { | ||
| valid: false, | ||
| errors: validator.errors, | ||
| warnings: validator.warnings, | ||
| additionalErrors: 0, | ||
| specification: specificationName | ||
| } | ||
| }; | ||
| } | ||
@@ -869,7 +1080,37 @@ // src/index.ts | ||
| parserOptions.dereference.circular = circular$RefOption; | ||
| result = validateSchema(parser.schema, options); | ||
| const openapiRules = _optionalChain([options, 'optionalAccess', _17 => _17.validate, 'optionalAccess', _18 => _18.rules, 'optionalAccess', _19 => _19.openapi]); | ||
| const swaggerRules = _optionalChain([options, 'optionalAccess', _20 => _20.validate, 'optionalAccess', _21 => _21.rules, 'optionalAccess', _22 => _22.swagger]); | ||
| const rules = { | ||
| openapi: { | ||
| "array-without-items": _optionalChain([openapiRules, 'optionalAccess', _23 => _23["array-without-items"]]) || "error", | ||
| "duplicate-non-request-body-parameters": _optionalChain([openapiRules, 'optionalAccess', _24 => _24["duplicate-non-request-body-parameters"]]) || "error", | ||
| "duplicate-operation-id": _optionalChain([openapiRules, 'optionalAccess', _25 => _25["duplicate-operation-id"]]) || "error", | ||
| "invalid-security-scheme-properties": _optionalChain([openapiRules, 'optionalAccess', _26 => _26["invalid-security-scheme-properties"]]) || "error", | ||
| "non-optional-path-parameters": _optionalChain([openapiRules, 'optionalAccess', _27 => _27["non-optional-path-parameters"]]) || "error", | ||
| "path-parameters-not-in-parameters": _optionalChain([openapiRules, 'optionalAccess', _28 => _28["path-parameters-not-in-parameters"]]) || "error", | ||
| "path-parameters-not-in-path": _optionalChain([openapiRules, 'optionalAccess', _29 => _29["path-parameters-not-in-path"]]) || "error" | ||
| }, | ||
| swagger: { | ||
| "array-without-items": _optionalChain([swaggerRules, 'optionalAccess', _30 => _30["array-without-items"]]) || "error", | ||
| "duplicate-non-request-body-parameters": _optionalChain([swaggerRules, 'optionalAccess', _31 => _31["duplicate-non-request-body-parameters"]]) || "error", | ||
| "duplicate-operation-id": _optionalChain([swaggerRules, 'optionalAccess', _32 => _32["duplicate-operation-id"]]) || "error", | ||
| "invalid-security-scheme-properties": _optionalChain([swaggerRules, 'optionalAccess', _33 => _33["invalid-security-scheme-properties"]]) || "error", | ||
| "non-optional-path-parameters": _optionalChain([swaggerRules, 'optionalAccess', _34 => _34["non-optional-path-parameters"]]) || "error", | ||
| "path-parameters-not-in-parameters": _optionalChain([swaggerRules, 'optionalAccess', _35 => _35["path-parameters-not-in-parameters"]]) || "error", | ||
| "path-parameters-not-in-path": _optionalChain([swaggerRules, 'optionalAccess', _36 => _36["path-parameters-not-in-path"]]) || "error", | ||
| "unknown-required-schema-property": _optionalChain([swaggerRules, 'optionalAccess', _37 => _37["unknown-required-schema-property"]]) || "error" | ||
| } | ||
| }; | ||
| const { result: preSchemaResult, flaggedInstancePaths } = validateSpecPreSchema(parser.schema, rules); | ||
| if (!preSchemaResult.valid) { | ||
| return preSchemaResult; | ||
| } | ||
| result = validateSchema(parser.schema, options, flaggedInstancePaths); | ||
| if (!result.valid) { | ||
| if (preSchemaResult.warnings.length) { | ||
| result.warnings = [...preSchemaResult.warnings, ...result.warnings]; | ||
| } | ||
| return result; | ||
| } | ||
| if (_optionalChain([parser, 'access', _14 => _14.$refs, 'optionalAccess', _15 => _15.circular])) { | ||
| if (_optionalChain([parser, 'access', _38 => _38.$refs, 'optionalAccess', _39 => _39.circular])) { | ||
| if (circular$RefOption === true) { | ||
@@ -883,23 +1124,6 @@ _jsonschemarefparser.dereferenceInternal.call(void 0, parser, parserOptions); | ||
| } | ||
| const openapiRules = _optionalChain([options, 'optionalAccess', _16 => _16.validate, 'optionalAccess', _17 => _17.rules, 'optionalAccess', _18 => _18.openapi]); | ||
| const swaggerRules = _optionalChain([options, 'optionalAccess', _19 => _19.validate, 'optionalAccess', _20 => _20.rules, 'optionalAccess', _21 => _21.swagger]); | ||
| result = validateSpec(parser.schema, { | ||
| openapi: { | ||
| "array-without-items": _optionalChain([openapiRules, 'optionalAccess', _22 => _22["array-without-items"]]) || "error", | ||
| "duplicate-non-request-body-parameters": _optionalChain([openapiRules, 'optionalAccess', _23 => _23["duplicate-non-request-body-parameters"]]) || "error", | ||
| "duplicate-operation-id": _optionalChain([openapiRules, 'optionalAccess', _24 => _24["duplicate-operation-id"]]) || "error", | ||
| "non-optional-path-parameters": _optionalChain([openapiRules, 'optionalAccess', _25 => _25["non-optional-path-parameters"]]) || "error", | ||
| "path-parameters-not-in-parameters": _optionalChain([openapiRules, 'optionalAccess', _26 => _26["path-parameters-not-in-parameters"]]) || "error", | ||
| "path-parameters-not-in-path": _optionalChain([openapiRules, 'optionalAccess', _27 => _27["path-parameters-not-in-path"]]) || "error" | ||
| }, | ||
| swagger: { | ||
| "array-without-items": _optionalChain([swaggerRules, 'optionalAccess', _28 => _28["array-without-items"]]) || "error", | ||
| "duplicate-non-request-body-parameters": _optionalChain([swaggerRules, 'optionalAccess', _29 => _29["duplicate-non-request-body-parameters"]]) || "error", | ||
| "duplicate-operation-id": _optionalChain([swaggerRules, 'optionalAccess', _30 => _30["duplicate-operation-id"]]) || "error", | ||
| "non-optional-path-parameters": _optionalChain([swaggerRules, 'optionalAccess', _31 => _31["non-optional-path-parameters"]]) || "error", | ||
| "path-parameters-not-in-parameters": _optionalChain([swaggerRules, 'optionalAccess', _32 => _32["path-parameters-not-in-parameters"]]) || "error", | ||
| "path-parameters-not-in-path": _optionalChain([swaggerRules, 'optionalAccess', _33 => _33["path-parameters-not-in-path"]]) || "error", | ||
| "unknown-required-schema-property": _optionalChain([swaggerRules, 'optionalAccess', _34 => _34["unknown-required-schema-property"]]) || "error" | ||
| } | ||
| }); | ||
| result = validateSpec(parser.schema, rules); | ||
| if (preSchemaResult.warnings.length) { | ||
| result.warnings = [...preSchemaResult.warnings, ...result.warnings]; | ||
| } | ||
| return result; | ||
@@ -906,0 +1130,0 @@ } |
+16
-0
@@ -44,2 +44,10 @@ import { JSONSchema4Object, JSONSchema6Object, JSONSchema7Object } from 'json-schema'; | ||
| /** | ||
| * Security schemes must only contain properties valid for their declared `type`. Catches | ||
| * common mistakes like an `http` scheme having a `name` (which belongs to `apiKey`) before | ||
| * AJV has a chance to surface confusing `oneOf` noise. The default is `error`. | ||
| * | ||
| * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object} | ||
| */ | ||
| 'invalid-security-scheme-properties': 'error' | 'warning'; | ||
| /** | ||
| * Parameters that are defined within the path URI must be specified as being `required`. The | ||
@@ -87,2 +95,10 @@ * default is `error`. | ||
| /** | ||
| * Security definitions must only contain properties valid for their declared `type`. Catches | ||
| * common mistakes like an `apiKey` definition with `in: cookie` (which is OAS 3.0+ only) | ||
| * before AJV has a chance to surface confusing `oneOf` noise. The default is `error`. | ||
| * | ||
| * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#security-scheme-object} | ||
| */ | ||
| 'invalid-security-scheme-properties': 'error' | 'warning'; | ||
| /** | ||
| * Parameters that are defined within the path URI must be specified as being `required`. The | ||
@@ -89,0 +105,0 @@ * default is `error`. |
+16
-0
@@ -44,2 +44,10 @@ import { JSONSchema4Object, JSONSchema6Object, JSONSchema7Object } from 'json-schema'; | ||
| /** | ||
| * Security schemes must only contain properties valid for their declared `type`. Catches | ||
| * common mistakes like an `http` scheme having a `name` (which belongs to `apiKey`) before | ||
| * AJV has a chance to surface confusing `oneOf` noise. The default is `error`. | ||
| * | ||
| * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object} | ||
| */ | ||
| 'invalid-security-scheme-properties': 'error' | 'warning'; | ||
| /** | ||
| * Parameters that are defined within the path URI must be specified as being `required`. The | ||
@@ -87,2 +95,10 @@ * default is `error`. | ||
| /** | ||
| * Security definitions must only contain properties valid for their declared `type`. Catches | ||
| * common mistakes like an `apiKey` definition with `in: cookie` (which is OAS 3.0+ only) | ||
| * before AJV has a chance to surface confusing `oneOf` noise. The default is `error`. | ||
| * | ||
| * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#security-scheme-object} | ||
| */ | ||
| 'invalid-security-scheme-properties': 'error' | 'warning'; | ||
| /** | ||
| * Parameters that are defined within the path URI must be specified as being `required`. The | ||
@@ -89,0 +105,0 @@ * default is `error`. |
+257
-33
@@ -110,4 +110,4 @@ import { | ||
| import { openapi } from "@readme/openapi-schemas"; | ||
| import AjvDraft4 from "ajv-draft-04"; | ||
| import Ajv from "ajv/dist/2020.js"; | ||
| import AjvDraft4 from "ajv-draft-04"; | ||
@@ -165,3 +165,3 @@ // src/lib/hasInvalidPaths.ts | ||
| } | ||
| function validateSchema(api, options = {}) { | ||
| function validateSchema(api, options = {}, suppressedInstancePaths = []) { | ||
| if (hasInvalidPaths(api)) { | ||
@@ -208,3 +208,8 @@ return { | ||
| let additionalErrors = 0; | ||
| let reducedErrors = reduceAjvErrors(ajv.errors); | ||
| let reducedErrors = reduceAjvErrors(ajv.errors).filter((err) => { | ||
| return !suppressedInstancePaths.some((path) => err.instancePath === path || err.instancePath.startsWith(`${path}/`)); | ||
| }); | ||
| if (!reducedErrors.length) { | ||
| return { valid: true, warnings: [], specification: specificationName }; | ||
| } | ||
| if (reducedErrors.length >= LARGE_SPEC_ERROR_CAP) { | ||
@@ -247,2 +252,7 @@ try { | ||
| warnings = []; | ||
| /** | ||
| * Instance paths flagged by `runPreSchemaChecks()`. Used to suppress AJV `oneOf` noise that | ||
| * the pre-schema validator has already produced a clearer error or warning for. | ||
| */ | ||
| flaggedInstancePaths = []; | ||
| reportError(message) { | ||
@@ -254,2 +264,7 @@ this.errors.push({ message }); | ||
| } | ||
| flagInstancePath(path) { | ||
| if (!this.flaggedInstancePaths.includes(path)) { | ||
| this.flaggedInstancePaths.push(path); | ||
| } | ||
| } | ||
| }; | ||
@@ -266,2 +281,5 @@ | ||
| } | ||
| runPreSchemaChecks() { | ||
| this.checkSecuritySchemes(); | ||
| } | ||
| run() { | ||
@@ -482,2 +500,115 @@ const operationIds = []; | ||
| /** | ||
| * Validates security schemes in `components.securitySchemes` against their declared `type`. | ||
| * | ||
| * AJV uses a `oneOf` schema to validate security schemes, so when a scheme is malformed AJV | ||
| * fails every branch and produces overwhelming, unhelpful errors. This pre-AJV pass surfaces | ||
| * a single targeted error per problem. | ||
| * | ||
| * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object} | ||
| */ | ||
| checkSecuritySchemes() { | ||
| const securitySchemes = this.api.components?.securitySchemes; | ||
| if (!securitySchemes) { | ||
| return; | ||
| } | ||
| const schemeTypeProps = { | ||
| apiKey: { | ||
| required: ["name", "in"], | ||
| foreign: { scheme: "http", bearerFormat: "http", flows: "oauth2", openIdConnectUrl: "openIdConnect" } | ||
| }, | ||
| http: { | ||
| required: ["scheme"], | ||
| foreign: { name: "apiKey", in: "apiKey", flows: "oauth2", openIdConnectUrl: "openIdConnect" } | ||
| }, | ||
| oauth2: { | ||
| required: ["flows"], | ||
| foreign: { | ||
| name: "apiKey", | ||
| in: "apiKey", | ||
| scheme: "http", | ||
| bearerFormat: "http", | ||
| openIdConnectUrl: "openIdConnect" | ||
| } | ||
| }, | ||
| openIdConnect: { | ||
| required: ["openIdConnectUrl"], | ||
| foreign: { name: "apiKey", in: "apiKey", scheme: "http", bearerFormat: "http", flows: "oauth2" } | ||
| }, | ||
| mutualTLS: { | ||
| required: [], | ||
| foreign: { | ||
| name: "apiKey", | ||
| in: "apiKey", | ||
| scheme: "http", | ||
| bearerFormat: "http", | ||
| flows: "oauth2", | ||
| openIdConnectUrl: "openIdConnect" | ||
| } | ||
| } | ||
| }; | ||
| Object.keys(securitySchemes).forEach((name) => { | ||
| const scheme = securitySchemes[name]; | ||
| if ("$ref" in scheme) { | ||
| return; | ||
| } | ||
| const schemeId = `/components/securitySchemes/${name}`; | ||
| const reportIssue = (message) => this.reportSecuritySchemeIssue(message, schemeId); | ||
| if (!("type" in scheme) || !scheme.type) { | ||
| reportIssue( | ||
| `\`${schemeId}\` is missing required property \`type\`. Must be one of: \`apiKey\`, \`http\`, \`oauth2\`, \`openIdConnect\`, \`mutualTLS\`.` | ||
| ); | ||
| return; | ||
| } | ||
| const type = scheme.type; | ||
| if (type === "basic") { | ||
| reportIssue( | ||
| `\`${schemeId}\` uses \`type: basic\`, which is a Swagger 2.0 value. In OpenAPI 3.x use \`type: http\` with \`scheme: basic\` instead.` | ||
| ); | ||
| return; | ||
| } | ||
| if (!(type in schemeTypeProps)) { | ||
| reportIssue( | ||
| `\`${schemeId}\` has an invalid \`type\`: \`${type}\`. Must be one of: \`apiKey\`, \`http\`, \`oauth2\`, \`openIdConnect\`, \`mutualTLS\`.` | ||
| ); | ||
| return; | ||
| } | ||
| const config = schemeTypeProps[type]; | ||
| config.required.forEach((prop) => { | ||
| if (!(prop in scheme)) { | ||
| reportIssue(`\`${schemeId}\` (\`type: ${type}\`) is missing required property \`${prop}\`.`); | ||
| } | ||
| }); | ||
| Object.entries(config.foreign).forEach(([prop, ownerType]) => { | ||
| if (prop in scheme) { | ||
| reportIssue( | ||
| `\`${schemeId}\` (\`type: ${type}\`) includes \`${prop}\`, which is only valid for \`type: ${ownerType}\` schemes.` | ||
| ); | ||
| } | ||
| }); | ||
| if (type === "apiKey" && typeof scheme.in === "string") { | ||
| const validIn = ["query", "header", "cookie"]; | ||
| if (!validIn.includes(scheme.in)) { | ||
| reportIssue( | ||
| `\`${schemeId}\` has an invalid \`in\` value: \`${scheme.in}\`. Must be one of: \`query\`, \`header\`, \`cookie\`.` | ||
| ); | ||
| } | ||
| } | ||
| if (type === "oauth2" && scheme.flows && typeof scheme.flows === "object") { | ||
| if (Object.keys(scheme.flows).length === 0) { | ||
| reportIssue( | ||
| `\`${schemeId}\` has empty \`flows\`. At least one grant type is required: \`implicit\`, \`password\`, \`clientCredentials\`, or \`authorizationCode\`.` | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| reportSecuritySchemeIssue(message, schemeId) { | ||
| this.flagInstancePath(schemeId); | ||
| if (this.rules["invalid-security-scheme-properties"] === "warning") { | ||
| this.reportWarning(message); | ||
| } else { | ||
| this.reportError(message); | ||
| } | ||
| } | ||
| /** | ||
| * Checks the given parameter list for duplicates. | ||
@@ -516,2 +647,5 @@ * | ||
| } | ||
| runPreSchemaChecks() { | ||
| this.checkSecurityDefinitions(); | ||
| } | ||
| run() { | ||
@@ -733,19 +867,5 @@ const operationIds = []; | ||
| validateRequiredPropertiesExist(schema, schemaId) { | ||
| function collectProperties(schemaObj, props) { | ||
| if (schemaObj.properties) { | ||
| Object.keys(schemaObj.properties).forEach((property) => { | ||
| if (schemaObj.properties.hasOwnProperty(property)) { | ||
| props[property] = schemaObj.properties[property]; | ||
| } | ||
| }); | ||
| } | ||
| if (schemaObj.allOf) { | ||
| schemaObj.allOf.forEach((parent) => { | ||
| collectProperties(parent, props); | ||
| }); | ||
| } | ||
| } | ||
| if (schema.required && Array.isArray(schema.required)) { | ||
| const props = {}; | ||
| collectProperties(schema, props); | ||
| this.collectProperties(schema, props); | ||
| schema.required.forEach((requiredProperty) => { | ||
@@ -764,2 +884,62 @@ if (!props[requiredProperty]) { | ||
| /** | ||
| * Recursively collects all properties of a schema and its ancestors. They are added to the | ||
| * supplied `props` object. | ||
| * | ||
| */ | ||
| collectProperties(schemaObj, props) { | ||
| if (schemaObj.properties) { | ||
| Object.keys(schemaObj.properties).forEach((property) => { | ||
| if (schemaObj.properties.hasOwnProperty(property)) { | ||
| props[property] = schemaObj.properties[property]; | ||
| } | ||
| }); | ||
| } | ||
| if (schemaObj.allOf) { | ||
| schemaObj.allOf.forEach((parent) => { | ||
| this.collectProperties(parent, props); | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Validates security definitions against their declared `type`. | ||
| * | ||
| * AJV uses `oneOf` to validate `securityDefinitions`, so when a definition is malformed AJV | ||
| * fails every branch and produces overwhelming, unhelpful errors. This pre-AJV pass surfaces | ||
| * a single targeted error per problem. | ||
| * | ||
| * @see {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#security-scheme-object} | ||
| */ | ||
| checkSecurityDefinitions() { | ||
| const securityDefinitions = this.api.securityDefinitions; | ||
| if (!securityDefinitions) { | ||
| return; | ||
| } | ||
| const validApiKeyIn = ["query", "header"]; | ||
| Object.keys(securityDefinitions).forEach((name) => { | ||
| const definition = securityDefinitions[name]; | ||
| const definitionId = `/securityDefinitions/${name}`; | ||
| const reportIssue = (message) => this.reportSecuritySchemeIssue(message, definitionId); | ||
| const type = definition.type; | ||
| if (type === "http") { | ||
| reportIssue( | ||
| `\`${definitionId}\` uses \`type: http\`, which is an OpenAPI 3.x value. In Swagger 2.0 use \`type: basic\` for HTTP Basic auth.` | ||
| ); | ||
| return; | ||
| } | ||
| if (type === "apiKey" && typeof definition.in === "string" && !validApiKeyIn.includes(definition.in)) { | ||
| reportIssue( | ||
| `\`${definitionId}\` has an invalid \`in\` value: \`${definition.in}\`. Swagger 2.0 only supports \`query\` or \`header\`.` | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
| reportSecuritySchemeIssue(message, definitionId) { | ||
| this.flagInstancePath(definitionId); | ||
| if (this.rules["invalid-security-scheme-properties"] === "warning") { | ||
| this.reportWarning(message); | ||
| } else { | ||
| this.reportError(message); | ||
| } | ||
| } | ||
| /** | ||
| * Checks the given parameter list for duplicates. | ||
@@ -811,2 +991,33 @@ * | ||
| } | ||
| function validateSpecPreSchema(api, rules) { | ||
| let validator; | ||
| const specificationName = getSpecificationName(api); | ||
| if (isOpenAPI(api)) { | ||
| validator = new OpenAPISpecificationValidator(api, rules.openapi); | ||
| } else { | ||
| validator = new SwaggerSpecificationValidator(api, rules.swagger); | ||
| } | ||
| validator.runPreSchemaChecks(); | ||
| const flaggedInstancePaths = validator.flaggedInstancePaths; | ||
| if (!validator.errors.length) { | ||
| return { | ||
| flaggedInstancePaths, | ||
| result: { | ||
| valid: true, | ||
| warnings: validator.warnings, | ||
| specification: specificationName | ||
| } | ||
| }; | ||
| } | ||
| return { | ||
| flaggedInstancePaths, | ||
| result: { | ||
| valid: false, | ||
| errors: validator.errors, | ||
| warnings: validator.warnings, | ||
| additionalErrors: 0, | ||
| specification: specificationName | ||
| } | ||
| }; | ||
| } | ||
@@ -869,18 +1080,5 @@ // src/index.ts | ||
| parserOptions.dereference.circular = circular$RefOption; | ||
| result = validateSchema(parser.schema, options); | ||
| if (!result.valid) { | ||
| return result; | ||
| } | ||
| if (parser.$refs?.circular) { | ||
| if (circular$RefOption === true) { | ||
| dereferenceInternal(parser, parserOptions); | ||
| } else if (circular$RefOption === false) { | ||
| throw new ReferenceError( | ||
| "The API contains circular references but the validator is configured to not permit them." | ||
| ); | ||
| } | ||
| } | ||
| const openapiRules = options?.validate?.rules?.openapi; | ||
| const swaggerRules = options?.validate?.rules?.swagger; | ||
| result = validateSpec(parser.schema, { | ||
| const rules = { | ||
| openapi: { | ||
@@ -890,2 +1088,3 @@ "array-without-items": openapiRules?.["array-without-items"] || "error", | ||
| "duplicate-operation-id": openapiRules?.["duplicate-operation-id"] || "error", | ||
| "invalid-security-scheme-properties": openapiRules?.["invalid-security-scheme-properties"] || "error", | ||
| "non-optional-path-parameters": openapiRules?.["non-optional-path-parameters"] || "error", | ||
@@ -899,2 +1098,3 @@ "path-parameters-not-in-parameters": openapiRules?.["path-parameters-not-in-parameters"] || "error", | ||
| "duplicate-operation-id": swaggerRules?.["duplicate-operation-id"] || "error", | ||
| "invalid-security-scheme-properties": swaggerRules?.["invalid-security-scheme-properties"] || "error", | ||
| "non-optional-path-parameters": swaggerRules?.["non-optional-path-parameters"] || "error", | ||
@@ -905,3 +1105,27 @@ "path-parameters-not-in-parameters": swaggerRules?.["path-parameters-not-in-parameters"] || "error", | ||
| } | ||
| }); | ||
| }; | ||
| const { result: preSchemaResult, flaggedInstancePaths } = validateSpecPreSchema(parser.schema, rules); | ||
| if (!preSchemaResult.valid) { | ||
| return preSchemaResult; | ||
| } | ||
| result = validateSchema(parser.schema, options, flaggedInstancePaths); | ||
| if (!result.valid) { | ||
| if (preSchemaResult.warnings.length) { | ||
| result.warnings = [...preSchemaResult.warnings, ...result.warnings]; | ||
| } | ||
| return result; | ||
| } | ||
| if (parser.$refs?.circular) { | ||
| if (circular$RefOption === true) { | ||
| dereferenceInternal(parser, parserOptions); | ||
| } else if (circular$RefOption === false) { | ||
| throw new ReferenceError( | ||
| "The API contains circular references but the validator is configured to not permit them." | ||
| ); | ||
| } | ||
| } | ||
| result = validateSpec(parser.schema, rules); | ||
| if (preSchemaResult.warnings.length) { | ||
| result.warnings = [...preSchemaResult.warnings, ...result.warnings]; | ||
| } | ||
| return result; | ||
@@ -908,0 +1132,0 @@ } |
+2
-4
| { | ||
| "name": "@readme/openapi-parser", | ||
| "version": "6.0.1", | ||
| "version": "6.1.0", | ||
| "description": "Swagger 2.0 and OpenAPI 3.x parser and validator for Node and browsers", | ||
@@ -81,5 +81,3 @@ "license": "MIT", | ||
| "vitest": "^4.0.8" | ||
| }, | ||
| "prettier": "@readme/standards/prettier", | ||
| "gitHead": "766e5550ef968794a0d275bda5c47631a927da04" | ||
| } | ||
| } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
298633
19.51%2587
21.86%