diff --git a/.github/workflows/config/lambda-environment-region-map.json b/.github/workflows/config/lambda-environment-region-map.json index 193833e146..898f669f8f 100644 --- a/.github/workflows/config/lambda-environment-region-map.json +++ b/.github/workflows/config/lambda-environment-region-map.json @@ -1,6 +1,7 @@ { "account-scoped": { "development": ["us-east-1"], "staging": ["us-east-1", "eu-west-1"], "production": ["us-east-1", "eu-west-1", "ca-central-1"] }, "scheduled-jobs-processor": { "development": ["us-east-1"], "staging": ["us-east-1", "eu-west-1"], "production": ["us-east-1", "eu-west-1", "ca-central-1"] }, + "external-task": { "development": ["us-east-1"], "staging": ["us-east-1", "eu-west-1"], "production": ["us-east-1", "eu-west-1", "ca-central-1"] }, "facebookCallback": { "development": ["us-east-1"], "staging": [], "production": ["us-east-1"] }, "facebookSignin": { "development": ["us-east-1"], "staging": [], "production": ["us-east-1"] }, "instagramWebhook": { "development": ["us-east-1"], "staging": [], "production": ["us-east-1"] }, diff --git a/.github/workflows/global-production-release-tag.yml b/.github/workflows/global-production-release-tag.yml index 0842109640..44c20fb1db 100644 --- a/.github/workflows/global-production-release-tag.yml +++ b/.github/workflows/global-production-release-tag.yml @@ -75,6 +75,7 @@ jobs: lambda_path: - account-scoped - scheduled-jobs-processor + - external-task - facebookCallback - facebookSignin - instagramWebhook diff --git a/.github/workflows/global-qa-release-tag.yml b/.github/workflows/global-qa-release-tag.yml index 68a405cdc3..8f2600e23d 100644 --- a/.github/workflows/global-qa-release-tag.yml +++ b/.github/workflows/global-qa-release-tag.yml @@ -117,6 +117,7 @@ jobs: lambda_path: - account-scoped - scheduled-jobs-processor + - external-task - facebookCallback - facebookSignin - instagramWebhook diff --git a/.github/workflows/twilio-lambda-ci.yml b/.github/workflows/twilio-lambda-ci.yml index 30e44911d0..58caf53707 100644 --- a/.github/workflows/twilio-lambda-ci.yml +++ b/.github/workflows/twilio-lambda-ci.yml @@ -76,6 +76,7 @@ jobs: lambda_path: - account-scoped - scheduled-jobs-processor + - external-task - facebookCallback - facebookSignin - instagramWebhook diff --git a/.github/workflows/twilio-lambda-deploy-all.yml b/.github/workflows/twilio-lambda-deploy-all.yml index cb4162716b..70f5b97827 100644 --- a/.github/workflows/twilio-lambda-deploy-all.yml +++ b/.github/workflows/twilio-lambda-deploy-all.yml @@ -55,6 +55,7 @@ jobs: lambda_path: - account-scoped - scheduled-jobs-processor + - external-task - facebookCallback - facebookSignin - instagramWebhook diff --git a/.github/workflows/twilio-lambda-deploy.yml b/.github/workflows/twilio-lambda-deploy.yml index e509f31c28..3394a42b8b 100644 --- a/.github/workflows/twilio-lambda-deploy.yml +++ b/.github/workflows/twilio-lambda-deploy.yml @@ -32,6 +32,7 @@ on: options: - account-scoped - scheduled-jobs-processor + - external-task - facebookCallback - facebookSignin - instagramWebhook diff --git a/lambdas/external-task/package.json b/lambdas/external-task/package.json new file mode 100644 index 0000000000..f24544439f --- /dev/null +++ b/lambdas/external-task/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tech-matters/twilio-external-task", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test:e2e": "jest tests/e2e", + "docker:build": "docker build -t external-task --build-arg lambda_name=external-task --build-arg lambda_dir=. -f ../Dockerfile ../" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/aws-lambda": "^8.10.108", + "@types/node": "^22.13.11", + "ts-node": "^10.9.1" + }, + "dependencies": { + "@tech-matters/result-type": "^1.0.0", + "@tech-matters/ssm-cache": "^1.0.0", + "@tech-matters/twilio-configuration": "^1.0.0", + "@tech-matters/twilio-types": "^1.0.0" + } +} diff --git a/lambdas/external-task/src/index.ts b/lambdas/external-task/src/index.ts new file mode 100644 index 0000000000..657aec42d8 --- /dev/null +++ b/lambdas/external-task/src/index.ts @@ -0,0 +1,196 @@ +/** + * Copyright (C) 2021-2025 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import type { ALBEvent, ALBResult } from 'aws-lambda'; +import type { DocumentInstance } from 'twilio/lib/rest/sync/v1/service/document'; +import { isErr } from '@tech-matters/result-type'; +import { + getMasterWorkflowSid, + getSyncServiceSid, + getTwilioClient, + getWorkspaceSid, +} from '@tech-matters/twilio-configuration'; +import { channelTypes } from '@tech-matters/twilio-types'; +import { authenticateWithExternalApiKey } from './requestValidator'; +import { getExternalTaskMapping } from './mappings'; + +const headers = { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST,OPTIONS', + 'Access-Control-Allow-Headers': + 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', +}; + +const handleError = (message: string, error?: Error, statusCode = 500): ALBResult => { + if (error) { + console.error(message, error); + } else { + console.error(message); + } + return { + statusCode, + headers, + body: JSON.stringify({ message, error: error?.message }), + }; +}; + +// TODO: factor out to share with account-scoped lambda? +const parseBody = ({ + body, + contentTypeHeader, + isBase64Encoded, +}: { + contentTypeHeader: string | null; + body: string | null; + isBase64Encoded: boolean; +}) => { + if (!body || !contentTypeHeader) { + return body; + } + + // if (contentTypeHeader.includes('application/x-www-form-urlencoded')) { + // if (isBase64Encoded) { + // const data = Buffer.from(body, 'base64').toString(); + // return qs.parse(data); + // } + + // return qs.parse(body); + // } + + if (contentTypeHeader === 'application/json') { + if (isBase64Encoded) { + const data = Buffer.from(body, 'base64').toString(); + return JSON.parse(data); + } + + return JSON.parse(body || 'null'); + } +}; + +export const handler = async (event: ALBEvent): Promise => { + try { + console.debug('[SENSITIVE] triggered with event', event); + + const validationResult = await authenticateWithExternalApiKey({ event }); + + if (isErr(validationResult)) { + return handleError( + validationResult.message, + validationResult.error.cause, + validationResult.error.statusCode, + ); + } + + const { accountSid, accountShortCode } = validationResult.data; + + if (event.httpMethod === 'POST') { + if (!event.body) { + return handleError('Event body is null or undefined', undefined, 400); + } + + let dedupDocument: DocumentInstance | null = null; + const twilioClient = await getTwilioClient(accountSid); + const syncService = await getSyncServiceSid(accountSid); + + try { + const body = parseBody({ + body: event.body, + contentTypeHeader: event.headers?.['content-type'] || null, + isBase64Encoded: event.isBase64Encoded, + }); + + const mappingResult = getExternalTaskMapping({ accountShortCode })(body); + if (isErr(mappingResult)) { + return handleError(mappingResult.message, mappingResult.error, 400); + } + + const { externalId, externalTaskAttributes } = mappingResult.unwrap(); + + const dedupDocumentName = `external-task-${externalId}`; + + console.debug(`Creating dedupDocument with name ${dedupDocumentName}`); + dedupDocument = await twilioClient.sync.v1 + .services(syncService) + .documents.create({ + ttl: 86400, // one day + uniqueName: dedupDocumentName, + }); + + const workspaceSid = await getWorkspaceSid(accountSid); + const workflowSid = await getMasterWorkflowSid(accountSid); + const createdTask = await twilioClient.taskrouter.v1 + .workspaces(workspaceSid) + .tasks.create({ + taskChannel: channelTypes.EXTERNAL, + workflowSid, + attributes: JSON.stringify({ + externalId, + externalTaskAttributes, + from: 'Placeholder', + name: 'Placeholder', + channelType: channelTypes.EXTERNAL, + customChannelType: channelTypes.EXTERNAL, + ignoreAgent: '', + transferTargetType: '', + }), + }); + + console.debug('[SENSITIVE] Created external task', createdTask); + + return { + statusCode: 200, + headers, + body: JSON.stringify({ taskSid: createdTask.sid }), + }; + } catch (error) { + if (dedupDocument) { + try { + console.debug(`Removing dedupDocument ${JSON.stringify(dedupDocument)}`); + await twilioClient.sync.v1 + .services(syncService) + .documents(dedupDocument.sid) + .remove(); + } catch (err) { + console.error(`Failed removing dedupDocument`); + } + } + + // bubble up + throw error; + } + } + + if (event.httpMethod === 'OPTIONS') { + // Handle preflight CORS requests + return { + statusCode: 200, + headers, + body: '', + }; + } + + // Handle unsupported HTTP methods + return { + statusCode: 405, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: 'Error: Method Not Allowed' }), + }; + } catch (error) { + const message = 'Error handling request'; + return handleError(message, error as Error); + } +}; diff --git a/lambdas/external-task/src/mappings/index.ts b/lambdas/external-task/src/mappings/index.ts new file mode 100644 index 0000000000..da6e17e18a --- /dev/null +++ b/lambdas/external-task/src/mappings/index.ts @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2021-2023 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { newErr, Result } from '@tech-matters/result-type'; +import { mappingFunction as uscrMapping } from './uscrMappings'; + +type ExternalTaskMapping = { + externalId: string; + externalTaskAttributes: Record; +}; +export type ExternalTaskMappingFunction = ( + payload: any, +) => Result; + +export const getExternalTaskMapping = ({ + accountShortCode, +}: { + accountShortCode: string; +}): ExternalTaskMappingFunction => { + switch (accountShortCode.toUpperCase()) { + case 'E2E': + case 'AS': + case 'USCR': { + return uscrMapping; + } + default: { + const message = `Mapping not defined for account short code ${accountShortCode}`; + return () => + newErr({ + message, + error: new Error(message), + }); + } + } +}; diff --git a/lambdas/external-task/src/mappings/uscrMappings/index.ts b/lambdas/external-task/src/mappings/uscrMappings/index.ts new file mode 100644 index 0000000000..752629deab --- /dev/null +++ b/lambdas/external-task/src/mappings/uscrMappings/index.ts @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2021-2023 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { newErr, newOk } from '@tech-matters/result-type'; +import type { ExternalTaskMappingFunction } from '..'; + +export const mappingFunction: ExternalTaskMappingFunction = payload => { + if (!payload.key) { + const message = 'Missing key property in payload'; + return newErr({ message, error: new Error(message) }); + } + + return newOk({ externalId: payload.key, externalTaskAttributes: {} }); +}; diff --git a/lambdas/external-task/src/requestValidator.ts b/lambdas/external-task/src/requestValidator.ts new file mode 100644 index 0000000000..6dac27351a --- /dev/null +++ b/lambdas/external-task/src/requestValidator.ts @@ -0,0 +1,117 @@ +/** + * Copyright (C) 2021-2025 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { timingSafeEqual } from 'crypto'; +import { ALBEvent } from 'aws-lambda'; +import { isErr, newErr, newOk, Result } from '@tech-matters/result-type'; +import { getSsmParameter } from '@tech-matters/ssm-cache'; +import { getAccountSid } from '@tech-matters/twilio-configuration'; +import { AccountSID } from '@tech-matters/twilio-types'; + +export type HttpError = { + statusCode: number; + cause?: Error; +}; +export const ROUTE_PREFIX = '/lambda/twilio/external-task/'; +const getAccountSidFromPath = async (event: ALBEvent) => { + try { + if (event.path.startsWith(ROUTE_PREFIX)) { + const path = event.path.substring(ROUTE_PREFIX.length); + const [accountShortCode] = path.split('/'); // ignore anothing after account short code + + if (!accountShortCode) { + const message = 'Missing account short code'; + return newErr({ + message, + error: { statusCode: 400 }, + }); + } + + const accountSid = await getAccountSid(accountShortCode); + return newOk({ accountSid, accountShortCode }); + } + + const message = 'Invalid route prefix'; + return newErr({ + message, + error: { statusCode: 400 }, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return newErr({ + message, + error: { statusCode: 500 }, + }); + } +}; + +export const authenticateWithExternalApiKey = async ({ + event, +}: { + event: ALBEvent; +}): Promise> => { + if (!event.headers) { + const message = 'Missing headers in request'; + return newErr({ message, error: { statusCode: 400 } }); + } + + const { + headers: { authorization }, + } = event; + + if (!authorization || !authorization.startsWith('Basic')) { + const message = 'Invalid authorization header'; + return newErr({ message, error: { statusCode: 400 } }); + } + + const result = await getAccountSidFromPath(event); + + if (isErr(result)) { + return result; + } + + const { accountSid } = result.data; + + const externalTaskApiKeyParam = `/${process.env.NODE_ENV}/twilio/${accountSid}/external_task_api_key`; + const requestSecret = authorization.replace('Basic ', ''); + const externalTaskApiKey = await getSsmParameter(externalTaskApiKeyParam); + if (!externalTaskApiKey) { + const message = `Missing parameter ${externalTaskApiKeyParam}`; + return newErr({ message, error: { statusCode: 500 } }); + } + + try { + console.debug(`Authenticating against key ${externalTaskApiKeyParam} `); + const isStaticSecretValid = + externalTaskApiKey && + requestSecret && + timingSafeEqual(Buffer.from(requestSecret), Buffer.from(externalTaskApiKey)); + + if (isStaticSecretValid) { + console.debug( + `Successfully authenticated against static key ${externalTaskApiKeyParam}`, + ); + + return newOk(result.data); + } + } catch (err) { + const message = `Static key authentication failed for ${externalTaskApiKeyParam}`; + return newErr({ message, error: { statusCode: 403 } }); + } + + const message = 'Invalid state reached'; + return newErr({ message, error: { statusCode: 500 } }); +}; diff --git a/lambdas/external-task/tsconfig.build.json b/lambdas/external-task/tsconfig.build.json new file mode 100644 index 0000000000..46052024a8 --- /dev/null +++ b/lambdas/external-task/tsconfig.build.json @@ -0,0 +1,12 @@ +// This file is copied to the lambda root at build time, so all paths are relative to the root +{ + "extends": "./tsconfig.base.json", + "files": [], + "references": [ + { "path": "packages/result-type" }, + { "path": "packages/ssm-cache" }, + { "path": "packages/twilio-types" }, + { "path": "packages/twilio-configuration" }, + { "path": "external-task" } + ] +} diff --git a/lambdas/external-task/tsconfig.json b/lambdas/external-task/tsconfig.json new file mode 100644 index 0000000000..839643cc66 --- /dev/null +++ b/lambdas/external-task/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src/**/*.ts", "src/**/*.json"], + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/lambdas/package-lock.json b/lambdas/package-lock.json index cdbd910b9d..69c49c2f3b 100644 --- a/lambdas/package-lock.json +++ b/lambdas/package-lock.json @@ -11,6 +11,7 @@ "workspaces": [ "account-scoped", "scheduled-jobs-processor", + "external-task", "facebookCallback", "facebookSignin", "instagramWebhook", @@ -27,8 +28,7 @@ "packages/*" ], "dependencies": { - "dotenv": "^16.3.1", - "twilio": "^6.0.2" + "dotenv": "^16.3.1" }, "devDependencies": { "@tsconfig/node18": "^18.2.1", @@ -76,6 +76,7 @@ "@tech-matters/hrm-form-definitions": "^1.0.0", "@tech-matters/hrm-types": "^1.0.0", "@tech-matters/result-type": "^1.0.0", + "@tech-matters/s3": "^1.0.0", "@tech-matters/scheduled-jobs": "^1.0.0", "@tech-matters/ssm-cache": "^1.0.0", "@tech-matters/twilio-configuration": "^1.0.0", @@ -951,6 +952,40 @@ "uuid": "dist/bin/uuid" } }, + "external-task": { + "name": "@tech-matters/twilio-external-task", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@tech-matters/result-type": "^1.0.0", + "@tech-matters/ssm-cache": "^1.0.0", + "@tech-matters/twilio-configuration": "^1.0.0", + "@tech-matters/twilio-types": "^1.0.0" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/aws-lambda": "^8.10.108", + "@types/node": "^22.13.11", + "ts-node": "^10.9.1" + } + }, + "external-task/node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "external-task/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "facebookCallback": { "name": "@tech-matters/facebookcallback", "version": "1.0.0", @@ -6583,6 +6618,10 @@ "resolved": "packages/twilio-configuration", "link": true }, + "node_modules/@tech-matters/twilio-external-task": { + "resolved": "external-task", + "link": true + }, "node_modules/@tech-matters/twilio-scheduled-jobs-processor": { "resolved": "scheduled-jobs-processor", "link": true @@ -18353,7 +18392,7 @@ "dependencies": { "@tech-matters/ssm-cache": "^1.0.0", "@tech-matters/twilio-types": "^1.0.0", - "twilio": "^6.0.2" + "twilio": "~6.0.2" }, "devDependencies": { "@tsconfig/node22": "^22.0.0", @@ -24152,6 +24191,7 @@ "@tech-matters/hrm-form-definitions": "^1.0.0", "@tech-matters/hrm-types": "^1.0.0", "@tech-matters/result-type": "^1.0.0", + "@tech-matters/s3": "^1.0.0", "@tech-matters/scheduled-jobs": "^1.0.0", "@tech-matters/ssm-cache": "^1.0.0", "@tech-matters/twilio-configuration": "^1.0.0", @@ -24828,10 +24868,40 @@ "@tech-matters/twilio-types": "^1.0.0", "@tsconfig/node22": "^22.0.0", "ts-node": "^10.1.0", - "twilio": "^6.0.2", + "twilio": "~6.0.2", "typescript": "^5.9.2" } }, + "@tech-matters/twilio-external-task": { + "version": "file:external-task", + "requires": { + "@tech-matters/result-type": "^1.0.0", + "@tech-matters/ssm-cache": "^1.0.0", + "@tech-matters/twilio-configuration": "^1.0.0", + "@tech-matters/twilio-types": "^1.0.0", + "@tsconfig/node22": "^22.0.0", + "@types/aws-lambda": "^8.10.108", + "@types/node": "^22.13.11", + "ts-node": "^10.9.1" + }, + "dependencies": { + "@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "requires": { + "undici-types": "~6.21.0" + } + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + } + } + }, "@tech-matters/twilio-scheduled-jobs-processor": { "version": "file:scheduled-jobs-processor", "requires": { diff --git a/lambdas/package.json b/lambdas/package.json index 54159fa479..df9873c246 100644 --- a/lambdas/package.json +++ b/lambdas/package.json @@ -7,6 +7,7 @@ "workspaces": [ "account-scoped", "scheduled-jobs-processor", + "external-task", "facebookCallback", "facebookSignin", "instagramWebhook", @@ -23,8 +24,7 @@ "packages/*" ], "dependencies": { - "dotenv": "^16.3.1", - "twilio": "^6.0.2" + "dotenv": "^16.3.1" }, "devDependencies": { "@tsconfig/node18": "^18.2.1", diff --git a/lambdas/packages/twilio-configuration/package.json b/lambdas/packages/twilio-configuration/package.json index 32cde735db..c9b841f512 100644 --- a/lambdas/packages/twilio-configuration/package.json +++ b/lambdas/packages/twilio-configuration/package.json @@ -6,7 +6,7 @@ "dependencies": { "@tech-matters/ssm-cache": "^1.0.0", "@tech-matters/twilio-types": "^1.0.0", - "twilio": "^6.0.2" + "twilio": "~6.0.2" }, "devDependencies": { "@tsconfig/node22": "^22.0.0", diff --git a/lambdas/packages/twilio-types/src/channelType.ts b/lambdas/packages/twilio-types/src/channelType.ts index ffbcc2d2e7..ca82618fd8 100644 --- a/lambdas/packages/twilio-types/src/channelType.ts +++ b/lambdas/packages/twilio-types/src/channelType.ts @@ -27,6 +27,7 @@ export const isAseloCustomChannelType = (channelType?: string): boolean => Object.values(aseloCustomChannelTypes).includes(channelType as AseloCustomChannelType); export const channelTypes = { + EXTERNAL: 'external', VOICEMAIL: 'voicemail', SMS: 'sms', VOICE: 'voice', diff --git a/lambdas/tsconfig.json b/lambdas/tsconfig.json index d2fe9b2bca..30d0d11657 100644 --- a/lambdas/tsconfig.json +++ b/lambdas/tsconfig.json @@ -15,6 +15,7 @@ { "path": "packages/scheduled-jobs" }, { "path": "account-scoped" }, { "path": "scheduled-jobs-processor" }, + { "path": "external-task" }, { "path": "facebookCallback" }, { "path": "facebookSignin" }, { "path": "instagramWebhook" },