From 7b70013d6dd1cd20f8b5a378f8a2072a3afd1b65 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 13 Apr 2026 17:53:40 -0300 Subject: [PATCH] chore: stop transpiling webhook integration scripts with Babel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: Webhook integration scripts are no longer transpiled with @babel/core + @babel/preset-env before being stored. Scripts now run as-is inside isolated-vm (modern V8). This means class method bodies are in strict mode per the ES2015 spec. Scripts that relied on sloppy-mode behaviors provided by the previous Babel transpilation must be updated: - Implicit globals: `msg = x` → `const msg = x` - this in nested functions: `function f() { this.JSON }` → use arrow function or pass dependency explicitly - arguments.callee → use named function expression - Octal literals: 0777 → 0o777 - Duplicate parameter names: function(a, a) → rename Changes: - New validateScriptSyntax helper using Node's built-in vm.Script for syntax validation without transpilation. - Drop @babel/core and @babel/preset-env from apps/meteor dependencies. @babel/runtime stays (used by Meteor-compiled packages). - Fix e2e test that used implicit global (msg → const msg). --- .changeset/integration-scripts-no-babel.md | 13 ++++ .../server/lib/validateOutgoingIntegration.ts | 24 ++----- .../server/lib/validateScriptSyntax.ts | 37 +++++++++++ .../incoming/addIncomingIntegration.ts | 27 +++----- .../incoming/updateIncomingIntegration.ts | 66 ++++++------------- apps/meteor/package.json | 2 - .../end-to-end/api/incoming-integrations.ts | 2 +- 7 files changed, 88 insertions(+), 83 deletions(-) create mode 100644 .changeset/integration-scripts-no-babel.md create mode 100644 apps/meteor/app/integrations/server/lib/validateScriptSyntax.ts diff --git a/.changeset/integration-scripts-no-babel.md b/.changeset/integration-scripts-no-babel.md new file mode 100644 index 0000000000000..0e962b99bd059 --- /dev/null +++ b/.changeset/integration-scripts-no-babel.md @@ -0,0 +1,13 @@ +--- +'@rocket.chat/meteor': major +--- + +**Breaking:** Stopped transpiling webhook integration scripts with Babel. Scripts now run as-is inside `isolated-vm` (modern V8). + +Class method bodies are now in strict mode per the ES2015 spec. Scripts that relied on sloppy-mode behaviors provided by the previous Babel transpilation must be updated: + +- **Implicit globals** — `msg = buildMessage(...)` inside a class method now throws `ReferenceError`. Add `let`, `const`, or `var`. +- **`this` in nested regular functions** — `function helper() { this.JSON.stringify(...) }` now has `this === undefined` instead of `globalThis`. Use arrow functions or pass the dependency explicitly. +- **`arguments.callee`** — Throws `TypeError`. Use a named function expression instead. +- **Octal literals** — `0777` is now a `SyntaxError`. Use `0o777`. +- **Duplicate parameter names** — `function(a, a) {}` is now a `SyntaxError`. diff --git a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts index d4b1a68af5796..ff4a3674a5a57 100644 --- a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts @@ -1,12 +1,10 @@ -import { transformSync } from '@babel/core'; -import presetEnv from '@babel/preset-env'; import type { IUser, INewOutgoingIntegration, IOutgoingIntegration, IUpdateOutgoingIntegration } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; -import { pick } from '@rocket.chat/tools'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { isScriptEngineFrozen } from './validateScriptEngine'; +import { validateScriptSyntax } from './validateScriptSyntax'; import { parseCSV } from '../../../../lib/utils/parseCSV'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { outgoingEvents } from '../../lib/outgoingEvents'; @@ -179,21 +177,11 @@ export const validateOutgoingIntegration = async function ( integration.script && integration.script.trim() !== '' ) { - try { - const result = transformSync(integration.script, { - presets: [presetEnv], - compact: true, - minified: true, - comments: false, - }); - - // TODO: Webhook Integration Editor should inform the user if the script is compiled successfully - integrationData.scriptCompiled = result?.code ?? undefined; - integrationData.scriptError = undefined; - } catch (e) { - integrationData.scriptCompiled = undefined; - integrationData.scriptError = e instanceof Error ? pick(e, 'name', 'message', 'stack') : undefined; - } + // isolated-vm embeds modern V8 and runs the script natively, so no + // transpilation is needed. Syntax is still validated at save time. + const { script, error } = validateScriptSyntax(integration.script); + integrationData.scriptCompiled = script; + integrationData.scriptError = error; } if (typeof integration.runOnEdits !== 'undefined') { diff --git a/apps/meteor/app/integrations/server/lib/validateScriptSyntax.ts b/apps/meteor/app/integrations/server/lib/validateScriptSyntax.ts new file mode 100644 index 0000000000000..5953b4a523e11 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/validateScriptSyntax.ts @@ -0,0 +1,37 @@ +import vm from 'node:vm'; + +/** + * Validate the syntax of a user-supplied integration script and return it + * as-is for storage in `scriptCompiled`. + * + * Integration scripts run inside `isolated-vm`, which embeds modern V8 and + * handles ES2023+ natively. Transpilation via Babel is no longer performed. + * + * ⚠️ **Breaking change (9.0.0):** Class method bodies are now in strict + * mode per the ES2015 spec. Scripts that relied on sloppy-mode behaviors + * (e.g. implicit globals, `arguments.callee`, `this === globalThis` inside + * regular nested functions) must be updated. See the migration guide in the + * PR description. + * + * Returns `{ script }` on success or `{ error }` when the input has a + * syntax error. `error` has the same `{ name, message, stack }` shape the + * previous flow persisted in `scriptError`. + */ +export function validateScriptSyntax( + script: string, +): { script: string; error?: undefined } | { script?: undefined; error: Pick } { + try { + // Wrap so top-level return/declarations parse the same way as in + // getCompatibilityScript at runtime. vm.Script only parses — it does + // not execute the code here. + // eslint-disable-next-line no-new + new vm.Script(`(function(){${script}})`); + return { script }; + } catch (e) { + if (e instanceof SyntaxError) { + const { name, message, stack } = e; + return { error: { name, message, stack } }; + } + throw e; + } +} diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 3a7a48835a427..e32d6fffec51e 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -1,5 +1,3 @@ -import { transformSync } from '@babel/core'; -import presetEnv from '@babel/preset-env'; import type { INewIncomingIntegration, IIncomingIntegration } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Integrations, Subscriptions, Users, Rooms } from '@rocket.chat/models'; @@ -7,12 +5,12 @@ import { Random } from '@rocket.chat/random'; import { removeEmpty } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateScriptEngine, isScriptEngineFrozen } from '../../lib/validateScriptEngine'; +import { validateScriptSyntax } from '../../lib/validateScriptSyntax'; const validChannelChars = ['@', '#']; @@ -104,27 +102,22 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn _createdBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }), }; - // Only compile the script if it is enabled and using a sandbox that is not frozen + // Validate the script syntax if it is enabled and using a sandbox that is + // not frozen. isolated-vm embeds modern V8 and runs the script natively, so + // no transpilation is needed. if ( !isScriptEngineFrozen(integrationData.scriptEngine) && integration.scriptEnabled === true && integration.script && integration.script.trim() !== '' ) { - try { - const result = transformSync(integration.script, { - presets: [presetEnv], - compact: true, - minified: true, - comments: false, - }); - - // TODO: Webhook Integration Editor should inform the user if the script is compiled successfully - integrationData.scriptCompiled = result?.code ?? undefined; - delete integrationData.scriptError; - } catch (e) { + const { script, error } = validateScriptSyntax(integration.script); + if (error) { integrationData.scriptCompiled = undefined; - integrationData.scriptError = e instanceof Error ? _.pick(e, 'name', 'message', 'stack') : undefined; + integrationData.scriptError = error; + } else { + integrationData.scriptCompiled = script; + delete integrationData.scriptError; } } diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index 5ce2ffd4496aa..810365246e70c 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -1,5 +1,3 @@ -import { transformSync } from '@babel/core'; -import presetEnv from '@babel/preset-env'; import type { IIntegration, INewIncomingIntegration, IUpdateIncomingIntegration } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Integrations, Subscriptions, Users, Rooms } from '@rocket.chat/models'; @@ -10,6 +8,7 @@ import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles' import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; +import { validateScriptSyntax } from '../../lib/validateScriptSyntax'; const validChannelChars = ['@', '#']; @@ -84,49 +83,26 @@ export const updateIncomingIntegration = async ( const isFrozen = isScriptEngineFrozen(scriptEngine); - if (!isFrozen) { - let scriptCompiled: string | undefined; - let scriptError: Pick | undefined; - - if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { - try { - const result = transformSync(integration.script, { - presets: [presetEnv], - compact: true, - minified: true, - comments: false, - }); - - // TODO: Webhook Integration Editor should inform the user if the script is compiled successfully - scriptCompiled = result?.code ?? undefined; - scriptError = undefined; - await Integrations.updateOne( - { _id: integrationId }, - { - $set: { - scriptCompiled, - }, - $unset: { scriptError: 1 as const }, - }, - ); - } catch (e) { - scriptCompiled = undefined; - if (e instanceof Error) { - const { name, message, stack } = e; - scriptError = { name, message, stack }; - } - await Integrations.updateOne( - { _id: integrationId }, - { - $set: { - scriptError, - }, - $unset: { - scriptCompiled: 1 as const, - }, - }, - ); - } + if (!isFrozen && integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + // isolated-vm embeds modern V8 and runs the script natively, so no + // transpilation is needed. Syntax is still validated at save time. + const { script, error } = validateScriptSyntax(integration.script); + if (error) { + await Integrations.updateOne( + { _id: integrationId }, + { + $set: { scriptError: error }, + $unset: { scriptCompiled: 1 as const }, + }, + ); + } else { + await Integrations.updateOne( + { _id: integrationId }, + { + $set: { scriptCompiled: script }, + $unset: { scriptError: 1 as const }, + }, + ); } } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 02a3c6499d1a9..aee9a011c0208 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -68,8 +68,6 @@ "@aws-sdk/client-s3": "^3.862.0", "@aws-sdk/lib-storage": "^3.862.0", "@aws-sdk/s3-request-presigner": "^3.862.0", - "@babel/core": "~7.28.6", - "@babel/preset-env": "~7.28.6", "@babel/runtime": "~7.28.6", "@bugsnag/js": "~7.20.2", "@bugsnag/plugin-react": "~7.19.0", diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index 238b3fd543c9b..ecf57448a1927 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -422,7 +422,7 @@ describe('[Incoming Integrations]', () => { ' \n' + ' class Script {\n' + ' process_incoming_request({ request }) {\n' + - ' msg = buildMessage(request.content);\n' + + ' const msg = buildMessage(request.content);\n' + ' \n' + ' return {\n' + ' content:{\n' +