@whook/whook
Version:
Build strong and efficient REST web services.
285 lines • 11.8 kB
JavaScript
import { noop } from 'common-services';
import { YError } from 'yerror';
import { YHTTPError } from 'yhttperror';
import Stream from 'node:stream';
import { ensureResolvedObject, } from 'ya-open-api-types';
import { parseArrayOfBooleans, parseArrayOfNumbers, parseArrayOfStrings, parseBoolean, parseNumber, } from './coercion.js';
import { identity } from './utils.js';
export async function prepareBodyValidator({ API, schemaValidators, }, operation) {
if (!('requestBody' in operation) || !operation.requestBody) {
return rejectAnyRequestBody;
}
const requestBodyObject = await ensureResolvedObject(API, operation.requestBody);
if (!requestBodyObject.content) {
return rejectAnyRequestBody;
}
const bodyValidators = {};
for (const mediaType of Object.keys(requestBodyObject.content)) {
const mediaTypeObject = requestBodyObject.content[mediaType];
if (!('schema' in mediaTypeObject) || !mediaTypeObject.schema) {
continue;
}
const schema = (await ensureResolvedObject(API, mediaTypeObject.schema));
const isBinaryContent = typeof schema === 'object' &&
schema &&
'type' in schema &&
schema.type === 'string' &&
schema.format === 'binary';
if (isBinaryContent) {
continue;
}
try {
bodyValidators[mediaType] = schemaValidators(mediaTypeObject.schema);
}
catch (err) {
throw YError.wrap(err, 'E_BAD_BODY_SCHEMA', operation.operationId, mediaType);
}
}
return validateRequestBody.bind(null, bodyValidators, !!requestBodyObject.required);
}
function validateRequestBody(bodyValidators, required, operation, contentType, body) {
if ('undefined' === typeof body) {
if (required) {
throw new YHTTPError(400, 'E_REQUIRED_REQUEST_BODY', operation.operationId, typeof body, body);
}
return;
}
// Streamed contents, let it pass
if (!bodyValidators[contentType]) {
return;
}
if (!bodyValidators[contentType](body)) {
throw new YHTTPError(400, 'E_BAD_REQUEST_BODY', operation.operationId, typeof body, body instanceof Stream ? 'Stream' : body, bodyValidators[contentType].errors);
}
}
function rejectAnyRequestBody(operation, _contentType, body) {
if ('undefined' !== typeof body) {
throw new YHTTPError(400, 'E_NO_REQUEST_BODY', operation.operationId, typeof body, body instanceof Stream ? 'Stream' : body);
}
}
// Supporting only mainstream schemes
// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
const SUPPORTED_HTTP_SCHEMES = ['basic', 'bearer', 'digest'];
export async function extractOperationSecurityParameters({ API }, operation) {
const operationSecuritySchemes = await pickupOperationSecuritySchemes({ API }, operation);
const securitySchemes = Object.keys(operationSecuritySchemes).map((schemeKey) => operationSecuritySchemes[schemeKey]);
return extractParametersFromSecuritySchemes(securitySchemes);
}
export async function pickupOperationSecuritySchemes({ API }, operation) {
const securitySchemes = (API.components && API.components.securitySchemes) || {};
const operationSecuritySchemes = {};
for (const security of operation.security || API.security || []) {
const schemeKey = Object.keys(security)[0];
if (!schemeKey) {
continue;
}
if (!securitySchemes[schemeKey]) {
throw new YError('E_UNDECLARED_SECURITY_SCHEME', schemeKey, operation.operationId);
}
operationSecuritySchemes[schemeKey] = await ensureResolvedObject({ API }, securitySchemes[schemeKey]);
}
return operationSecuritySchemes;
}
export function extractParametersFromSecuritySchemes(securitySchemes) {
const hasOAuth = securitySchemes.some((securityScheme) => ['oauth2', 'openIdConnect'].includes(securityScheme.type));
const httpSchemes = [
...new Set([
...securitySchemes
.filter((securityScheme) => securityScheme.type === 'http')
.map((securityScheme) => {
if (!SUPPORTED_HTTP_SCHEMES.includes(securityScheme.scheme)) {
throw new YError('E_UNSUPPORTED_HTTP_SCHEME', securityScheme.scheme);
}
return securityScheme.scheme;
}),
...(hasOAuth ? ['bearer'] : []),
]),
];
const hasBearerAuth = httpSchemes.includes('bearer');
let hasAuthorizationApiKey = false;
let hasAccessTokenApiKey = false;
const securityParameters = securitySchemes
.filter((securityScheme) => securityScheme.type === 'apiKey')
.map((securityScheme) => {
if (securityScheme.in === 'cookie') {
throw new YError('E_UNSUPPORTED_API_KEY_SOURCE', 'cookie', securityScheme.name);
}
// This overlaps with OAuth/HTTP schemes
if (securityScheme.in === 'header' &&
securityScheme.name.toLowerCase() === 'authorization') {
hasAuthorizationApiKey = true;
}
// This overlaps with OAuth and BearerAuth schemes
if (securityScheme.in === 'query' &&
securityScheme.name === 'access_token') {
hasAccessTokenApiKey = true;
}
return {
in: securityScheme.in,
name: securityScheme.in === 'header'
? securityScheme.name.toLowerCase()
: securityScheme.name,
schema: {
type: 'string',
},
};
})
.concat(httpSchemes.length && !hasAuthorizationApiKey
? [
{
in: 'header',
name: 'authorization',
schema: {
type: 'string',
pattern: `(${httpSchemes
.map((httpScheme) => `(${httpScheme[0]}|${httpScheme[0].toUpperCase()})${httpScheme.slice(1)}`)
.join('|')}) .*`,
},
},
]
: [])
.concat(hasBearerAuth && !hasAccessTokenApiKey
? [
{
in: 'query',
name: 'access_token',
schema: {
type: 'string',
},
},
]
: []);
return securityParameters;
}
export async function resolveParameters({ API, log = noop, }, parameters) {
const resolvedParameters = [];
for (const parameter of parameters) {
const resolvedParameter = await ensureResolvedObject(API, parameter);
if ('style' in resolvedParameter) {
log('warning', '⚠️ - Only defaults styles are supported currently!');
log('debug', JSON.stringify(resolvedParameter));
}
if ('string' !== typeof resolvedParameter.name) {
throw new YError('E_BAD_PARAMETER_NAME', resolvedParameter);
}
if ('content' in resolvedParameter) {
throw new YError('E_UNSUPPORTED_PARAMETER_DEFINITION', resolvedParameter.name, 'content');
}
if ('style' in resolvedParameter && 'simple' !== resolvedParameter.style) {
throw new YError('E_UNSUPPORTED_PARAMETER_DEFINITION', resolvedParameter.name, 'style', resolvedParameter.style);
}
if (!['query', 'header', 'path'].includes(resolvedParameter.in)) {
throw new YError('E_UNSUPPORTED_PARAMETER_DEFINITION', resolvedParameter.name, 'in', resolvedParameter.in);
}
if (!resolvedParameter.schema) {
throw new YError('E_PARAMETER_WITHOUT_SCHEMA', resolvedParameter.name);
}
resolvedParameters.push(resolvedParameter);
}
return resolvedParameters;
}
export async function getCasterForSchema({ API, COERCION_OPTIONS, }, schema) {
const resolvedSchema = (await ensureResolvedObject(API, schema));
if (!('type' in resolvedSchema && resolvedSchema.type)) {
throw new YError('E_UNSUPPORTED_SCHEMA', resolvedSchema);
}
if (resolvedSchema.type === 'number') {
return parseNumber.bind(null, COERCION_OPTIONS);
}
else if (resolvedSchema.type === 'boolean') {
return parseBoolean;
}
else if (resolvedSchema.type === 'string') {
return identity;
}
else if (resolvedSchema.type === 'array') {
if (!('items' in resolvedSchema && resolvedSchema.items) ||
'prefixItems' in resolvedSchema) {
throw new YError('E_UNSUPPORTED_SCHEMA', resolvedSchema);
}
const itemSchema = (await ensureResolvedObject(API, resolvedSchema.items));
if (!('type' in itemSchema && itemSchema.type)) {
throw new YError('E_UNSUPPORTED_PARAMETER_SCHEMA', resolvedSchema);
}
if (itemSchema.type === 'string') {
return parseArrayOfStrings;
}
else if (itemSchema.type === 'number') {
return parseArrayOfNumbers.bind(null, COERCION_OPTIONS);
}
else if (itemSchema.type === 'boolean') {
return parseArrayOfBooleans;
}
}
throw new YError('E_UNSUPPORTED_PARAMETER_SCHEMA', resolvedSchema);
}
export async function createParameterValidator({ API, COERCION_OPTIONS, schemaValidators, }, parameter) {
let validator;
let caster = undefined;
const schema = (await ensureResolvedObject(API, parameter.schema));
if (!('type' in schema && schema.type)) {
throw new YError('E_UNSUPPORTED_PARAMETER_SCHEMA', parameter);
}
if (schema.type === 'number') {
caster = parseNumber.bind(null, COERCION_OPTIONS);
}
else if (schema.type === 'boolean') {
caster = parseBoolean;
}
else if (schema.type === 'array') {
if (!('items' in schema && schema.items) || 'prefixItems' in schema) {
throw new YError('E_UNSUPPORTED_PARAMETER_SCHEMA', parameter);
}
const itemSchema = (await ensureResolvedObject(API, schema.items));
if (!('type' in itemSchema && itemSchema.type)) {
throw new YError('E_UNSUPPORTED_PARAMETER_SCHEMA', parameter);
}
if (itemSchema.type === 'string') {
caster = parseArrayOfStrings;
}
else if (itemSchema.type === 'number') {
caster = parseArrayOfNumbers.bind(null, COERCION_OPTIONS);
}
else if (itemSchema.type === 'boolean') {
caster = parseArrayOfBooleans;
}
}
try {
validator = schemaValidators(parameter.schema);
}
catch (err) {
throw YError.wrap(err, 'E_BAD_PARAMETER_SCHEMA', parameter.name);
}
return validateParameter.bind(null, parameter, caster, validator);
}
export async function createParametersValidators({ API, COERCION_OPTIONS, schemaValidators, }, parameters) {
const parameterValidators = {
query: {},
header: {},
path: {},
cookie: {},
};
for (const parameter of parameters) {
parameterValidators[parameter.in][parameter.name] =
await createParameterValidator({
API,
COERCION_OPTIONS,
schemaValidators,
}, parameter);
}
return parameterValidators;
}
export function validateParameter(parameter, caster, validator, str) {
if ('undefined' === typeof str) {
if (parameter.required) {
throw new YHTTPError(400, 'E_REQUIRED_PARAMETER', parameter.name, typeof str, str);
}
return undefined;
}
const value = caster ? caster(str) : str;
if (!validator(value)) {
throw new YHTTPError(400, 'E_BAD_PARAMETER', parameter.name, typeof value, value, validator.errors);
}
return value;
}
//# sourceMappingURL=validation.js.map