UNPKG

@allthings/cloud-toolkit

Version:

Standardizes the setup of aws, datadog and other things

1,148 lines (1,106 loc) 39.6 kB
'use strict'; var querystring = require('querystring'); var zlib = require('zlib'); var crypto = require('crypto'); var clientDynamodb = require('@aws-sdk/client-dynamodb'); var libDynamodb = require('@aws-sdk/lib-dynamodb'); var Bottleneck = require('bottleneck'); var clientLambda = require('@aws-sdk/client-lambda'); var clientSsm = require('@aws-sdk/client-ssm'); var aws$1 = require('@aws-sdk/client-ses'); var nodemailer = require('nodemailer'); var clientSqs = require('@aws-sdk/client-sqs'); var winston = require('winston'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var querystring__namespace = /*#__PURE__*/_interopNamespaceDefault(querystring); var zlib__namespace = /*#__PURE__*/_interopNamespaceDefault(zlib); var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto); var aws__namespace = /*#__PURE__*/_interopNamespaceDefault(aws$1); const defaultErrorHandler = (request, response, error) => { const { requestId } = request.requestContext; const errorName = error.name && typeof error.name === 'string' ? error.name.replace(/([A-Z])([A-Z])([a-z])|([a-z])([A-Z])/g, '$1$4 $2$3$5') : 'Error'; const errorMessage = error.message || ''; const statusCode = error.statusCode ? ` ${error.statusCode}` : ''; return error.name === 'ClientError' || (error.statusCode >= 400 && error.statusCode < 500) ? response.respondTo({ html: `<html><body><strong>${errorName}${statusCode}</strong>: ${errorMessage}<br/>Request ID: ${requestId}</body></html>`, json: { error: error.name, message: errorMessage, requestId }, }, error.statusCode || 400) : response.respondTo({ html: `<html><body>An internal server error occurred.<br/>Request ID: ${requestId}</body></html>`, json: { error: 'Internal server error', message: 'An internal server error occurred', requestId, }, }, 500); }; class ClientError extends Error { name = 'ClientError'; statusCode = 400; constructor(message, name, statusCode = 400) { super(String(message)); this.name = name || this.name; this.statusCode = statusCode; } } class ServerError extends Error { name = 'ServerError'; statusCode = 500; constructor(message, name, statusCode = 500) { super(String(message)); this.name = name || this.name; this.statusCode = statusCode; } } const applyMiddleware = async (middlewareList, initialData, ...arguments_) => middlewareList.reduce(async (applied, middleware) => middleware(await applied, ...arguments_), Promise.resolve(initialData)); const decodeBase64 = (encoded) => { const bodyBuffer = Buffer.from(encoded, 'base64'); return bodyBuffer.toString('utf8'); }; const decodeBase64Body = (request) => request.isBase64Encoded && typeof request.body === 'string' ? { ...request, body: decodeBase64(request.body), } : request; const parseCookiePair = (pair) => { const indexOfEqualCharacter = pair.indexOf('='); const looksLikeKeyValue = indexOfEqualCharacter !== -1; const key = looksLikeKeyValue && pair.slice(0, Math.max(0, indexOfEqualCharacter)).trim(); const value = looksLikeKeyValue && pair.substr(indexOfEqualCharacter + 1, pair.length).trim(); const cleanValue = value && '"' === value[0] ? value.slice(1, -1) : value; return key ? { [key]: cleanValue ? decodeURIComponent(cleanValue) : '' } : {}; }; const parseCookieHeader = (cookieHeader) => { const pairs = cookieHeader.split(/; */); return pairs.reduce((parsedCookies, pair) => ({ ...parsedCookies, ...parseCookiePair(pair) }), {}); }; const cookies = (request) => ({ ...request, cookies: parseCookieHeader(request.headers?.cookie || ''), }); const setHostname = (request) => ({ ...request, hostname: request.headers?.host || undefined, }); const parseJson = (stringified) => { try { return JSON.parse(stringified); } catch { return {}; } }; const parseJsonBody = (request) => { const { headers, body } = request; return headers['content-type'] && headers['content-type'].split(';').shift() === 'application/json' && typeof body === 'string' ? { ...request, body: parseJson(body) } : request; }; const state = new Map([['invocationCount', 0]]); const meta = (request) => { const invocationCount = Number(state.get('invocationCount')); return { ...request, meta: { ...request.meta, coldStart: !invocationCount, invocationCount: state .set('invocationCount', invocationCount + 1) .get('invocationCount'), }, }; }; const normalizeHeaders = ({ headers = {}, ...request }) => ({ ...request, headers: Object.keys(headers).reduce((normalizedHeaders, key) => { const normalizedHeaderKey = key.toLowerCase(); return request.provider === 'aws' && Reflect.has(normalizedHeaders, normalizedHeaderKey) && normalizedHeaderKey === 'content-type' && headers[key] === 'application/json' ? normalizedHeaders : { ...normalizedHeaders, [normalizedHeaderKey]: headers[key], }; }, {}), }); const normalizeProgrammingModel = (request) => ({ ...request, method: request.httpMethod ?? request.requestContext?.http?.method ?? '', path: request.path ?? request.requestContext?.http?.path ?? '', provider: 'aws', query: request.queryStringParameters || {}, source: request.requestContext ? 'api-gateway' : request.source, }); const addTimestamp = (request) => ({ ...request, timestamp: Date.now(), }); const parseUrlEncodedBody = (request) => { const { headers, body } = request; return headers['content-type'] && headers['content-type'].split(';').shift() === 'application/x-www-form-urlencoded' && typeof body === 'string' ? { ...request, body: querystring__namespace.parse(body) } : request; }; var makeRequest = async (event, context, options = {}) => applyMiddleware([ addTimestamp, normalizeProgrammingModel, normalizeHeaders, decodeBase64Body, cookies, parseUrlEncodedBody, parseJsonBody, setHostname, meta, ...(options.requestMiddleware || []), ], Object.freeze({ ...event, context, }), options); const setHeader = (response, responseData, key, value) => { Object.assign(responseData.headers, { [key]: value }); return response; }; const makeResponseObject = (responseData, body = '', statusCode = 200, { headers = {}, ...options } = {}, contentType) => Object.freeze({ ...responseData, body: body || '', headers: { ...responseData?.headers, 'content-type': contentType, ...headers, }, statusCode, ...options, }); const html = (responseData, _, body, statusCode, options) => makeResponseObject(responseData, body, statusCode, options, 'text/html'); const stringifyIfNotStringifiedJson = (data) => { const maybeTrimmedDataString = typeof data === 'string' ? data.trim() : data; const isStringifiedJson = typeof data === 'string' && ['{', '"'].includes(maybeTrimmedDataString[0]) && ['}', '"'].includes(maybeTrimmedDataString.at(-1)); return isStringifiedJson ? maybeTrimmedDataString : JSON.stringify(data); }; const json = (responseData, _, body, statusCode, options) => makeResponseObject(responseData, stringifyIfNotStringifiedJson(body), statusCode, options, 'application/json'); const COMPRESSIBLE = [ 'application/activity+json', 'application/alto-costmap+json', 'application/alto-costmapfilter+json', 'application/alto-directory+json', 'application/alto-endpointcost+json', 'application/alto-endpointcostparams+json', 'application/alto-endpointprop+json', 'application/alto-endpointpropparams+json', 'application/alto-error+json', 'application/alto-networkmap+json', 'application/alto-networkmapfilter+json', 'application/atom+xml', 'application/calendar+json', 'application/coap-group+json', 'application/csvm+json', 'application/dart', 'application/dicom+json', 'application/ecmascript', 'application/fhir+json', 'application/fido.trusted-apps+json', 'application/geo+json', 'application/javascript', 'application/jf2feed+json', 'application/jose+json', 'application/jrd+json', 'application/json', 'application/json-patch+json', 'application/jsonml+json', 'application/jwk+json', 'application/jwk-set+json', 'application/ld+json', 'application/manifest+json', 'application/merge-patch+json', 'application/mud+json', 'application/postscript', 'application/ppsp-tracker+json', 'application/problem+json', 'application/raml+yaml', 'application/rdap+json', 'application/rdf+xml', 'application/reputon+json', 'application/rss+xml', 'application/rtf', 'application/scim+json', 'application/soap+xml', 'application/tar', 'application/vcard+json', 'application/vnd.amadeus+json', 'application/vnd.api+json', 'application/vnd.apothekende.reservation+json', 'application/vnd.avalon+json', 'application/vnd.bbf.usp.msg+json', 'application/vnd.bekitzur-stech+json', 'application/vnd.capasystems-pg+json', 'application/vnd.collection+json', 'application/vnd.collection.doc+json', 'application/vnd.collection.next+json', 'application/vnd.coreos.ignition+json', 'application/vnd.dart', 'application/vnd.datapackage+json', 'application/vnd.dataresource+json', 'application/vnd.document+json', 'application/vnd.drive+json', 'application/vnd.geo+json', 'application/vnd.google-earth.kml+xml', 'application/vnd.hal+json', 'application/vnd.hc+json', 'application/vnd.heroku+json', 'application/vnd.hyper+json', 'application/vnd.hyper-item+json', 'application/vnd.hyperdrive+json', 'application/vnd.ims.lis.v2.result+json', 'application/vnd.ims.lti.v2.toolconsumerprofile+json', 'application/vnd.ims.lti.v2.toolproxy+json', 'application/vnd.ims.lti.v2.toolproxy.id+json', 'application/vnd.ims.lti.v2.toolsettings+json', 'application/vnd.ims.lti.v2.toolsettings.simple+json', 'application/vnd.las.las+json', 'application/vnd.mason+json', 'application/vnd.micro+json', 'application/vnd.miele+json', 'application/vnd.mozilla.xul+xml', 'application/vnd.ms-fontobject', 'application/vnd.ms-opentype', 'application/vnd.nearst.inv+json', 'application/vnd.oftn.l10n+json', 'application/vnd.oma.lwm2m+json', 'application/vnd.oracle.resource+json', 'application/vnd.pagerduty+json', 'application/vnd.restful+json', 'application/vnd.siren+json', 'application/vnd.sun.wadl+xml', 'application/vnd.tableschema+json', 'application/vnd.vel+json', 'application/vnd.xacml+json', 'application/voucher-cms+json', 'application/wasm', 'application/webpush-options+json', 'application/x-httpd-php', 'application/x-javascript', 'application/x-ns-proxy-autoconfig', 'application/x-sh', 'application/x-tar', 'application/x-virtualbox-hdd', 'application/x-virtualbox-ova', 'application/x-virtualbox-ovf', 'application/x-virtualbox-vbox', 'application/x-virtualbox-vdi', 'application/x-virtualbox-vhd', 'application/x-virtualbox-vmdk', 'application/x-web-app-manifest+json', 'application/x-www-form-urlencoded', 'application/xhtml+xml', 'application/xml', 'application/xml-dtd', 'application/xop+xml', 'application/yang-data+json', 'application/yang-patch+json', 'font/otf', 'image/bmp', 'image/svg+xml', 'image/vnd.adobe.photoshop', 'image/x-icon', 'image/x-ms-bmp', 'message/imdn+xml', 'message/rfc822', 'model/gltf+json', 'model/gltf-binary', 'model/x3d+xml', 'text/cache-manifest', 'text/calender', 'text/cmd', 'text/css', 'text/csv', 'text/html', 'text/javascript', 'text/jsx', 'text/markdown', 'text/n3', 'text/plain', 'text/richtext', 'text/rtf', 'text/tab-separated-values', 'text/uri-list', 'text/vcard', 'text/vtt', 'text/x-gwt-rpc', 'text/x-jquery-tmpl', 'text/x-markdown', 'text/x-org', 'text/x-processing', 'text/x-suse-ymp', 'text/xml', 'x-shader/x-fragment', 'x-shader/x-vertex', ]; const ZLIB_DEFAULT_OPTIONS = { level: 5, }; const acceptEncoding = (accepts, encodings) => accepts.find((accept) => encodings.some((encoding) => accept === encoding)); const getBestAcceptEncoding = (request) => { const { headers } = request; const acceptEncodings = (headers['accept-encoding'] || '').split(/, */); const encoding = acceptEncoding(acceptEncodings, [ 'gzip', 'deflate', 'identity', ]); return encoding === 'deflate' && acceptEncoding(acceptEncodings, ['gzip']) ? acceptEncoding(acceptEncodings, ['gzip', 'identity']) : encoding; }; const compressResponse = (response, encoding) => { const { body, headers, ...rest } = response; const compressed = encoding === 'gzip' ? zlib__namespace.gzipSync(body, ZLIB_DEFAULT_OPTIONS) : zlib__namespace.deflateSync(body, ZLIB_DEFAULT_OPTIONS); return { ...rest, body: compressed.toString('base64'), headers: { ...headers, 'content-encoding': encoding, 'content-length': compressed.byteLength, }, isBase64Encoded: true, }; }; const compress = (response, request) => { const { body, headers } = response; const encoding = getBestAcceptEncoding(request); const weShouldCompress = COMPRESSIBLE.includes(headers['content-type']) && encoding !== undefined && encoding !== 'identity' && body && body.length > 256; return weShouldCompress && encoding ? compressResponse(response, encoding) : response; }; const contentLengthHeader = (response) => { const { body, headers, ...rest } = response; return { ...rest, body, headers: { ...headers, 'content-length': headers['content-length'] || Buffer.byteLength(body), }, }; }; const DEFAULT_POLICIES = { 'default-src': "'self' https: 'unsafe-inline'", 'img-src': '* data: blob:', }; const cspHeaders = (response, _, options) => { const { headers = {}, ...rest } = response; const cspPolicies = { ...DEFAULT_POLICIES, ...options.cspPolicies, }; const cspPolicy = Object.keys(cspPolicies) .map((policy) => `${policy} ${cspPolicies[policy]}`) .join(';'); return { ...rest, headers: { ...headers, 'content-security-policy': cspPolicy, 'referrer-policy': 'strict-origin-when-cross-origin', 'x-content-type-options': 'nosniff', 'x-frame-options': { "'none'": 'DENY', "'self'": 'SAMEORIGIN', }[cspPolicies['frame-ancestors']] || undefined, 'x-xss-protection': '1; mode=block', }, }; }; const enforcedHeaders = (response, _, options) => ({ ...response, headers: { ...response.headers, ...(options.headers ?? {}), }, }); const EMPTY_ENTITY_TAG = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'; const entitytag = (entity) => { const hash = crypto__namespace .createHash('sha1') .update(entity, 'utf8') .digest('base64') .slice(0, 27); const length = typeof entity === 'string' ? Buffer.byteLength(entity, 'utf8') : entity.length; return `"${length.toString(16)}-${hash}"`; }; const etag = (entity) => entity.length === 0 ? EMPTY_ENTITY_TAG : entitytag(entity); const etagHeader = (response) => ({ ...response, headers: { ...response.headers, etag: etag(response.body), }, }); const aws = (request) => { const { headers: requestHeaders, requestContext, context } = request; return { apig: { requestId: requestContext.requestId, resourcePath: requestContext.resourcePath ?? '', }, awsAccountId: requestContext.accountId, cloudfront: { country: requestHeaders['cloudfront-viewer-country'], desktop: requestHeaders['cloudfront-is-desktop-viewer'] === 'true', mobile: requestHeaders['cloudfront-is-mobile-viewer'] === 'true', smarttv: requestHeaders['cloudfront-is-smarttv-viewer'] === 'true', tablet: requestHeaders['cloudfront-is-tablet-viewer'] === 'true', }, deploymentStage: requestContext.stage, lambda: { functionName: context.functionName, memoryLimit: Number(context.memoryLimitInMB), remainingExecutionTime: context.getRemainingTimeInMillis?.(), requestId: context.awsRequestId, }, sourceIp: requestContext.identity ?.sourceIp ?? requestContext?.http?.sourceIp, }; }; const logger$1 = (request, response) => { const { source, headers: requestHeaders, meta, requestContext, provider, hostname, body, timestamp, path, query, } = request; const error = response.statusCode >= 500 && response.statusCode < 600; const logEntry = { dateTime: new Date().toISOString(), eventSource: source, hostname, httpMethod: requestContext.httpMethod, httpProtocol: requestHeaders.via?.split(' ')[0], httpReferrer: requestHeaders.referer, httpUserAgent: requestHeaders['user-agent'], requestContentLength: (body && body.length > 0) || 0, requestTime: Date.now() - timestamp, requestUri: path + (query && Object.keys(query).length > 0 ? `?${querystring__namespace.stringify(query)}` : ''), responseContentLength: response.headers['content-length'], statusCode: response.statusCode, xForwardedHost: requestHeaders['x-forwarded-host'], ...meta, ...(provider === 'aws' && aws(request)), }; return ((error ? process.stderr : process.stdout).write(JSON.stringify(logEntry)) && logEntry); }; const logResponse = (response, request, options) => (options.enableLogger && (typeof options.logger === 'function' ? options.logger(request, response) : logger$1(request, response)) && response) || response; const redirect = (responseData, _, location, statusCode = 302, options = {}) => makeResponseObject(responseData, '', statusCode, { ...options, headers: { ...options.headers, location, }, }); const getBestMatchedFormat = (formats, accept) => { const fallback = formats.default || 'html'; const acceptFormats = accept && typeof accept === 'string' && accept .split(';')[0] .split(',') .map((type) => { const mimeParts = type.split('/'); return mimeParts.at(-1); }); return ((acceptFormats && acceptFormats.find((type) => Reflect.has(formats, type))) || fallback); }; const getBestMatchedResponseHelper = (format) => { switch (format) { case 'html': return html; case 'json': return json; } throw new TypeError(`"${format}" is not a valid Alagarr respondTo() format.`); }; const getBestMatchedFormatBody = (formats, format) => { switch (format) { case 'html': return formats.html; case 'json': return formats.json; } throw new TypeError(`"${format}" is not a valid Alagarr respondTo() format.`); }; const respondTo = (responseData, request, formats, statusCode, options) => { const { headers: { accept }, } = request; const format = getBestMatchedFormat(formats, accept); const responseHelper = getBestMatchedResponseHelper(format); const body = getBestMatchedFormatBody(formats, format); return responseHelper(responseData, request, body, statusCode, options); }; const text = (responseData, _, body, statusCode, options) => makeResponseObject(responseData, body, statusCode, options, 'text/plain'); const middlewareMap = { enableCompression: compress, enableContentLength: contentLengthHeader, enableCspHeaders: cspHeaders, enableETagHeader: etagHeader, enableEnforcedHeaders: enforcedHeaders, }; var makeResponse = async (request, callback, options) => { const responseData = makeResponseObject(); const responseWithHelpers = [ text, html, json, redirect, respondTo, ].reduce((methods, method) => ({ ...methods, [method.name]: async (...arguments_) => callback(null, await applyMiddleware(Object.keys(middlewareMap).reduce((middlewareList, middleware) => options[middleware] ? [...middlewareList, middlewareMap[middleware]] : middlewareList, [ ...(options.responseMiddleware || []), ...(options.enableLogger ? [logResponse] : []), ]), method(responseData, request, ...arguments_), request, options)), }), { raw: callback }); const response = [setHeader].reduce((responseMethods, method) => ({ ...responseMethods, [method.name]: (...arguments_) => method(response, responseData, ...arguments_), }), responseWithHelpers); return response; }; const DEFAULT_OPTIONS = { cspPolicies: [], enableCompression: true, enableContentLength: true, enableCspHeaders: true, enableETagHeader: true, enableEnforcedHeaders: true, enableLogger: true, enableStrictTransportSecurity: true, errorHandler: defaultErrorHandler, requestMiddleware: [], responseMiddleware: [], }; const noopHandler = (_, _1) => { throw new ServerError('Misconfiguration in Alagarr setup. No handler function was provided.'); }; const alagarr = (handler = noopHandler, options = DEFAULT_OPTIONS) => async (event, context, callback) => { const mergedOptions = { ...DEFAULT_OPTIONS, ...options, }; const request = await makeRequest(event, context, mergedOptions); const response = await makeResponse(request, callback, mergedOptions); try { return await handler(request, response, context); } catch (handlerError) { const errorHandler = typeof mergedOptions.errorHandler === 'function' ? mergedOptions.errorHandler : defaultErrorHandler; try { return await errorHandler(request, response, handlerError); } catch (error) { console.error('There was an error in the error handler provided to Alagaar', error, errorHandler.toString()); return defaultErrorHandler(request, response, error); } } }; const DEFAULT_AWS_CONFIGURATION = { region: 'eu-central-2', }; const splitEvery = (every, list) => Array.from({ length: Math.ceil(list.length / every) }).map((_, count) => list.slice(count * every, count * every + every)); const until = async (predicate, transformer, initialValue, iterationCount = 0) => { const transformed = await transformer(initialValue, iterationCount); return (await predicate(transformed, iterationCount)) ? transformed : until(predicate, transformer, transformed, iterationCount + 1); }; const DEFAULT_BATCH_ITEMS_MAX_RETRIES = 50; let queue; const makeKey = (key) => { if (typeof key === 'string') { return { id: key }; } return key; }; const deleteItem = async (config, documentClient, table, key, options = {}) => { try { const { ReturnValues = 'NONE' } = options; const command = new libDynamodb.DeleteCommand({ Key: makeKey(key), ReturnValues, TableName: config.tableNamePrefix + table, }); const result = await queue.schedule(() => documentClient.send(command)); return result.Attributes; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const getItem = async (config, documentClient, table, key, options = {}) => { try { const { ConsistentRead = false } = options; const command = new libDynamodb.GetCommand({ ConsistentRead, Key: makeKey(key), TableName: config.tableNamePrefix + table, }); const result = await queue.schedule(() => documentClient.send(command)); return result.Item; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const putItem = async (config, documentClient, table, Item) => { try { const { expires, ...data } = Item; const command = new libDynamodb.PutCommand({ Item: expires === null ? data : Item, TableName: config.tableNamePrefix + table, }); return (await queue.schedule(() => documentClient.send(command))) && Item; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const updateItem = async (config, documentClient, table, key, options = {}) => { try { const command = new libDynamodb.UpdateCommand({ Key: makeKey(key), ...options, ReturnValues: 'ALL_NEW', TableName: config.tableNamePrefix + table, }); const result = await queue.schedule(async () => documentClient.send(command)); return result.Attributes; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const hasNoUnprocessedItems = ({ UnprocessedItems, }) => !UnprocessedItems || Object.keys(UnprocessedItems).length === 0; const makeBatchWriteRequestWithExponentialBackoff = (documentClient, table, batchWriteMaxRetries) => async (batchItems, retries) => { if (retries > batchWriteMaxRetries) { throw new Error('Reached maximum number of DynamoDB BatchWrite request retries.'); } const command = new libDynamodb.BatchWriteCommand({ RequestItems: batchItems.UnprocessedItems ?? { [table]: batchItems.map((Item) => ({ PutRequest: { Item }, })), }, ReturnConsumedCapacity: 'TOTAL', ReturnItemCollectionMetrics: 'SIZE', }); return documentClient.send(command); }; const batchWriteItems = async (config, documentClient, table, items) => { try { const batches = splitEvery(25, items); const result = await Promise.all(batches.map(async (batch) => queue.schedule(async () => until(hasNoUnprocessedItems, makeBatchWriteRequestWithExponentialBackoff(documentClient, config.tableNamePrefix + table, config.batchWriteMaxRetries ?? DEFAULT_BATCH_ITEMS_MAX_RETRIES), batch)))); return result && items; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const scanTable = async (config, documentClient, table, options = {}) => { try { const { ConsistentRead = false } = options; const command = new libDynamodb.ScanCommand({ ...options, ConsistentRead, TableName: config.tableNamePrefix + table, }); const result = await queue.schedule(() => documentClient.send(command)); return result; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const queryTable = async (config, documentClient, table, options = {}) => { try { const { ConsistentRead = false } = options; const command = new libDynamodb.QueryCommand({ ...options, ConsistentRead, TableName: config.tableNamePrefix + table, }); const result = await queue.schedule(() => documentClient.send(command)); return result; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const dynamodb = (userConfig) => { const { clientConfig, ...config } = { ...DEFAULT_AWS_CONFIGURATION, ...userConfig, }; const ddbClient = new clientDynamodb.DynamoDBClient(config.dockerConfig ? { credentials: { accessKeyId: 'foobarkey', secretAccessKey: 'foobarsecret', }, endpoint: config.dockerConfig.endpoint, region: 'local', retryMode: 'standard', } : { retryMode: 'standard', ...clientConfig }); const documentClient = libDynamodb.DynamoDBDocumentClient.from(ddbClient); queue = new Bottleneck({ maxConcurrent: config.queueConfig?.queueConcurrency ?? 25, minTime: config.queueConfig?.queueDelay ?? undefined, }); return { batchWriteItems: (table, items) => batchWriteItems(config, documentClient, table, items), deleteItem: (table, key, options) => deleteItem(config, documentClient, table, key, options), getItem: (table, key, options) => getItem(config, documentClient, table, key, options), putItem: (table, item) => putItem(config, documentClient, table, item), queryTable: (table, options) => queryTable(config, documentClient, table, options), scanTable: (table, options) => scanTable(config, documentClient, table, options), updateItem: (table, key, options) => updateItem(config, documentClient, table, key, options), }; }; const invokeLambda = async (lambdaClient, config, invocationType, payload) => { try { const invokeCommand = new clientLambda.InvokeCommand({ FunctionName: config.functionName, InvocationType: invocationType, Payload: Buffer.from(JSON.stringify(payload)), }); const result = await lambdaClient.send(invokeCommand); if (config.logger) { config.logger.info(JSON.stringify(result)); } return true; } catch (error) { if (config.logger) { config.logger.error(error); } return false; } }; const lambda = (userConfig) => { const { clientConfig, ...config } = { ...DEFAULT_AWS_CONFIGURATION, ...userConfig, }; const lambdaClient = new clientLambda.LambdaClient(clientConfig); return { invokeLambda: (invocationType, payload) => invokeLambda(lambdaClient, config, invocationType, payload), }; }; const DEFAULT_CLIENT_CONFIG$1 = { clientConfig: DEFAULT_AWS_CONFIGURATION, onlyDefault: false, prefix: process.env.SSM_PARAM_PATH, }; const getSecrets = async (ssm, config, parameters_, defaultValues) => { if (config.onlyDefault) { return defaultValues; } try { const command = new clientSsm.GetParametersCommand({ Names: parameters_.map((parameter) => `${config.prefix ?? ''}${parameter}`), WithDecryption: true, }); const parameters = await ssm.send(command); return (parameters?.Parameters?.map((parameter) => { const defaultIfNoParameter = !parameter.Value && parameter.Name ? defaultValues.find((defaultValue) => defaultValue.name === parameter.Name) : defaultValues[0]; return { name: parameter.Name ?? defaultIfNoParameter?.name ?? '', value: parameter.Value ?? defaultIfNoParameter?.value ?? '', }; }) ?? defaultValues); } catch (error) { if (config.logger) { config.logger.error(error); } return defaultValues; } }; const getSecret = async (ssm, config, parameter_, defaultValue) => { if (config.onlyDefault) { return defaultValue; } try { const parameter = await ssm.send(new clientSsm.GetParameterCommand({ Name: `${config.prefix ?? ''}${parameter_}`, WithDecryption: true, })); return parameter?.Parameter?.Value ?? defaultValue; } catch (error) { if (config.logger) { config.logger.error(error); } return defaultValue; } }; const parameterStore = (userConfig) => { const { clientConfig, ...config } = { ...DEFAULT_CLIENT_CONFIG$1, ...userConfig, }; const ssm = new clientSsm.SSMClient(clientConfig); return { getSecret: (parameter, defaultValue = '') => getSecret(ssm, config, parameter, defaultValue), getSecrets: (parameters, defaultValues = []) => getSecrets(ssm, config, parameters, defaultValues), }; }; const DEFAULT_CLIENT_CONFIG = { clientConfig: { region: 'eu-west-1' }, }; const sendMail = async (config, transport, options, from, responseCallback) => { try { if (!options.html && !options.text) { if (config.logger) { config.logger.error('The option text or html needs to be set.'); } return false; } if (!options.to || !options.subject) { if (config.logger) { config.logger.error('The fields to and subject have to be set.'); } return false; } const transportResponse = await transport.sendMail({ from, ...options, }); if (responseCallback) { await responseCallback(transportResponse); } if (!transportResponse) { if (config.logger) { config.logger.error('Sending email failed!'); } return false; } return true; } catch (error) { if (config.logger) { config.logger.error(error); } return false; } }; const ses = (fromEmailAddress, userConfig) => { const { clientConfig, ...config } = { ...DEFAULT_CLIENT_CONFIG, ...userConfig, }; const sesClient = new aws__namespace.SESClient(clientConfig); const transport = nodemailer.createTransport(userConfig.customSMTPConfig ?? { SES: { aws: aws__namespace, ses: sesClient }, }); return { sendMail: (options, responseCallback) => sendMail(config, transport, options, fromEmailAddress, responseCallback), }; }; const deleteQueueItem = async (sqsClient, config, queueUrl, receiptHandle) => { try { const deleteMessageCommand = new clientSqs.DeleteMessageCommand({ QueueUrl: queueUrl, ReceiptHandle: receiptHandle, }); const result = await sqsClient.send(deleteMessageCommand); return result; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const getQueueItems = async (sqsClient, config, queueUrl, maxNumberOfMessages = 1) => { try { const receiveMessageCommand = new clientSqs.ReceiveMessageCommand({ MaxNumberOfMessages: maxNumberOfMessages, QueueUrl: queueUrl, }); const result = await sqsClient.send(receiveMessageCommand); return result; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const getQueueUrl = async (sqsClient, config, queueName) => { try { const getUrlCommand = new clientSqs.GetQueueUrlCommand({ QueueName: queueName }); const result = await sqsClient.send(getUrlCommand); return result.QueueUrl; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const sendMessage = async (sqsClient, config, queueUrl, message, fifoSettings = {}, messageAttributes, messageDelay = 0) => { try { const sendMessageCommand = new clientSqs.SendMessageCommand({ ...fifoSettings, ...(messageAttributes ? { MessageAttributes: messageAttributes } : {}), DelaySeconds: messageDelay, MessageBody: message, QueueUrl: queueUrl, }); const result = await sqsClient.send(sendMessageCommand); return result.MessageId; } catch (error) { if (config.logger) { config.logger.error(error); } return undefined; } }; const sqs = (userConfig) => { const { clientConfig, ...config } = { ...DEFAULT_AWS_CONFIGURATION, ...userConfig, }; const sqsClient = new clientSqs.SQSClient(clientConfig); return { deleteQueueItem: (queueUrl, receiptHandle) => deleteQueueItem(sqsClient, config, queueUrl, receiptHandle), getQueueItems: (queueUrl, maxNumberOfMessages) => getQueueItems(sqsClient, config, queueUrl, maxNumberOfMessages), getQueueUrl: (queueName) => getQueueUrl(sqsClient, config, queueName), sendMessage: (queueUrl, message, fifoSettings, messageAttributes, messageDelay) => sendMessage(sqsClient, config, queueUrl, message, fifoSettings, messageAttributes, messageDelay), }; }; const logger = (config) => { const loggerConfig = { defaultMeta: { ...config.defaultLogInformation, deploymentStage: config.deploymentStage, service: config.service, }, format: winston.format.combine(winston.format.timestamp({ format: 'DD-MM-YYYY HH:mm:ss', }), winston.format.json()), }; if (config.datadogApiKey) { const httpOptions = { host: 'http-intake.logs.datadoghq.eu', path: `/api/v2/logs?dd-api-key=${config.datadogApiKey}&ddsource=nodejs&service=${config.service}`, port: 443, ssl: true, }; return winston.createLogger({ ...loggerConfig, transports: [new winston.transports.Http(httpOptions)], }); } return winston.createLogger({ ...loggerConfig, transports: [new winston.transports.Console({ forceConsole: true })], }); }; Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return winston.Logger; } }); exports.ClientError = ClientError; exports.DEFAULT_PARAMETER_STORE_CLIENT_CONFIG = DEFAULT_CLIENT_CONFIG$1; exports.DEFAULT_SES_CLIENT_CONFIG = DEFAULT_CLIENT_CONFIG; exports.ServerError = ServerError; exports.alagarr = alagarr; exports.dynamodb = dynamodb; exports.lambda = lambda; exports.logger = logger; exports.parameterStore = parameterStore; exports.ses = ses; exports.sqs = sqs;