New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@readme/openapi-parser

Package Overview
Dependencies
Maintainers
10
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@readme/openapi-parser - npm Package Compare versions

Comparing version 1.2.1 to 2.0.0

lib/validators/spec/openapi.js

28

lib/validators/schema.js

@@ -96,7 +96,4 @@ const { ono } = require('@jsdevtools/ono');

const flattened = new Map();
const instances = errors.sort((a, b) => {
return a.instancePath.match(/\//g).length > b.instancePath.match(/\//g).length ? 1 : -1;
});
instances.forEach(err => {
errors.forEach(err => {
// These two errors appear when a child schema of them has a problem and instead of polluting the user with

@@ -109,19 +106,24 @@ // indecipherable noise we should instead relay the more specific error to them. If this is all that's present in

// If this is our first run through let's initialize our dataset and move along.
if (!flattened.size) {
flattened.set(err.instancePath, err);
return;
}
if (flattened.has(err.instancePath)) {
} else if (flattened.has(err.instancePath)) {
// If we already have an error recorded for this `instancePath` we can ignore it because we (likely) already have
// recorded the more specific error.
return;
}
const lookup = err.instancePath.split('/').slice(0, -1).join('/');
if (!flattened.has(lookup)) {
// If this error hasn't already been recorded, maybe it's an error against the same `instancePath` stack, in which
// case we should ignore it because the more specific error has already been recorded.
let shouldRecordError = true;
flattened.forEach(flat => {
if (flat.instancePath.includes(err.instancePath)) {
shouldRecordError = false;
}
});
if (shouldRecordError) {
flattened.set(err.instancePath, err);
return;
}
flattened.delete(lookup);
flattened.set(err.instancePath, err);
});

@@ -128,0 +130,0 @@

@@ -1,348 +0,16 @@

const util = require('../util');
const { ono } = require('@jsdevtools/ono');
const swaggerMethods = require('@apidevtools/swagger-methods');
const validateSwagger = require('./spec/swagger');
const validateOpenAPI = require('./spec/openapi');
const primitiveTypes = ['array', 'boolean', 'integer', 'number', 'string'];
const schemaTypes = ['array', 'boolean', 'integer', 'number', 'string', 'object', 'null', undefined];
module.exports = validateSpec;
/**
* Validates parts of the Swagger 2.0 spec that aren't covered by the Swagger 2.0 JSON Schema.
* Validates either a Swagger 2.0 or OpenAPI 3.x API definition against cases that aren't covered by their JSON Schema
* definitions.
*
* @param {SwaggerObject} api
*/
function validateSpec(api) {
module.exports = function validateSpec(api) {
if (api.openapi) {
// We don't (yet) support validating against the OpenAPI spec
return;
return validateOpenAPI(api);
}
const paths = Object.keys(api.paths || {});
const operationIds = [];
for (const pathName of paths) {
const path = api.paths[pathName];
const pathId = `/paths${pathName}`;
if (path && pathName.indexOf('/') === 0) {
validatePath(api, path, pathId, operationIds);
}
}
const definitions = Object.keys(api.definitions || {});
for (const definitionName of definitions) {
const definition = api.definitions[definitionName];
const definitionId = `/definitions/${definitionName}`;
validateRequiredPropertiesExist(definition, definitionId);
}
}
/**
* Validates the given path.
*
* @param {SwaggerObject} api - The entire Swagger API object
* @param {object} path - A Path object, from the Swagger API
* @param {string} pathId - A value that uniquely identifies the path
* @param {string} operationIds - An array of collected operationIds found in other paths
*/
function validatePath(api, path, pathId, operationIds) {
for (const operationName of swaggerMethods) {
const operation = path[operationName];
const operationId = `${pathId}/${operationName}`;
if (operation) {
const declaredOperationId = operation.operationId;
if (declaredOperationId) {
if (operationIds.indexOf(declaredOperationId) === -1) {
operationIds.push(declaredOperationId);
} else {
throw ono.syntax(`Validation failed. Duplicate operation id '${declaredOperationId}'`);
}
}
validateParameters(api, path, pathId, operation, operationId);
const responses = Object.keys(operation.responses || {});
for (const responseName of responses) {
const response = operation.responses[responseName];
const responseId = `${operationId}/responses/${responseName}`;
validateResponse(responseName, response || {}, responseId);
}
}
}
}
/**
* Validates the parameters for the given operation.
*
* @param {SwaggerObject} api - The entire Swagger API object
* @param {object} path - A Path object, from the Swagger API
* @param {string} pathId - A value that uniquely identifies the path
* @param {object} operation - An Operation object, from the Swagger API
* @param {string} operationId - A value that uniquely identifies the operation
*/
function validateParameters(api, path, pathId, operation, operationId) {
const pathParams = path.parameters || [];
const operationParams = operation.parameters || [];
// Check for duplicate path parameters
try {
checkForDuplicates(pathParams);
} catch (e) {
throw ono.syntax(e, `Validation failed. ${pathId} has duplicate parameters`);
}
// Check for duplicate operation parameters
try {
checkForDuplicates(operationParams);
} catch (e) {
throw ono.syntax(e, `Validation failed. ${operationId} has duplicate parameters`);
}
// Combine the path and operation parameters,
// with the operation params taking precedence over the path params
const params = pathParams.reduce((combinedParams, value) => {
const duplicate = combinedParams.some(param => {
return param.in === value.in && param.name === value.name;
});
if (!duplicate) {
combinedParams.push(value);
}
return combinedParams;
}, operationParams.slice());
validateBodyParameters(params, operationId);
validatePathParameters(params, pathId, operationId);
validateParameterTypes(params, api, operation, operationId);
}
/**
* Validates body and formData parameters for the given operation.
*
* @param {object[]} params - An array of Parameter objects
* @param {string} operationId - A value that uniquely identifies the operation
*/
function validateBodyParameters(params, operationId) {
const bodyParams = params.filter(param => {
return param.in === 'body';
});
const formParams = params.filter(param => {
return param.in === 'formData';
});
// There can only be one "body" parameter
if (bodyParams.length > 1) {
throw ono.syntax(
`Validation failed. ${operationId} has ${bodyParams.length} body parameters. Only one is allowed.`
);
} else if (bodyParams.length > 0 && formParams.length > 0) {
// "body" params and "formData" params are mutually exclusive
throw ono.syntax(
`Validation failed. ${operationId} has body parameters and formData parameters. Only one or the other is allowed.`
);
}
}
/**
* Validates path parameters for the given path.
*
* @param {object[]} params - An array of Parameter objects
* @param {string} pathId - A value that uniquely identifies the path
* @param {string} operationId - A value that uniquely identifies the operation
*/
function validatePathParameters(params, pathId, operationId) {
// Find all {placeholders} in the path string
const placeholders = pathId.match(util.swaggerParamRegExp) || [];
// Check for duplicates
for (let i = 0; i < placeholders.length; i++) {
for (let j = i + 1; j < placeholders.length; j++) {
if (placeholders[i] === placeholders[j]) {
throw ono.syntax(`Validation failed. ${operationId} has multiple path placeholders named ${placeholders[i]}`);
}
}
}
// eslint-disable-next-line no-param-reassign
params = params.filter(param => {
return param.in === 'path';
});
for (const param of params) {
if (param.required !== true) {
throw ono.syntax(
'Validation failed. Path parameters cannot be optional. ' +
`Set required=true for the "${param.name}" parameter at ${operationId}`
);
}
const match = placeholders.indexOf(`{${param.name}}`);
if (match === -1) {
throw ono.syntax(
`Validation failed. ${operationId} has a path parameter named "${param.name}", ` +
`but there is no corresponding {${param.name}} in the path string`
);
}
placeholders.splice(match, 1);
}
if (placeholders.length > 0) {
throw ono.syntax(`Validation failed. ${operationId} is missing path parameter(s) for ${placeholders}`);
}
}
/**
* Validates data types of parameters for the given operation.
*
* @param {object[]} params - An array of Parameter objects
* @param {object} api - The entire Swagger API object
* @param {object} operation - An Operation object, from the Swagger API
* @param {string} operationId - A value that uniquely identifies the operation
*/
function validateParameterTypes(params, api, operation, operationId) {
for (const param of params) {
const parameterId = `${operationId}/parameters/${param.name}`;
let schema;
let validTypes;
switch (param.in) {
case 'body':
schema = param.schema;
validTypes = schemaTypes;
break;
case 'formData':
schema = param;
validTypes = primitiveTypes.concat('file');
break;
default:
schema = param;
validTypes = primitiveTypes;
}
validateSchema(schema, parameterId, validTypes);
validateRequiredPropertiesExist(schema, parameterId);
if (schema.type === 'file') {
// "file" params must consume at least one of these MIME types
const formData = /multipart\/(.*\+)?form-data/; // eslint-disable-line unicorn/no-unsafe-regex
const urlEncoded = /application\/(.*\+)?x-www-form-urlencoded/; // eslint-disable-line unicorn/no-unsafe-regex
const consumes = operation.consumes || api.consumes || [];
const hasValidMimeType = consumes.some(consume => {
return formData.test(consume) || urlEncoded.test(consume);
});
if (!hasValidMimeType) {
throw ono.syntax(
`Validation failed. ${operationId} has a file parameter, so it must consume multipart/form-data ` +
'or application/x-www-form-urlencoded'
);
}
}
}
}
/**
* Checks the given parameter list for duplicates, and throws an error if found.
*
* @param {object[]} params - An array of Parameter objects
*/
function checkForDuplicates(params) {
for (let i = 0; i < params.length - 1; i++) {
const outer = params[i];
for (let j = i + 1; j < params.length; j++) {
const inner = params[j];
if (outer.name === inner.name && outer.in === inner.in) {
throw ono.syntax(`Validation failed. Found multiple ${outer.in} parameters named "${outer.name}"`);
}
}
}
}
/**
* Validates the given response object.
*
* @param {string} code - The HTTP response code (or "default")
* @param {object} response - A Response object, from the Swagger API
* @param {string} responseId - A value that uniquely identifies the response
*/
function validateResponse(code, response, responseId) {
if (code !== 'default' && (code < 100 || code > 599)) {
throw ono.syntax(`Validation failed. ${responseId} has an invalid response code (${code})`);
}
const headers = Object.keys(response.headers || {});
for (const headerName of headers) {
const header = response.headers[headerName];
const headerId = `${responseId}/headers/${headerName}`;
validateSchema(header, headerId, primitiveTypes);
}
if (response.schema) {
const validTypes = schemaTypes.concat('file');
if (validTypes.indexOf(response.schema.type) === -1) {
throw ono.syntax(
`Validation failed. ${responseId} has an invalid response schema type (${response.schema.type})`
);
} else {
validateSchema(response.schema, `${responseId}/schema`, validTypes);
}
}
}
/**
* Validates the given Swagger schema object.
*
* @param {object} schema - A Schema object, from the Swagger API
* @param {string} schemaId - A value that uniquely identifies the schema object
* @param {string[]} validTypes - An array of the allowed schema types
*/
function validateSchema(schema, schemaId, validTypes) {
if (validTypes.indexOf(schema.type) === -1) {
throw ono.syntax(`Validation failed. ${schemaId} has an invalid type (${schema.type})`);
}
if (schema.type === 'array' && !schema.items) {
throw ono.syntax(`Validation failed. ${schemaId} is an array, so it must include an "items" schema`);
}
}
/**
* Validates that the declared properties of the given Swagger schema object actually exist.
*
* @param {object} schema - A Schema object, from the Swagger API
* @param {string} schemaId - A value that uniquely identifies the schema object
*/
function validateRequiredPropertiesExist(schema, schemaId) {
/**
* Recursively collects all properties of the schema and its ancestors. They are added to the props object.
*/
function collectProperties(schemaObj, props) {
if (schemaObj.properties) {
for (const property in schemaObj.properties) {
// eslint-disable-next-line no-prototype-builtins
if (schemaObj.properties.hasOwnProperty(property)) {
// eslint-disable-next-line no-param-reassign
props[property] = schemaObj.properties[property];
}
}
}
if (schemaObj.allOf) {
for (const parent of schemaObj.allOf) {
collectProperties(parent, props);
}
}
}
if (schema.required && Array.isArray(schema.required)) {
const props = {};
collectProperties(schema, props);
for (const requiredProperty of schema.required) {
if (!props[requiredProperty]) {
throw ono.syntax(
`Validation failed. Property '${requiredProperty}' listed as required but does not exist in '${schemaId}'`
);
}
}
}
}
return validateSwagger(api);
};
{
"name": "@readme/openapi-parser",
"version": "1.2.1",
"version": "2.0.0",
"description": "Swagger 2.0 and OpenAPI 3.x parser and validator for Node and browsers",

@@ -37,2 +37,5 @@ "keywords": [

],
"engines": {
"node": "^12 || ^14 || ^16"
},
"scripts": {

@@ -53,8 +56,8 @@ "clean": "shx rm -rf .nyc_output coverage",

"@babel/polyfill": "^7.12.1",
"@commitlint/cli": "^15.0.0",
"@commitlint/config-conventional": "^15.0.0",
"@commitlint/cli": "^16.0.1",
"@commitlint/config-conventional": "^16.0.0",
"@jsdevtools/host-environment": "^2.1.2",
"@jsdevtools/karma-config": "^3.1.7",
"@readme/eslint-config": "^8.0.2",
"@types/node": "^16.11.0",
"@types/node": "^17.0.5",
"chai": "^4.3.4",

@@ -68,3 +71,3 @@ "eslint": "^8.3.0",

"nyc": "^15.1.0",
"openapi-types": "^9.3.0",
"openapi-types": "^10.0.0",
"prettier": "^2.5.0",

@@ -79,3 +82,3 @@ "shx": "^0.3.2",

"@jsdevtools/ono": "^7.1.3",
"@readme/better-ajv-errors": "^1.1.0",
"@readme/better-ajv-errors": "^1.4.0",
"@readme/json-schema-ref-parser": "^1.0.0",

@@ -82,0 +85,0 @@ "ajv": "^8.6.3",

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc