@bitblit/epsilon
Version:
Tiny adapter to simplify building API gateway Lambda APIS
253 lines • 14.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RouterUtil = void 0;
const misconfigured_error_1 = require("../error/misconfigured-error");
const common_1 = require("@bitblit/ratchet/common");
const common_2 = require("@bitblit/ratchet/common");
const null_returned_object_handling_1 = require("../../config/http/null-returned-object-handling");
const built_in_filters_1 = require("../../built-in/http/built-in-filters");
const built_in_handlers_1 = require("../../built-in/http/built-in-handlers");
const built_in_auth_filters_1 = require("../../built-in/http/built-in-auth-filters");
const log_level_manipulation_filter_1 = require("../../built-in/http/log-level-manipulation-filter");
/**
* Endpoints about the api itself
*/
class RouterUtil {
// Prevent instantiation
// eslint-disable-next-line @typescript-eslint/no-empty-function
constructor() { }
static defaultAuthenticationHeaderParsingEpsilonPreFilters(webTokenManipulator) {
return [
(fCtx) => built_in_auth_filters_1.BuiltInAuthFilters.parseAuthorizationHeader(fCtx, webTokenManipulator),
(fCtx) => built_in_auth_filters_1.BuiltInAuthFilters.applyOpenApiAuthorization(fCtx),
].concat(RouterUtil.defaultEpsilonPreFilters());
}
static defaultEpsilonPreFilters() {
return [
(fCtx) => built_in_filters_1.BuiltInFilters.autoRespondToOptionsRequestWithCors(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.ensureEventMaps(fCtx),
(fCtx) => log_level_manipulation_filter_1.LogLevelManipulationFilter.setLogLevelForTransaction(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.parseJsonBodyToObject(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.fixStillEncodedQueryParams(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.uriDecodeQueryParams(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.disallowStringNullAsPathParameter(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.disallowStringNullAsQueryStringParameter(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.validateInboundBody(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.validateInboundQueryParams(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.validateInboundQueryParams(fCtx),
];
}
static defaultEpsilonPostFilters() {
return [
(fCtx) => built_in_filters_1.BuiltInFilters.validateOutboundResponse(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.addAWSRequestIdHeader(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.addAllowReflectionCORSHeaders(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.applyGzipIfPossible(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.checkMaximumLambdaBodySize(fCtx),
(fCtx) => log_level_manipulation_filter_1.LogLevelManipulationFilter.clearLogLevelForTransaction(fCtx),
];
}
static defaultEpsilonErrorFilters() {
return [
(fCtx) => built_in_filters_1.BuiltInFilters.addAWSRequestIdHeader(fCtx),
(fCtx) => built_in_filters_1.BuiltInFilters.addAllowReflectionCORSHeaders(fCtx),
(fCtx) => log_level_manipulation_filter_1.LogLevelManipulationFilter.clearLogLevelForTransaction(fCtx),
];
}
static defaultHttpMetaProcessingConfigWithAuthenticationHeaderParsing(webTokenManipulator) {
const defaults = {
configName: 'EpsilonDefaultHttpMetaProcessingConfig',
timeoutMS: 30000,
overrideAuthorizerName: null,
preFilters: RouterUtil.defaultAuthenticationHeaderParsingEpsilonPreFilters(webTokenManipulator),
postFilters: RouterUtil.defaultEpsilonPostFilters(),
errorFilters: RouterUtil.defaultEpsilonErrorFilters(),
nullReturnedObjectHandling: null_returned_object_handling_1.NullReturnedObjectHandling.Return404NotFoundResponse,
};
return defaults;
}
static defaultHttpMetaProcessingConfig() {
const defaults = {
configName: 'EpsilonDefaultHttpMetaProcessingConfig',
timeoutMS: 30000,
overrideAuthorizerName: null,
preFilters: RouterUtil.defaultEpsilonPreFilters(),
postFilters: RouterUtil.defaultEpsilonPostFilters(),
errorFilters: RouterUtil.defaultEpsilonErrorFilters(),
nullReturnedObjectHandling: null_returned_object_handling_1.NullReturnedObjectHandling.Return404NotFoundResponse,
};
return defaults;
}
static assignDefaultsOnHttpConfig(cfg) {
const defaults = {
handlers: new Map(),
authorizers: new Map(),
defaultMetaHandling: this.defaultHttpMetaProcessingConfig(),
staticContentRoutes: {},
prefixesToStripBeforeRouteMatch: [],
filterHandledRouteMatches: ['options .*'], // Ignore all Options since they are handled by the default prefilter
};
const rval = Object.assign({}, defaults, cfg || {});
return rval;
}
// Search the overrides in order to find a match, otherwise return default
static findApplicableMeta(httpConfig, method, path) {
let rval = null;
if (httpConfig === null || httpConfig === void 0 ? void 0 : httpConfig.overrideMetaHandling) {
for (let i = 0; i < httpConfig.overrideMetaHandling.length && !rval; i++) {
const test = httpConfig.overrideMetaHandling[i];
if (!test.methods ||
test.methods.length === 0 ||
test.methods.map((s) => s.toLocaleLowerCase()).includes(method.toLocaleLowerCase())) {
const matches = !!path.match(test.pathRegex); // .match(path);
if ((matches && !test.invertPathMatching) || (!matches && test.invertPathMatching)) {
rval = test.config;
}
}
}
}
if (!rval) {
rval = httpConfig.defaultMetaHandling || RouterUtil.defaultHttpMetaProcessingConfig(); // If nothing found, use epsilon defaults
}
return rval;
}
// Parses an open api file to create a router config
static openApiYamlToRouterConfig(httpConfig, openApiDoc, modelValidator, backgroundHttpAdapterHandler) {
var _a;
if (!openApiDoc || !httpConfig) {
throw new misconfigured_error_1.MisconfiguredError('Cannot configure, missing either yaml or cfg');
}
const rval = {
routes: [],
openApiModelValidator: modelValidator,
config: RouterUtil.assignDefaultsOnHttpConfig(httpConfig),
};
if ((_a = openApiDoc === null || openApiDoc === void 0 ? void 0 : openApiDoc.components) === null || _a === void 0 ? void 0 : _a.securitySchemes) {
// Just validation, nothing to wire here
Object.keys(openApiDoc.components.securitySchemes).forEach((sk) => {
if (!rval.config.authorizers || !rval.config.authorizers.get(sk)) {
throw new misconfigured_error_1.MisconfiguredError().withFormattedErrorMessage('Doc requires authorizer %s but not found in map', sk);
}
});
}
const missingPaths = [];
const filterHandledPathMatches = httpConfig.filterHandledRouteMatches || [];
if (openApiDoc === null || openApiDoc === void 0 ? void 0 : openApiDoc.paths) {
Object.keys(openApiDoc.paths).forEach((path) => {
Object.keys(openApiDoc.paths[path]).forEach((method) => {
const convertedPath = RouterUtil.openApiPathToRouteParserPath(path);
const finder = method + ' ' + path;
const applicableMeta = RouterUtil.findApplicableMeta(httpConfig, method, path);
const entry = openApiDoc.paths[path][method];
const isBackgroundEndpoint = path.startsWith(backgroundHttpAdapterHandler.httpSubmissionPath);
const isBackgroundMetaEndpoint = path === backgroundHttpAdapterHandler.httpMetaEndpoint;
const isBackgroundStatusEndpoint = path === backgroundHttpAdapterHandler.httpStatusPath;
// Auto-assign the background handler endpoints
if (isBackgroundEndpoint) {
rval.config.handlers.set(finder, (evt, ctx) => backgroundHttpAdapterHandler.handleBackgroundSubmission(evt, ctx));
}
if (isBackgroundMetaEndpoint) {
rval.config.handlers.set(finder, (evt, ctx) => backgroundHttpAdapterHandler.handleBackgroundMetaRequest(evt, ctx));
}
if (isBackgroundStatusEndpoint) {
rval.config.handlers.set(finder, (evt, ctx) => backgroundHttpAdapterHandler.handleBackgroundStatusRequest(evt, ctx));
}
if (!rval.config.handlers || !rval.config.handlers.get(finder)) {
const match = filterHandledPathMatches.find((reg) => finder.match(reg));
if (match) {
common_1.Logger.debug('Adding filter-handled handler for %s', finder);
// Insert a placeholder for these, which still handles them in runtime if the filter is misconfigured
rval.config.handlers.set(finder, (evt) => built_in_handlers_1.BuiltInHandlers.expectedHandledByFilter(evt));
}
else {
missingPaths.push(finder);
}
}
if (entry && entry['security'] && entry['security'].length > 1) {
throw new misconfigured_error_1.MisconfiguredError('Epsilon does not currently support multiple security (path was ' + finder + ')');
}
const authorizerName = entry['security'] && entry['security'].length == 1 ? Object.keys(entry['security'][0])[0] : null;
const newRoute = {
path: convertedPath,
method: method,
function: rval.config.handlers.get(finder),
authorizerName: applicableMeta.overrideAuthorizerName || authorizerName,
metaProcessingConfig: applicableMeta,
validation: null,
outboundValidation: null,
};
// Add inbound validation, if available
if (entry['requestBody'] &&
entry['requestBody']['content'] &&
entry['requestBody']['content']['application/json'] &&
entry['requestBody']['content']['application/json']['schema']) {
// TODO: this is brittle as hell, need to firm up
const schema = entry['requestBody']['content'];
common_1.Logger.silly('Applying schema %j to %s', schema, finder);
const modelName = this.findAndValidateModelName(method, path, schema, rval.config.overrideModelValidator || rval.openApiModelValidator);
const required = common_2.BooleanRatchet.parseBool(entry['requestBody']['required']);
const validation = {
extraPropertiesAllowed: true,
emptyAllowed: !required,
modelName: modelName,
};
newRoute.validation = validation;
}
// Add outbound validation, if available
if (entry['responses'] &&
entry['responses']['200'] &&
entry['responses']['200']['content'] &&
entry['responses']['200']['content']['application/json'] &&
entry['responses']['200']['content']['application/json']['schema']) {
// TODO: this is brittle as hell, need to firm up
const schema = entry['responses']['200']['content'];
common_1.Logger.silly('Applying schema %j to %s', schema, finder);
const modelName = this.findAndValidateModelName(method, path, schema, rval.config.overrideModelValidator || rval.openApiModelValidator);
const validation = {
extraPropertiesAllowed: false,
emptyAllowed: false,
modelName: modelName,
};
newRoute.outboundValidation = validation;
}
rval.routes.push(newRoute);
});
});
}
if (missingPaths.length > 0) {
throw new misconfigured_error_1.MisconfiguredError().withFormattedErrorMessage('Missing expected handlers : %j', missingPaths);
}
return rval;
}
static findAndValidateModelName(method, path, schema, modelValidator) {
let rval = undefined;
const schemaPath = schema['application/json']['schema']['$ref'];
const inlinePath = schema['application/json']['schema']['type'];
if (schemaPath) {
rval = schemaPath.substring(schemaPath.lastIndexOf('/') + 1);
if (!modelValidator.fetchModel(rval)) {
throw new misconfigured_error_1.MisconfiguredError(`Path ${method} ${path} refers to schema ${rval} but its not in the schema section`);
}
}
else if (inlinePath) {
rval = `${method}-${path}-requestBodyModel`;
const model = schema['application/json']['schema'];
modelValidator.addModel(rval, model);
}
return rval;
}
static openApiPathToRouteParserPath(input) {
let rval = input;
if (rval) {
let sIdx = rval.indexOf('{');
while (sIdx > -1) {
const eIdx = rval.indexOf('}');
rval = rval.substring(0, sIdx) + ':' + rval.substring(sIdx + 1, eIdx) + rval.substring(eIdx + 1);
sIdx = rval.indexOf('{');
}
}
return rval;
}
}
exports.RouterUtil = RouterUtil;
//# sourceMappingURL=router-util.js.map