From 6d65bea828338a6dd44cb357c324d9b63e74e081 Mon Sep 17 00:00:00 2001 From: makserik <15821542+makserik@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:25:58 +0200 Subject: [PATCH] FRIDGE-814 upgrade to twilio 4.23.0, fix tests (#509) * FRIDGE-814 upgrade to twilio 4.23.0, fix tests * improvement(@twilio-labs/serverless-runtime-types, @twilio/runtime-handler, twilio-run): Twilio SDK Twilio SDK bump from 3.x to 4.23 with fixes to types, tests BREAKING CHANGE: Underlying Twilio SDK was bumped by a major version, Twilio SDK 3.x to 4.23. This might require changes in how importing this package will work. fix #473 #483 #455 * FRIDGE-814 revert logger.ts * FRIDGE-814 add changeset * improvement(plugin-serverless plugin-assets runtime-handler serverless-api serverless-twilio-runtime change node.js to 18 minimum in required and in docs where necessary BREAKING CHANGE: minimum Node.js now 18 * FRIDGE-814 remove node 16 github actions, make it use npm@10 * FRIDGE-814 replace deprecated npm-run-all with a maintained fork * FRIDGE-814 remove npm-run-all deps and convert commands to regular as the package seems broken on windows * add typescript install explicitly * Revert "FRIDGE-814 remove node 16 github actions, make it use npm@10" This reverts commit 2ca5371bee3506921d5c303c8461a59f31a97877. * remove node 16 * update snapshots * fix snapshots --- .changeset/strong-pugs-battle.md | 7 ++ .github/workflows/nodejs.yml | 2 +- .gitignore | 2 + docs/CONTRIBUTING.md | 4 +- package.json | 2 +- packages/plugin-assets/package.json | 2 +- packages/plugin-serverless/package.json | 2 +- .../utils/requireFromProject.test.ts | 2 +- packages/runtime-handler/package.json | 10 +-- packages/serverless-api/package.json | 2 +- .../serverless-runtime-types/package.json | 2 +- packages/serverless-twilio-runtime/Readme.md | 2 +- .../runtime/internal/response.test.ts | 35 +++++++-- packages/twilio-run/package.json | 10 +-- .../src/runtime/internal/functionRunner.ts | 3 +- .../src/runtime/internal/response.ts | 74 ++++++++++++++++--- packages/twilio-run/src/runtime/route.ts | 15 +++- 17 files changed, 137 insertions(+), 39 deletions(-) create mode 100644 .changeset/strong-pugs-battle.md diff --git a/.changeset/strong-pugs-battle.md b/.changeset/strong-pugs-battle.md new file mode 100644 index 00000000..022e7e9d --- /dev/null +++ b/.changeset/strong-pugs-battle.md @@ -0,0 +1,7 @@ +--- +'@twilio-labs/serverless-runtime-types': major +'@twilio/runtime-handler': major +'twilio-run': major +--- + +Twilio SDK from 3.x to 4.23.0. Required Node version bumped to 18 min. diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index c9bdd336..71c6cc89 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - node-version: [ lts/-2, lts/-1, lts/*] + node-version: [ lts/-1, lts/*] steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index afa6e160..9bf6cf7d 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,5 @@ dist/ packages/serverless-api/docs/ .idea + +**/.DS_Store diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 3638be51..7e647123 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -14,8 +14,8 @@ If you are planning to contribute something that does not have an open issue yet ## Requirements -Make sure you have Node.js 14 or newer installed. Due to compatibility with Twilio -Functions this project has to support at least Node.js 14.0.0. +Make sure you have Node.js 18 or newer installed. Due to compatibility with Twilio +Functions this project has to support at least Node.js 18.0.0. We are using the npm CLI to manage our project. You'll need at least `npm` version 8 or newer. diff --git a/package.json b/package.json index 7a982d3b..f47b5bf0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@changesets/cli": "^2.26.0", "@commitlint/cli": "^19.1.0", "@commitlint/config-conventional": "^19.1.0", - "@twilio/test-dep": "npm:twilio@3.84.0", + "@twilio/test-dep": "npm:twilio@4.23.0", "@types/jest": "^29.2.4", "all-contributors-cli": "^6.1.2", "commitizen": "^4.2.4", diff --git a/packages/plugin-assets/package.json b/packages/plugin-assets/package.json index 2127e35a..02998bb2 100644 --- a/packages/plugin-assets/package.json +++ b/packages/plugin-assets/package.json @@ -23,7 +23,7 @@ "rimraf": "^3.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "files": [ "/oclif.manifest.json", diff --git a/packages/plugin-serverless/package.json b/packages/plugin-serverless/package.json index bb4f6a8d..b1935b53 100644 --- a/packages/plugin-serverless/package.json +++ b/packages/plugin-serverless/package.json @@ -22,7 +22,7 @@ "oclif": "^4.0.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" }, "files": [ "/oclif.manifest.json", diff --git a/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts b/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts index fb80fc9c..4b17bd42 100644 --- a/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts +++ b/packages/runtime-handler/__tests__/dev-runtime/utils/requireFromProject.test.ts @@ -2,7 +2,7 @@ import { join } from 'path'; import { requireFromProject } from '../../../src/dev-runtime/utils/requireFromProject'; const PROJECT_DIR = join(__dirname, '../../../../twilio-run'); - +// only works if test-dep version is different from root package version jest.mock('../../../../twilio-run/node_modules/@twilio/test-dep', () => { const x = jest.genMockFromModule('@twilio/test-dep'); (x as any)['__TYPE__'] = 'PROJECT_BASED'; diff --git a/packages/runtime-handler/package.json b/packages/runtime-handler/package.json index 6467bb9a..c40a1baa 100644 --- a/packages/runtime-handler/package.json +++ b/packages/runtime-handler/package.json @@ -36,22 +36,22 @@ "build": "tsc", "watch": "tsc --watch", "build:noemit": "tsc --noEmit", - "clean": "rimraf ./dist" + "clean": "rimraf ./dist ./node_modules" }, "devDependencies": { + "@twilio/test-dep": "npm:twilio@4.22.0", "@types/common-tags": "^1.8.0", "@types/cookie-parser": "^1.4.2", "@types/debug": "^4.1.4", "@types/express-useragent": "^0.2.21", "@types/jest": "^26.0.24", "@types/lodash.debounce": "^4.0.6", - "@types/node": "^14.0.19", + "@types/node": "^18.0.0", "@types/supertest": "^2.0.8", "npm-run-all": "^4.1.5", "rimraf": "^2.6.3", "supertest": "^3.1.0", - "typescript": "^4.9.4", - "@twilio/test-dep": "npm:twilio@3.80.0" + "typescript": "^4.9.4" }, "bugs": { "url": "https://github.com/twilio-labs/serverless-toolkit/issues" @@ -69,7 +69,7 @@ "nocache": "^2.1.0", "normalize.css": "^8.0.1", "serialize-error": "^7.0.1", - "twilio": "3.80.0" + "twilio": "4.23.0" }, "gitHead": "6db273648ed19474f4125042556b10c051529912" } diff --git a/packages/serverless-api/package.json b/packages/serverless-api/package.json index 383b1122..7d6b1bde 100644 --- a/packages/serverless-api/package.json +++ b/packages/serverless-api/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@types/mime-types": "^2.1.0", - "@types/node": "^14.17.19", + "@types/node": "^18.0.0", "@types/recursive-readdir": "^2.2.0", "debug": "^4.1.1", "fast-redact": "^1.5.0", diff --git a/packages/serverless-runtime-types/package.json b/packages/serverless-runtime-types/package.json index 2a3bb2ff..72c94185 100644 --- a/packages/serverless-runtime-types/package.json +++ b/packages/serverless-runtime-types/package.json @@ -30,7 +30,7 @@ "dependencies": { "@types/express": "^4.17.11", "@types/qs": "^6.9.4", - "twilio": "^4.20.1" + "twilio": "^4.23.0" }, "devDependencies": { "@types/express": "^4.17.11", diff --git a/packages/serverless-twilio-runtime/Readme.md b/packages/serverless-twilio-runtime/Readme.md index 72dea8a6..8afa3c06 100644 --- a/packages/serverless-twilio-runtime/Readme.md +++ b/packages/serverless-twilio-runtime/Readme.md @@ -8,7 +8,7 @@ Serverless Framework plugin to deploy to the Twilio Runtime. ### Pre-requisites -- Node.js v12.x (this is the runtime version supported by Twilio Functions) +- Node.js v18.x (this is the runtime version supported by Twilio Functions) - Serverless CLI v1.50.0+. You can run npm i -g serverless if you don't already have it. - A Twilio account. If you don't have one you can [sign up quickly](https://www.twilio.com/try-twilio). diff --git a/packages/twilio-run/__tests__/runtime/internal/response.test.ts b/packages/twilio-run/__tests__/runtime/internal/response.test.ts index 18602fc9..472a4bdf 100644 --- a/packages/twilio-run/__tests__/runtime/internal/response.test.ts +++ b/packages/twilio-run/__tests__/runtime/internal/response.test.ts @@ -5,7 +5,9 @@ test('has correct defaults', () => { const response = new Response(); expect(response['body']).toBeNull(); expect(response['statusCode']).toBe(200); - expect(response['headers']).toEqual({}); + expect(response['headers']).toEqual({ + 'Set-Cookie': [], + }); }); test('sets status code, body and headers from constructor', () => { @@ -24,6 +26,7 @@ test('sets status code, body and headers from constructor', () => { 'Access-Control-Allow-Origin': 'example.com', 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE', 'Access-Control-Allow-Headers': 'Content-Type', + 'Set-Cookie': [], }); }); @@ -45,7 +48,9 @@ test('sets body correctly', () => { test('sets headers correctly', () => { const response = new Response(); - expect(response['headers']).toEqual({}); + expect(response['headers']).toEqual({ + 'Set-Cookie': [], + }); response.setHeaders({ 'Access-Control-Allow-Origin': 'example.com', 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE', @@ -64,26 +69,33 @@ test('sets headers correctly', () => { test('appends a new header correctly', () => { const response = new Response(); - expect(response['headers']).toEqual({}); + expect(response['headers']).toEqual({ + 'Set-Cookie': [], + }); response.appendHeader('Access-Control-Allow-Origin', 'dkundel.com'); expect(response['headers']).toEqual({ 'Access-Control-Allow-Origin': 'dkundel.com', + 'Set-Cookie': [], }); response.appendHeader('Content-Type', 'application/json'); expect(response['headers']).toEqual({ 'Access-Control-Allow-Origin': 'dkundel.com', 'Content-Type': 'application/json', + 'Set-Cookie': [], }); }); test('appends a header correctly with no existing one', () => { const response = new Response(); - expect(response['headers']).toEqual({}); + expect(response['headers']).toEqual({ + 'Set-Cookie': [], + }); // @ts-ignore response['headers'] = undefined; response.appendHeader('Access-Control-Allow-Origin', 'dkundel.com'); expect(response['headers']).toEqual({ 'Access-Control-Allow-Origin': 'dkundel.com', + 'Set-Cookie': [], }); }); @@ -121,7 +133,10 @@ test('calls express response correctly', () => { expect(mockRes.send).toHaveBeenCalledWith(`I'm a teapot!`); expect(mockRes.status).toHaveBeenCalledWith(418); - expect(mockRes.set).toHaveBeenCalledWith({ 'Content-Type': 'text/plain' }); + expect(mockRes.set).toHaveBeenCalledWith({ + 'Content-Type': 'text/plain', + 'Set-Cookie': [], + }); }); test('serializes a response', () => { @@ -134,7 +149,10 @@ test('serializes a response', () => { expect(serialized.body).toEqual("I'm a teapot!"); expect(serialized.statusCode).toEqual(418); - expect(serialized.headers).toEqual({ 'Content-Type': 'text/plain' }); + expect(serialized.headers).toEqual({ + 'Content-Type': 'text/plain', + 'Set-Cookie': [], + }); }); test('serializes a response with content type set to application/json', () => { @@ -149,5 +167,8 @@ test('serializes a response with content type set to application/json', () => { JSON.stringify({ url: 'https://dkundel.com' }) ); expect(serialized.statusCode).toEqual(200); - expect(serialized.headers).toEqual({ 'Content-Type': 'application/json' }); + expect(serialized.headers).toEqual({ + 'Content-Type': 'application/json', + 'Set-Cookie': [], + }); }); diff --git a/packages/twilio-run/package.json b/packages/twilio-run/package.json index 8343386d..fa0ee25c 100644 --- a/packages/twilio-run/package.json +++ b/packages/twilio-run/package.json @@ -35,7 +35,7 @@ "license": "MIT", "dependencies": { "@twilio-labs/serverless-api": "^5.5.1", - "@twilio-labs/serverless-runtime-types": "2.1.0-rc.0", + "@twilio-labs/serverless-runtime-types": "3.0.0", "@types/express": "4.17.7", "@types/inquirer": "^6.0.3", "@types/is-ci": "^2.0.0", @@ -76,7 +76,7 @@ "serialize-error": "^7.0.1", "terminal-link": "^1.3.0", "title": "^3.4.1", - "twilio": "^3.60.0", + "twilio": "^4.23.0", "type-fest": "^0.15.1", "window-size": "^1.1.1", "wrap-ansi": "^7.0.0", @@ -102,7 +102,7 @@ "@types/lodash.kebabcase": "^4.1.6", "@types/lodash.startcase": "^4.4.6", "@types/mock-fs": "^4.10.0", - "@types/node": "^14.0.19", + "@types/node": "^18.0.0", "@types/prompts": "^2.0.1", "@types/supertest": "^2.0.8", "@types/title": "^1.0.5", @@ -113,7 +113,7 @@ "nock": "^12.0.2", "supertest": "^3.1.0", "typescript": "^4.9.4", - "@twilio/test-dep": "npm:twilio@3.60.0" + "@twilio/test-dep": "npm:twilio@4.22.0" }, "files": [ "bin/", @@ -122,7 +122,7 @@ "README.md" ], "engines": { - "node": ">=12.22.1" + "node": ">=18.0.0" }, "gitHead": "6db273648ed19474f4125042556b10c051529912" } diff --git a/packages/twilio-run/src/runtime/internal/functionRunner.ts b/packages/twilio-run/src/runtime/internal/functionRunner.ts index 4f88351a..de98cf66 100644 --- a/packages/twilio-run/src/runtime/internal/functionRunner.ts +++ b/packages/twilio-run/src/runtime/internal/functionRunner.ts @@ -3,6 +3,7 @@ import { serializeError } from 'serialize-error'; import { getRouteMap } from '../internal/route-cache'; import { constructContext, constructGlobalScope, isTwiml } from '../route'; import { Response } from './response'; +import { Headers } from '@twilio/runtime-handler/dist/dev-runtime/types'; const sendDebugMessage = (debugMessage: string, ...debugArgs: any) => { process.send && process.send({ debugMessage, debugArgs }); @@ -10,7 +11,7 @@ const sendDebugMessage = (debugMessage: string, ...debugArgs: any) => { export type Reply = { body?: string | number | boolean | object; - headers?: { [key: string]: number | string }; + headers?: Headers; statusCode: number; }; diff --git a/packages/twilio-run/src/runtime/internal/response.ts b/packages/twilio-run/src/runtime/internal/response.ts index f21442e7..4cd32f03 100644 --- a/packages/twilio-run/src/runtime/internal/response.ts +++ b/packages/twilio-run/src/runtime/internal/response.ts @@ -1,8 +1,13 @@ import { TwilioResponse } from '@twilio-labs/serverless-runtime-types/types'; import { Response as ExpressResponse } from 'express'; import { getDebugFunction } from '../../utils/logger'; +import { + HeaderValue, + Headers, +} from '@twilio/runtime-handler/dist/dev-runtime/types'; const debug = getDebugFunction('twilio-run:response'); +const COOKIE_HEADER = 'Set-Cookie'; type ResponseOptions = { headers?: Headers; @@ -10,11 +15,6 @@ type ResponseOptions = { body?: object | string; }; -type HeaderValue = number | string; -type Headers = { - [key: string]: HeaderValue; -}; - export class Response implements TwilioResponse { private body: null | any; private statusCode: number; @@ -34,21 +34,29 @@ export class Response implements TwilioResponse { if (options && options.headers) { this.headers = options.headers; } + // if Set-Cookie is not already in the headers, then add it as an empty list + const cookieHeader: HeaderValue = this.headers[COOKIE_HEADER]; + if (!(COOKIE_HEADER in this.headers)) { + this.headers[COOKIE_HEADER] = []; + } + if (!Array.isArray(cookieHeader) && typeof cookieHeader !== 'undefined') { + this.headers[COOKIE_HEADER] = [cookieHeader]; + } } - setStatusCode(statusCode: number): Response { + setStatusCode(statusCode: number): TwilioResponse { debug('Setting status code to %d', statusCode); this.statusCode = statusCode; return this; } - setBody(body: object | string): Response { + setBody(body: object | string): TwilioResponse { debug('Setting response body to %o', body); this.body = body; return this; } - setHeaders(headersObject: Headers): Response { + setHeaders(headersObject: Headers): TwilioResponse { debug('Setting headers to: %P', headersObject); if (typeof headersObject !== 'object') { return this; @@ -60,7 +68,31 @@ export class Response implements TwilioResponse { appendHeader(key: string, value: HeaderValue): Response { debug('Appending header for %s', key, value); this.headers = this.headers || {}; - this.headers[key] = value; + let newHeaderValue: HeaderValue = []; + if (key.toLowerCase() === COOKIE_HEADER.toLowerCase()) { + const existingValue: HeaderValue = this.headers[COOKIE_HEADER]; + if (existingValue) { + newHeaderValue = [existingValue, value].flat(); + if (newHeaderValue) { + this.headers[COOKIE_HEADER] = newHeaderValue; + } + } else { + this.headers[COOKIE_HEADER] = Array.isArray(value) ? value : [value]; + } + } else { + const existingValue: HeaderValue = this.headers[key]; + if (existingValue) { + newHeaderValue = [existingValue, value].flat(); + if (newHeaderValue) { + this.headers[key] = newHeaderValue; + } + } else { + this.headers[key] = value; + } + } + if (!(COOKIE_HEADER in this.headers)) { + this.headers[COOKIE_HEADER] = []; + } return this; } @@ -85,4 +117,28 @@ export class Response implements TwilioResponse { headers: this.headers, }; } + + setCookie(key: string, value: string, attributes: string[] = []): Response { + debug('Setting cookie %s=%s', key, value); + const cookie = + `${key}=${value}` + + (attributes.length > 0 ? `;${attributes.join(';')}` : ''); + this.appendHeader(COOKIE_HEADER, cookie); + return this; + } + + removeCookie(key: string): TwilioResponse { + debug('Removing cookie %s', key); + let cookieHeader: HeaderValue = this.headers[COOKIE_HEADER]; + if (!Array.isArray(cookieHeader)) { + cookieHeader = [cookieHeader]; + } + const newCookies: (string | number)[] = cookieHeader.filter( + (cookie: string | number) => + typeof cookie === 'string' && !cookie.startsWith(`${key}=`) + ); + newCookies.push(`${key}=;Max-Age=0`); + this.headers[COOKIE_HEADER] = newCookies; + return this; + } } diff --git a/packages/twilio-run/src/runtime/route.ts b/packages/twilio-run/src/runtime/route.ts index 6616afaa..342fa55a 100644 --- a/packages/twilio-run/src/runtime/route.ts +++ b/packages/twilio-run/src/runtime/route.ts @@ -1,6 +1,7 @@ import { Context, ServerlessCallback, + ServerlessEventObject, ServerlessFunctionSignature, } from '@twilio-labs/serverless-runtime-types/types'; import { fork } from 'child_process'; @@ -22,6 +23,7 @@ import { cleanUpStackTrace } from '../utils/stack-trace/clean-up'; import { Reply } from './internal/functionRunner'; import { Response } from './internal/response'; import * as Runtime from './internal/runtime'; +import * as PATH from 'path'; const RUNNER_PATH = process.env.NODE_ENV === 'test' @@ -32,7 +34,9 @@ const { VoiceResponse, MessagingResponse, FaxResponse } = twiml; const debug = getDebugFunction('twilio-run:route'); -export function constructEvent(req: ExpressRequest): T { +export function constructEvent( + req: ExpressRequest +): T { return { ...req.query, ...req.body }; } @@ -62,7 +66,14 @@ export function constructContext( } const DOMAIN_NAME = url.replace(/^https?:\/\//, ''); const PATH = functionPath; - return { PATH, DOMAIN_NAME, ...env, getTwilioClient }; + return { + ENVIRONMENT_SID: undefined, + SERVICE_SID: undefined, + PATH, + DOMAIN_NAME, + ...env, + getTwilioClient, + }; } export function constructGlobalScope(config: StartCliConfig): void {