boostr
Version:
Build and deploy your Layr apps
685 lines • 29.2 kB
JavaScript
import { promises as fsAsync } from 'fs';
import fsExtra from 'fs-extra';
import { join, sep } from 'path';
import { temporaryDirectoryTask } from 'tempy';
// @ts-ignore
import zip from 'cross-zip';
import hasha from 'hasha';
import { sleep, SECOND, MINUTE, HOUR, DAY } from '@layr/utilities';
import isEqual from 'lodash/isEqual.js';
import pull from 'lodash/pull.js';
import bytes from 'bytes';
import { AWSBaseResource } from './base.js';
import { ensureMaximumStringLength } from '../../utilities.js';
const DEFAULT_LAMBDA_RUNTIME = 'nodejs16.x';
const DEFAULT_LAMBDA_EXECUTION_ROLE = 'boostr-backend-lambda-role-v2';
const DEFAULT_LAMBDA_MEMORY_SIZE = 128;
const DEFAULT_LAMBDA_TIMEOUT = 10;
const DEFAULT_API_GATEWAY_CORS_CONFIGURATION = {
AllowOrigins: ['*'],
AllowHeaders: ['content-type'],
AllowMethods: ['GET', 'POST', 'OPTIONS'],
ExposeHeaders: ['*'],
MaxAge: 3600 // 1 hour
};
const DEFAULT_IAM_LAMBDA_POLICY_NAME = 'basic-lambda-policy';
const DEFAULT_IAM_LAMBDA_ASSUME_ROLE_POLICY_DOCUMENT = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com'
},
Action: 'sts:AssumeRole'
}
]
};
const DEFAULT_IAM_LAMBDA_POLICY_DOCUMENT = {
Version: '2012-10-17',
Statement: [
{
Action: ['logs:*', 'lambda:InvokeFunction'],
Effect: 'Allow',
Resource: '*'
}
]
};
export class AWSFunctionResource extends AWSBaseResource {
constructor(config, options = {}) {
super(config, options);
}
normalizeConfig(config) {
let { directory, environment = {}, backgroundMethods = [], lambda: { runtime = DEFAULT_LAMBDA_RUNTIME, executionRole = DEFAULT_LAMBDA_EXECUTION_ROLE, memorySize = DEFAULT_LAMBDA_MEMORY_SIZE, timeout, reservedConcurrentExecutions } = {}, ...otherAttributes } = config;
if (!directory) {
this.throwError(`A 'directory' property is required in the configuration`);
}
let longestBackgroundMethod;
for (const backgroundMethod of backgroundMethods) {
if (backgroundMethod.maximumDuration === undefined) {
continue;
}
if (longestBackgroundMethod === undefined) {
longestBackgroundMethod = backgroundMethod;
continue;
}
if (backgroundMethod.maximumDuration > longestBackgroundMethod.maximumDuration) {
longestBackgroundMethod = backgroundMethod;
}
}
if (longestBackgroundMethod !== undefined) {
const maximumTimeout = longestBackgroundMethod.maximumDuration / SECOND;
if (timeout === undefined) {
timeout = maximumTimeout;
}
else if (timeout < maximumTimeout) {
this.throwError(`The Lambda function timeout specified in the configuration (${timeout} seconds) shouldn't be lower than the maximum background method duration (method: '${longestBackgroundMethod.path}', maximum duration: ${maximumTimeout} seconds)`);
}
}
else {
timeout ?? (timeout = DEFAULT_LAMBDA_TIMEOUT);
}
return {
...super.normalizeConfig(otherAttributes),
directory,
environment,
backgroundMethods,
lambda: {
runtime,
executionRole,
memorySize,
timeout,
reservedConcurrentExecutions
}
};
}
async deploy() {
const config = this.getConfig();
this.logMessage(`Starting the deployment of a function to AWS...`);
await this.getRoute53HostedZone();
await this.ensureIAMLambdaRole();
await this.createOrUpdateLambdaFunction();
await this.configureEventBridgeRules();
await this.ensureACMCertificate();
await this.createOrUpdateAPIGateway();
await this.createOrUpdateAPIGatewayCustomDomainName();
this.logMessage(`Deployment completed`);
this.logMessage(`The service should be available at https://${config.domainName}`);
}
// === Lambda ===
async createOrUpdateLambdaFunction() {
this.logMessage('Checking the Lambda function...');
const lambdaFunction = await this.getLambdaFunction({ throwIfMissing: false });
if (lambdaFunction === undefined) {
await this.createLambdaFunction();
await this.setLambdaFunctionTags();
return;
}
await this.checkLambdaFunctionTags();
if (await this.checkIfLambdaFunctionCodeHasChanged()) {
await this.updateLambdaFunctionCode();
}
else {
this.logMessage(`The Lambda function code was unchanged`);
}
if (await this.checkIfLambdaFunctionConfigurationHasChanged()) {
await this.updateLambdaFunctionConfiguration();
}
if (await this.checkIfLambdaFunctionConcurrencyHasChanged()) {
await this.updateLambdaFunctionConcurrency();
}
}
async getLambdaFunction({ throwIfMissing = true } = {}) {
if (this._lambdaFunction === undefined) {
const lambda = this.getLambdaClient();
try {
const result = await lambda
.getFunction({
FunctionName: this.getLambdaName()
})
.promise();
const config = result.Configuration;
this._lambdaFunction = {
arn: config.FunctionArn,
executionRole: config.Role.split('/')[1],
runtime: config.Runtime,
memorySize: config.MemorySize,
timeout: config.Timeout,
reservedConcurrentExecutions: result.Concurrency?.ReservedConcurrentExecutions,
environment: config.Environment?.Variables ?? {},
codeSHA256: config.CodeSha256,
tags: result.Tags
};
}
catch (err) {
if (err.code !== 'ResourceNotFoundException') {
throw err;
}
}
}
if (this._lambdaFunction === undefined && throwIfMissing) {
this.throwError(`Couldn't get the Lambda function`);
}
return this._lambdaFunction;
}
async createLambdaFunction() {
const config = this.getConfig();
const lambda = this.getLambdaClient();
const role = (await this.getIAMLambdaRole());
const zipArchive = await this.getZipArchive();
this.logMessage(`Creating the Lambda function (${bytes(zipArchive.length)})...`);
let errors = 0;
while (this._lambdaFunction === undefined) {
try {
const lambdaFunction = await lambda
.createFunction({
FunctionName: this.getLambdaName(),
Handler: 'handler.handler',
Runtime: config.lambda.runtime,
Role: role.arn,
MemorySize: config.lambda.memorySize,
Timeout: config.lambda.timeout,
Environment: { Variables: config.environment },
Code: { ZipFile: zipArchive }
})
.promise();
this._lambdaFunction = { arn: lambdaFunction.FunctionArn };
}
catch (err) {
const roleMayNotBeReady = err.code === 'InvalidParameterValueException' && ++errors <= 10;
if (!roleMayNotBeReady) {
throw err;
}
await sleep(3000);
}
}
if (config.lambda.reservedConcurrentExecutions !== undefined) {
await this.updateLambdaFunctionConcurrency();
}
}
async checkLambdaFunctionTags() {
const lambdaFunction = (await this.getLambdaFunction());
if (!this.constructor.managerIdentifiers.includes(lambdaFunction.tags?.['managed-by'])) {
this.throwError(`Cannot update a Lambda function that was not originally created by this tool (function: '${this.getLambdaName()}')`);
}
}
async setLambdaFunctionTags() {
const lambda = this.getLambdaClient();
const lambdaFunction = (await this.getLambdaFunction());
await lambda
.tagResource({
Resource: lambdaFunction.arn,
Tags: { 'managed-by': this.constructor.managerIdentifiers[0] }
})
.promise();
}
async checkIfLambdaFunctionConfigurationHasChanged() {
const config = this.getConfig();
const lambdaFunction = (await this.getLambdaFunction());
if (lambdaFunction.runtime !== config.lambda.runtime) {
return true;
}
if (lambdaFunction.executionRole !== config.lambda.executionRole) {
return true;
}
if (lambdaFunction.memorySize !== config.lambda.memorySize) {
return true;
}
if (lambdaFunction.timeout !== config.lambda.timeout) {
return true;
}
const configEnvironmentWithoutUndefinedValues = {};
for (const [name, value] of Object.entries(config.environment)) {
if (value !== undefined) {
configEnvironmentWithoutUndefinedValues[name] = value;
}
}
if (!isEqual(lambdaFunction.environment, configEnvironmentWithoutUndefinedValues)) {
return true;
}
return false;
}
async updateLambdaFunctionConfiguration() {
const config = this.getConfig();
const lambda = this.getLambdaClient();
const role = (await this.getIAMLambdaRole());
this.logMessage('Updating the Lambda function configuration...');
await lambda
.updateFunctionConfiguration({
FunctionName: this.getLambdaName(),
Runtime: config.lambda.runtime,
Role: role.arn,
MemorySize: config.lambda.memorySize,
Timeout: config.lambda.timeout,
Environment: { Variables: config.environment }
})
.promise();
await this.waitForLambdaFunctionUpdated();
}
async checkIfLambdaFunctionConcurrencyHasChanged() {
const config = this.getConfig();
const lambdaFunction = (await this.getLambdaFunction());
return (lambdaFunction.reservedConcurrentExecutions !== config.lambda.reservedConcurrentExecutions);
}
async updateLambdaFunctionConcurrency() {
const config = this.getConfig();
const lambda = this.getLambdaClient();
this.logMessage('Updating the Lambda function concurrency...');
if (config.lambda.reservedConcurrentExecutions === undefined) {
await lambda.deleteFunctionConcurrency({ FunctionName: this.getLambdaName() }).promise();
}
else {
await lambda
.putFunctionConcurrency({
FunctionName: this.getLambdaName(),
ReservedConcurrentExecutions: config.lambda.reservedConcurrentExecutions
})
.promise();
}
await this.waitForLambdaFunctionUpdated();
}
async checkIfLambdaFunctionCodeHasChanged() {
const lambdaFunction = (await this.getLambdaFunction());
const zipArchive = await this.getZipArchive();
const zipArchiveSHA256 = hasha(zipArchive, { encoding: 'base64', algorithm: 'sha256' });
return lambdaFunction.codeSHA256 !== zipArchiveSHA256;
}
async updateLambdaFunctionCode() {
const lambda = this.getLambdaClient();
const zipArchive = await this.getZipArchive();
this.logMessage(`Updating the Lambda function code (${bytes(zipArchive.length)})...`);
await lambda
.updateFunctionCode({
FunctionName: this.getLambdaName(),
ZipFile: zipArchive
})
.promise();
await this.waitForLambdaFunctionUpdated();
}
async waitForLambdaFunctionUpdated() {
const lambda = this.getLambdaClient();
await lambda.waitFor('functionUpdatedV2', { FunctionName: this.getLambdaName() }).promise();
}
async getZipArchive() {
if (this._zipArchive === undefined) {
const config = this.getConfig();
this.logMessage(`Building the ZIP archive...`);
await temporaryDirectoryTask(async (temporaryDirectory) => {
const codeDirectory = join(temporaryDirectory, 'code');
const zipArchiveFile = join(temporaryDirectory, 'archive.zip');
await fsExtra.copy(config.directory, codeDirectory);
await this.resetFileTimes(codeDirectory);
zip.zipSync(`${codeDirectory}${sep}.`, zipArchiveFile);
this._zipArchive = await fsAsync.readFile(zipArchiveFile);
});
}
return this._zipArchive;
}
async resetFileTimes(directory) {
const fixedDate = new Date(Date.UTC(1984, 0, 24));
const entries = await fsAsync.readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const entryPath = join(directory, entry.name);
await fsAsync.utimes(entryPath, fixedDate, fixedDate);
if (entry.isDirectory()) {
await this.resetFileTimes(entryPath);
}
}
}
async allowLambdaFunctionInvocationFromAPIGateway() {
const config = this.getConfig();
const lambda = this.getLambdaClient();
const lambdaFunction = (await this.getLambdaFunction());
const apiGateway = (await this.getAPIGateway());
const matches = /arn:aws:.+:.+:(\d+):/.exec(lambdaFunction.arn);
const accountId = matches?.[1];
if (!accountId) {
this.throwError('Unable to find out the AWS account ID');
}
const sourceARN = `arn:aws:execute-api:${config.region}:${accountId}:${apiGateway.id}/*/*`;
await lambda
.addPermission({
FunctionName: lambdaFunction.arn,
Action: 'lambda:InvokeFunction',
Principal: 'apigateway.amazonaws.com',
StatementId: 'allow_api_gateway',
SourceArn: sourceARN
})
.promise();
}
getLambdaName() {
return domainNameToLambdaFunctionName(this.getConfig().domainName);
}
// === EventBridge rules ===
async configureEventBridgeRules() {
const config = this.getConfig();
const expectedRules = config.backgroundMethods
.filter(({ scheduling }) => scheduling)
.map(({ path, scheduling, query }) => ({
name: ensureMaximumStringLength(`${this.getLambdaName()}.${path}`, 64),
description: `Invokes the method ${path}() from the Lambda function '${this.getLambdaName()}'.`,
scheduleExpression: millisecondsToEventBridgeScheduleExpression(scheduling.rate),
input: JSON.stringify({ query })
}));
const existingRules = await this.findEventBridgeExistingRules();
for (const expectedRule of expectedRules) {
const existingRule = existingRules.find((existingRule) => existingRule.name === expectedRule.name);
if (existingRule === undefined) {
await this.createEventBridgeRule(expectedRule);
}
else {
if (existingRule.scheduleExpression !== expectedRule.scheduleExpression) {
await this.updateEventBridgeRule({ ...existingRule, ...expectedRule });
}
pull(existingRules, existingRule);
}
}
for (const existingRule of existingRules) {
await this.removeEventBridgeRule(existingRule);
}
}
async findEventBridgeExistingRules() {
const eventBridge = this.getEventBridgeClient();
const lambdaFunction = (await this.getLambdaFunction());
this.logMessage(`Searching for EventBridge existing rules...`);
const result = await eventBridge
.listRuleNamesByTarget({ TargetArn: lambdaFunction.arn })
.promise();
if (result.NextToken) {
this.throwError(`Whoa, you have a lot of EventBridge rules associated to the Lambda function! Unfortunately, this tool cannot list them all.`);
}
const existingRuleNames = result.RuleNames;
const existingRules = [];
for (const name of existingRuleNames) {
const result = await eventBridge.describeRule({ Name: name }).promise();
existingRules.push({
name: result.Name,
description: result.Description,
arn: result.Arn,
scheduleExpression: result.ScheduleExpression
});
}
return existingRules;
}
async createEventBridgeRule(rule) {
const lambda = this.getLambdaClient();
const eventBridge = this.getEventBridgeClient();
this.logMessage(`Creating the EventBridge rule '${rule.name}'...`);
const putRuleResult = await eventBridge
.putRule({
Name: rule.name,
Description: rule.description,
ScheduleExpression: rule.scheduleExpression,
Tags: [{ Key: 'managed-by', Value: this.constructor.managerIdentifiers[0] }]
})
.promise();
await lambda
.addPermission({
FunctionName: this.getLambdaName(),
StatementId: eventBridgeRuleNameToLambdaPermissionStatementId(rule.name),
Action: 'lambda:InvokeFunction',
Principal: 'events.amazonaws.com',
SourceArn: putRuleResult.RuleArn
})
.promise();
const lambdaFunction = (await this.getLambdaFunction());
const putTargetsResult = await eventBridge
.putTargets({
Rule: rule.name,
Targets: [{ Id: rule.name, Arn: lambdaFunction.arn, Input: rule.input }]
})
.promise();
if (putTargetsResult.FailedEntryCount !== 0) {
this.throwError(`An error occurred while setting the target of the EventBridge rule (code: '${putTargetsResult.FailedEntries[0].ErrorCode}', message: '${putTargetsResult.FailedEntries[0].ErrorMessage}')`);
}
}
async updateEventBridgeRule(rule) {
const eventBridge = this.getEventBridgeClient();
if (!(await this.checkEventBridgeRuleTags(rule))) {
this.throwError(`Cannot update an EventBridge rule that was not originally created by this tool (rule: '${rule.name}')`);
}
this.logMessage(`Updating the EventBridge rule '${rule.name}'...`);
await eventBridge
.putRule({
Name: rule.name,
Description: rule.description,
ScheduleExpression: rule.scheduleExpression
})
.promise();
}
async removeEventBridgeRule(rule) {
const lambda = this.getLambdaClient();
const eventBridge = this.getEventBridgeClient();
if (!(await this.checkEventBridgeRuleTags(rule))) {
return;
}
this.logMessage(`Removing the EventBridge rule '${rule.name}'...`);
const removeTargetsResult = await eventBridge
.removeTargets({ Rule: rule.name, Ids: [rule.name] })
.promise();
if (removeTargetsResult.FailedEntryCount !== 0) {
this.throwError(`An error occurred while setting the target of the EventBridge rule (code: '${removeTargetsResult.FailedEntries[0].ErrorCode}', message: '${removeTargetsResult.FailedEntries[0].ErrorMessage}')`);
}
await lambda
.removePermission({
FunctionName: this.getLambdaName(),
StatementId: eventBridgeRuleNameToLambdaPermissionStatementId(rule.name)
})
.promise();
await eventBridge.deleteRule({ Name: rule.name }).promise();
}
async checkEventBridgeRuleTags(rule) {
const tags = await this.getEventBridgeRuleTags(rule);
return this.constructor.managerIdentifiers.includes(tags['managed-by']);
}
async getEventBridgeRuleTags(rule) {
const eventBridge = this.getEventBridgeClient();
const result = await eventBridge.listTagsForResource({ ResourceARN: rule.arn }).promise();
const tags = Object.create(null);
for (const { Key, Value } of result.Tags) {
tags[Key] = Value;
}
return tags;
}
// === IAM for Lambda ===
async ensureIAMLambdaRole() {
this.logMessage('Checking the IAM Lambda role...');
if ((await this.getIAMLambdaRole({ throwIfMissing: false })) === undefined) {
this.logMessage('Creating the IAM Lambda role...');
await this.createIAMLambdaRole();
}
}
async getIAMLambdaRole({ throwIfMissing = true } = {}) {
if (this._iamLambdaRole === undefined) {
const iam = this.getIAMClient();
try {
const result = await iam
.getRole({ RoleName: this.getConfig().lambda.executionRole })
.promise();
this._iamLambdaRole = { arn: result.Role.Arn };
}
catch (err) {
if (err.code !== 'NoSuchEntity') {
throw err;
}
}
}
if (this._iamLambdaRole === undefined && throwIfMissing) {
this.throwError(`Couldn't get the IAM Lambda role`);
}
return this._iamLambdaRole;
}
async createIAMLambdaRole() {
const iam = this.getIAMClient();
const assumeRolePolicyDocument = JSON.stringify(DEFAULT_IAM_LAMBDA_ASSUME_ROLE_POLICY_DOCUMENT, undefined, 2);
const { Role: { Arn: arn } } = await iam
.createRole({
RoleName: this.getConfig().lambda.executionRole,
AssumeRolePolicyDocument: assumeRolePolicyDocument
})
.promise();
const policyDocument = JSON.stringify(DEFAULT_IAM_LAMBDA_POLICY_DOCUMENT, undefined, 2);
await iam
.putRolePolicy({
RoleName: this.getConfig().lambda.executionRole,
PolicyName: DEFAULT_IAM_LAMBDA_POLICY_NAME,
PolicyDocument: policyDocument
})
.promise();
await sleep(15000); // Wait 15 secs so AWS can replicate the role in all regions
this._iamLambdaRole = { arn };
}
// === API Gateway ===
async createOrUpdateAPIGateway() {
this.logMessage(`Checking the API Gateway...`);
const api = await this.getAPIGateway({ throwIfMissing: false });
if (api === undefined) {
await this.createAPIGateway();
await this.allowLambdaFunctionInvocationFromAPIGateway();
}
else {
await this.checkAPIGatewayTags();
}
}
async getAPIGateway({ throwIfMissing = true } = {}) {
if (this._apiGateway === undefined) {
const apiGateway = this.getAPIGatewayV2Client();
const result = await apiGateway.getApis().promise();
const item = result.Items?.find((item) => item.Name === this.getAPIGatewayName());
if (item !== undefined) {
this._apiGateway = { id: item.ApiId, endpoint: item.ApiEndpoint, tags: item.Tags };
}
else if (result.NextToken) {
this.throwError(`Whoa, you have a lot of API Gateways! Unfortunately, this tool cannot list them all.`);
}
}
if (this._apiGateway === undefined && throwIfMissing) {
this.throwError(`Couldn't find the API Gateway`);
}
return this._apiGateway;
}
async createAPIGateway() {
const apiGateway = this.getAPIGatewayV2Client();
this.logMessage(`Creating the API Gateway...`);
const tags = { 'managed-by': this.constructor.managerIdentifiers[0] };
const result = await apiGateway
.createApi({
Name: this.getAPIGatewayName(),
ProtocolType: 'HTTP',
Target: (await this.getLambdaFunction()).arn,
CorsConfiguration: DEFAULT_API_GATEWAY_CORS_CONFIGURATION,
Tags: tags
})
.promise();
this._apiGateway = { id: result.ApiId, endpoint: result.ApiEndpoint, tags };
}
async checkAPIGatewayTags() {
const api = (await this.getAPIGateway());
if (!this.constructor.managerIdentifiers.includes(api.tags['managed-by'])) {
this.throwError(`Cannot use an API Gateway that was not originally created by this tool (name: '${this.getAPIGatewayName()}')`);
}
}
async createOrUpdateAPIGatewayCustomDomainName() {
const config = this.getConfig();
this.logMessage(`Checking the API Gateway custom domain name...`);
let customDomainName = await this.getAPIGatewayCustomDomainName({ throwIfMissing: false });
if (customDomainName === undefined) {
customDomainName = await this.createAPIGatewayCustomDomainName();
}
const targetDomainName = customDomainName.apiGatewayDomainName;
const targetHostedZoneId = customDomainName.hostedZoneId;
await this.ensureRoute53Alias({ name: config.domainName, targetDomainName, targetHostedZoneId });
}
async getAPIGatewayCustomDomainName({ throwIfMissing = true } = {}) {
if (!this._apiGatewayCustomDomainName) {
const config = this.getConfig();
const apiGateway = this.getAPIGatewayV2Client();
let result;
try {
result = await apiGateway
.getDomainName({
DomainName: config.domainName
})
.promise();
}
catch (err) {
if (err.code !== 'NotFoundException') {
throw err;
}
}
if (result !== undefined) {
this._apiGatewayCustomDomainName = {
domainName: result.DomainName,
apiGatewayDomainName: result.DomainNameConfigurations[0].ApiGatewayDomainName,
hostedZoneId: result.DomainNameConfigurations[0].HostedZoneId
};
}
}
if (this._apiGatewayCustomDomainName === undefined && throwIfMissing) {
this.throwError('API Gateway custom domain name not found');
}
return this._apiGatewayCustomDomainName;
}
async createAPIGatewayCustomDomainName() {
const config = this.getConfig();
const apiGateway = this.getAPIGatewayV2Client();
this.logMessage(`Creating the API Gateway custom domain name...`);
const api = (await this.getAPIGateway());
const certificate = (await this.getACMCertificate());
const result = await apiGateway
.createDomainName({
DomainName: config.domainName,
DomainNameConfigurations: [
{
ApiGatewayDomainName: api.endpoint,
CertificateArn: certificate.arn,
EndpointType: 'REGIONAL',
SecurityPolicy: 'TLS_1_2'
}
]
})
.promise();
await apiGateway
.createApiMapping({ ApiId: api.id, DomainName: config.domainName, Stage: '$default' })
.promise();
this._apiGatewayCustomDomainName = {
domainName: result.DomainName,
apiGatewayDomainName: result.DomainNameConfigurations[0].ApiGatewayDomainName,
hostedZoneId: result.DomainNameConfigurations[0].HostedZoneId
};
return this._apiGatewayCustomDomainName;
}
getAPIGatewayName() {
return this.getConfig().domainName;
}
}
function millisecondsToEventBridgeScheduleExpression(milliseconds) {
let value;
let unit;
if (milliseconds > 0 && milliseconds % DAY === 0) {
value = milliseconds / DAY;
unit = 'day';
}
else if (milliseconds > 0 && milliseconds % HOUR === 0) {
value = milliseconds / HOUR;
unit = 'hour';
}
else if (milliseconds > 0 && milliseconds % MINUTE === 0) {
value = milliseconds / MINUTE;
unit = 'minute';
}
else {
throw new Error(`The specified number of milliseconds (${milliseconds}) cannot be converted to an EventBridge schedule expression`);
}
if (value > 1) {
unit += 's';
}
return `rate(${value} ${unit})`;
}
export function domainNameToLambdaFunctionName(domainName) {
return ensureMaximumStringLength(domainName.replace(/\./g, '-'), 64);
}
function eventBridgeRuleNameToLambdaPermissionStatementId(name) {
return name.replace(/\./g, '-');
}
//# sourceMappingURL=function.js.map