From c217518c2c2726c0287e67a4490acfa66660acff Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 14 Apr 2026 18:48:37 -0300 Subject: [PATCH 1/5] chore: add per-integration scriptTranspile flag to opt-out of Babel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integration scripts are transpiled with @babel/core + @babel/preset-env before being stored. In 9.0.0, this transpilation will be removed entirely (scripts run as-is in isolated-vm's modern V8). This commit adds a per-integration `scriptTranspile` flag (default `true`) so admins can opt-out one integration at a time, test their scripts for strict-mode compatibility, and migrate gradually before upgrading to 9.0.0. Changes: - New apps/meteor/app/integrations/server/lib/compileIntegrationScript helper: takes `{ transpile: boolean }` and either runs Babel (legacy) or validates syntax with Node's built-in vm.Script (9.0.0 behavior). - Add `scriptTranspile?: boolean` to IIncomingIntegration and IOutgoingIntegration. Field is documented as deprecated. - Extend rest-typings Create and Update schemas to accept the flag. - Add/Update methods (incoming + outgoing) now pass `integration.scriptTranspile !== false` to the compiler and persist the flag on the document. Known strict-mode breaking patterns (documented in 9.0.0 changeset) when `scriptTranspile: false`: - Implicit globals: msg = x → const msg = x - this in nested functions: this === undefined instead of globalThis - arguments.callee: TypeError - Octal literals: 0777 → 0o777 - Duplicate parameter names --- .../integration-script-transpile-opt-out.md | 7 ++ .../server/lib/compileIntegrationScript.ts | 71 +++++++++++++++++++ .../server/lib/validateOutgoingIntegration.ts | 28 +++----- .../incoming/addIncomingIntegration.ts | 29 ++++---- .../incoming/updateIncomingIntegration.ts | 69 +++++++----------- .../outgoing/updateOutgoingIntegration.ts | 1 + packages/core-typings/src/IIntegration.ts | 14 ++++ .../integrations/IntegrationsCreateProps.ts | 10 +++ .../integrations/IntegrationsUpdateProps.ts | 10 +++ 9 files changed, 158 insertions(+), 81 deletions(-) create mode 100644 .changeset/integration-script-transpile-opt-out.md create mode 100644 apps/meteor/app/integrations/server/lib/compileIntegrationScript.ts diff --git a/.changeset/integration-script-transpile-opt-out.md b/.changeset/integration-script-transpile-opt-out.md new file mode 100644 index 0000000000000..32380fdf70bac --- /dev/null +++ b/.changeset/integration-script-transpile-opt-out.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/rest-typings': patch +--- + +Added a `scriptTranspile` flag (default `true`) to webhook integrations. When set to `false`, the integration script is stored as-is without Babel transpilation — matching the 9.0.0 default where Babel is removed entirely. Admins can flip the flag per-integration to validate strict-mode compatibility before upgrading. The field is deprecated and will be removed in 9.0.0. diff --git a/apps/meteor/app/integrations/server/lib/compileIntegrationScript.ts b/apps/meteor/app/integrations/server/lib/compileIntegrationScript.ts new file mode 100644 index 0000000000000..5d0e250afe011 --- /dev/null +++ b/apps/meteor/app/integrations/server/lib/compileIntegrationScript.ts @@ -0,0 +1,71 @@ +import vm from 'node:vm'; + +import { transformSync } from '@babel/core'; +import presetEnv from '@babel/preset-env'; + +/** + * Compile or validate a user-supplied integration script for storage in + * `scriptCompiled`. + * + * When `transpile` is `true` (the default, controlled by each integration's + * `scriptTranspile` flag), the script is transpiled with `@babel/core + + * @babel/preset-env` — the historical behavior. When `false`, the script is + * validated with Node's built-in `vm.Script` and stored as-is, matching the + * 9.0.0 default where Babel transpilation is removed entirely. + * + * Integration scripts run inside `isolated-vm`, which embeds modern V8 and + * handles ES2023+ natively. The transpilation only exists to preserve the + * sloppy-mode semantics (implicit globals in class methods, `this` in nested + * functions, etc.) that early scripts relied on. Admins can flip + * `scriptTranspile: false` per integration to test strict-mode compatibility + * before the 9.0.0 upgrade. + * + * Returns `{ script }` on success or `{ error }` with the same + * `{ name, message, stack }` shape persisted in `scriptError`. + */ +export function compileIntegrationScript( + script: string, + { transpile }: { transpile: boolean }, +): { script: string; error?: undefined } | { script?: undefined; error: Pick } { + if (!transpile) { + return validateOnly(script); + } + + return transpileWithBabel(script); +} + +function validateOnly( + script: string, +): { script: string; error?: undefined } | { script?: undefined; error: Pick } { + try { + 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; + } +} + +function transpileWithBabel( + script: string, +): { script: string; error?: undefined } | { script?: undefined; error: Pick } { + try { + const result = transformSync(script, { + presets: [presetEnv], + compact: true, + minified: true, + comments: false, + }); + + return { script: result?.code ?? script }; + } catch (e) { + if (e instanceof Error) { + const { name, message, stack } = e; + return { error: { name, message, stack } }; + } + throw e; + } +} diff --git a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts index d4b1a68af5796..24679896cc3ec 100644 --- a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts @@ -1,11 +1,9 @@ -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 { compileIntegrationScript } from './compileIntegrationScript'; import { isScriptEngineFrozen } from './validateScriptEngine'; import { parseCSV } from '../../../../lib/utils/parseCSV'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -172,28 +170,20 @@ export const validateOutgoingIntegration = async function ( delete integrationData.triggerWords; } - // Only compile the script if it is enabled and using a sandbox that is not frozen + // Default to transpiling with Babel for backwards compatibility; integrations + // can opt-out per-record by setting `scriptTranspile: false` (removed in 9.0.0). + const scriptTranspile = integration.scriptTranspile !== false; + integrationData.scriptTranspile = scriptTranspile; + 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; - integrationData.scriptError = undefined; - } catch (e) { - integrationData.scriptCompiled = undefined; - integrationData.scriptError = e instanceof Error ? pick(e, 'name', 'message', 'stack') : undefined; - } + const { script, error } = compileIntegrationScript(integration.script, { transpile: scriptTranspile }); + integrationData.scriptCompiled = script; + integrationData.scriptError = error; } if (typeof integration.runOnEdits !== 'undefined') { diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 3a7a48835a427..c15ffb8529605 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,11 +5,11 @@ 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 { compileIntegrationScript } from '../../lib/compileIntegrationScript'; import { validateScriptEngine, isScriptEngineFrozen } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; @@ -92,9 +90,14 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn }); } + // Default to transpiling with Babel for backwards compatibility; integrations + // can opt-out per-record by setting `scriptTranspile: false` (removed in 9.0.0). + const scriptTranspile = integration.scriptTranspile !== false; + const integrationData: IIncomingIntegration = { ...integration, scriptEngine: integration.scriptEngine ?? 'isolated-vm', + scriptTranspile, type: 'webhook-incoming', channel: channels, overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled ?? false, @@ -104,27 +107,19 @@ 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 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 } = compileIntegrationScript(integration.script, { transpile: scriptTranspile }); + 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..d4baed95ebc2b 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'; @@ -9,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'; import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; +import { compileIntegrationScript } from '../../lib/compileIntegrationScript'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; @@ -84,49 +83,28 @@ 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, - }, - }, - ); - } + // Default to transpiling with Babel for backwards compatibility; integrations + // can opt-out per-record by setting `scriptTranspile: false` (removed in 9.0.0). + const scriptTranspile = integration.scriptTranspile !== false; + + if (!isFrozen && integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + const { script, error } = compileIntegrationScript(integration.script, { transpile: scriptTranspile }); + if (error) { + await Integrations.updateOne( + { _id: integrationId }, + { + $set: { scriptError: error, scriptTranspile }, + $unset: { scriptCompiled: 1 as const }, + }, + ); + } else { + await Integrations.updateOne( + { _id: integrationId }, + { + $set: { scriptCompiled: script, scriptTranspile }, + $unset: { scriptError: 1 as const }, + }, + ); } } @@ -192,6 +170,7 @@ export const updateIncomingIntegration = async ( script: integration.script, scriptEnabled: integration.scriptEnabled, scriptEngine, + scriptTranspile, }), ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index 29b150a60011a..804183eb8b6c6 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -87,6 +87,7 @@ export const updateOutgoingIntegration = async ( script: integration.script, scriptEnabled: integration.scriptEnabled, scriptEngine, + scriptTranspile: integration.scriptTranspile, ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), }), triggerWords: integration.triggerWords, diff --git a/packages/core-typings/src/IIntegration.ts b/packages/core-typings/src/IIntegration.ts index 22fa520818757..1df4ba083da28 100644 --- a/packages/core-typings/src/IIntegration.ts +++ b/packages/core-typings/src/IIntegration.ts @@ -16,6 +16,13 @@ export interface IIncomingIntegration extends IRocketChatRecord { script: string; scriptCompiled?: string; scriptError?: Pick; + /** + * Whether to transpile the script with Babel before storing it in + * `scriptCompiled`. Defaults to `true`. Set to `false` to run the script + * as-is inside `isolated-vm` (the 9.0.0 default). Deprecated field — + * removed in 9.0.0 together with the Babel transpilation path. + */ + scriptTranspile?: boolean; name: string; enabled: boolean; @@ -56,6 +63,13 @@ export interface IOutgoingIntegration extends IRocketChatRecord { script: string; scriptCompiled?: string; scriptError?: Pick; + /** + * Whether to transpile the script with Babel before storing it in + * `scriptCompiled`. Defaults to `true`. Set to `false` to run the script + * as-is inside `isolated-vm` (the 9.0.0 default). Deprecated field — + * removed in 9.0.0 together with the Babel transpilation path. + */ + scriptTranspile?: boolean; runOnEdits?: boolean; retryFailedCalls?: boolean; diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts index 89d388191c26d..74ed8a156d0f9 100644 --- a/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts +++ b/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts @@ -9,6 +9,7 @@ export type IntegrationsCreateProps = channel: string; overrideDestinationChannelEnabled?: boolean; scriptEnabled: boolean; + scriptTranspile?: boolean; script?: string; name: string; enabled: boolean; @@ -30,6 +31,7 @@ export type IntegrationsCreateProps = token?: string; scriptEnabled: boolean; + scriptTranspile?: boolean; script?: string; runOnEdits?: boolean; @@ -69,6 +71,10 @@ const integrationsCreateSchema = { type: 'boolean', nullable: false, }, + scriptTranspile: { + type: 'boolean', + nullable: true, + }, overrideDestinationChannelEnabled: { type: 'boolean', nullable: true, @@ -157,6 +163,10 @@ const integrationsCreateSchema = { type: 'boolean', nullable: false, }, + scriptTranspile: { + type: 'boolean', + nullable: true, + }, script: { type: 'string', nullable: true, diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts index 6a2f5a4c5b657..bc9ba8120e5ea 100644 --- a/packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts +++ b/packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts @@ -8,6 +8,7 @@ export type IntegrationsUpdateProps = integrationId: string; channel: string; scriptEnabled: boolean; + scriptTranspile?: boolean; scriptEngine: 'isolated-vm'; overrideDestinationChannelEnabled?: boolean; script?: string; @@ -32,6 +33,7 @@ export type IntegrationsUpdateProps = token?: string; scriptEnabled: boolean; + scriptTranspile?: boolean; scriptEngine: 'isolated-vm'; script?: string; runOnEdits?: boolean; @@ -71,6 +73,10 @@ const integrationsUpdateSchema = { type: 'boolean', nullable: false, }, + scriptTranspile: { + type: 'boolean', + nullable: true, + }, scriptEngine: { type: 'string', nullable: false, @@ -167,6 +173,10 @@ const integrationsUpdateSchema = { type: 'boolean', nullable: false, }, + scriptTranspile: { + type: 'boolean', + nullable: true, + }, scriptEngine: { type: 'string', nullable: false, From 5839d64045fe6dd59d70b97adb59b621a544db1c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 17 Apr 2026 14:38:02 -0300 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Guilherme Gazzo --- .changeset/integration-script-transpile-opt-out.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/integration-script-transpile-opt-out.md b/.changeset/integration-script-transpile-opt-out.md index 32380fdf70bac..aec2c773b9d7b 100644 --- a/.changeset/integration-script-transpile-opt-out.md +++ b/.changeset/integration-script-transpile-opt-out.md @@ -1,7 +1,7 @@ --- -'@rocket.chat/meteor': patch -'@rocket.chat/core-typings': patch -'@rocket.chat/rest-typings': patch +'@rocket.chat/meteor': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor --- -Added a `scriptTranspile` flag (default `true`) to webhook integrations. When set to `false`, the integration script is stored as-is without Babel transpilation — matching the 9.0.0 default where Babel is removed entirely. Admins can flip the flag per-integration to validate strict-mode compatibility before upgrading. The field is deprecated and will be removed in 9.0.0. +Adds a `scriptTranspile` flag (default `true`) to webhook integrations. When set to `false`, the integration script is stored as-is without Babel transpilation — matching the 9.0.0 default where Babel is removed entirely. Admins can flip the flag per-integration to validate strict-mode compatibility before upgrading. The field is deprecated and will be removed in 9.0.0. From a5063a02b6ba729c47f4c79abb1bc92cef12e58e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 17 Apr 2026 18:05:17 -0300 Subject: [PATCH 3/5] refactor: rename scriptTranspile to skipTranspile and add sloppy-mode test Rename the flag from `scriptTranspile` (default true) to `skipTranspile` (default false) for clearer intent. Add e2e test that verifies a sloppy-mode script (implicit global assignment) fails at webhook execution time when `skipTranspile: true`. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../integration-script-transpile-opt-out.md | 2 +- .../server/lib/compileIntegrationScript.ts | 4 +- .../server/lib/validateOutgoingIntegration.ts | 8 +-- .../incoming/addIncomingIntegration.ts | 8 +-- .../incoming/updateIncomingIntegration.ts | 12 ++-- .../outgoing/updateOutgoingIntegration.ts | 2 +- .../end-to-end/api/incoming-integrations.ts | 62 +++++++++++++++++++ packages/core-typings/src/IIntegration.ts | 22 ++++--- .../integrations/IntegrationsCreateProps.ts | 8 +-- .../integrations/IntegrationsUpdateProps.ts | 8 +-- 10 files changed, 100 insertions(+), 36 deletions(-) diff --git a/.changeset/integration-script-transpile-opt-out.md b/.changeset/integration-script-transpile-opt-out.md index aec2c773b9d7b..3e6451943d03f 100644 --- a/.changeset/integration-script-transpile-opt-out.md +++ b/.changeset/integration-script-transpile-opt-out.md @@ -4,4 +4,4 @@ '@rocket.chat/rest-typings': minor --- -Adds a `scriptTranspile` flag (default `true`) to webhook integrations. When set to `false`, the integration script is stored as-is without Babel transpilation — matching the 9.0.0 default where Babel is removed entirely. Admins can flip the flag per-integration to validate strict-mode compatibility before upgrading. The field is deprecated and will be removed in 9.0.0. +Adds a `skipTranspile` flag (default `false`) to webhook integrations. When set to `true`, the integration script is stored as-is without Babel transpilation — matching the 9.0.0 default where Babel is removed entirely. Admins can flip the flag per-integration to validate strict-mode compatibility before upgrading. The field is deprecated and will be removed in 9.0.0. diff --git a/apps/meteor/app/integrations/server/lib/compileIntegrationScript.ts b/apps/meteor/app/integrations/server/lib/compileIntegrationScript.ts index 5d0e250afe011..5e366cd1e18ea 100644 --- a/apps/meteor/app/integrations/server/lib/compileIntegrationScript.ts +++ b/apps/meteor/app/integrations/server/lib/compileIntegrationScript.ts @@ -8,7 +8,7 @@ import presetEnv from '@babel/preset-env'; * `scriptCompiled`. * * When `transpile` is `true` (the default, controlled by each integration's - * `scriptTranspile` flag), the script is transpiled with `@babel/core + + * `skipTranspile` flag), the script is transpiled with `@babel/core + * @babel/preset-env` — the historical behavior. When `false`, the script is * validated with Node's built-in `vm.Script` and stored as-is, matching the * 9.0.0 default where Babel transpilation is removed entirely. @@ -17,7 +17,7 @@ import presetEnv from '@babel/preset-env'; * handles ES2023+ natively. The transpilation only exists to preserve the * sloppy-mode semantics (implicit globals in class methods, `this` in nested * functions, etc.) that early scripts relied on. Admins can flip - * `scriptTranspile: false` per integration to test strict-mode compatibility + * `skipTranspile: true` per integration to test strict-mode compatibility * before the 9.0.0 upgrade. * * Returns `{ script }` on success or `{ error }` with the same diff --git a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts index 24679896cc3ec..b4656f629d834 100644 --- a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts @@ -171,9 +171,9 @@ export const validateOutgoingIntegration = async function ( } // Default to transpiling with Babel for backwards compatibility; integrations - // can opt-out per-record by setting `scriptTranspile: false` (removed in 9.0.0). - const scriptTranspile = integration.scriptTranspile !== false; - integrationData.scriptTranspile = scriptTranspile; + // can opt-out per-record by setting `skipTranspile: true` (removed in 9.0.0). + const skipTranspile = integration.skipTranspile === true; + integrationData.skipTranspile = skipTranspile; if ( !isScriptEngineFrozen(integrationData.scriptEngine) && @@ -181,7 +181,7 @@ export const validateOutgoingIntegration = async function ( integration.script && integration.script.trim() !== '' ) { - const { script, error } = compileIntegrationScript(integration.script, { transpile: scriptTranspile }); + const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile }); integrationData.scriptCompiled = script; integrationData.scriptError = error; } diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index fe2e83d8a9427..49116d1335ee8 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -91,13 +91,13 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn } // Default to transpiling with Babel for backwards compatibility; integrations - // can opt-out per-record by setting `scriptTranspile: false` (removed in 9.0.0). - const scriptTranspile = integration.scriptTranspile !== false; + // can opt-out per-record by setting `skipTranspile: true` (removed in 9.0.0). + const skipTranspile = integration.skipTranspile === true; const integrationData: IIncomingIntegration = { ...integration, scriptEngine: integration.scriptEngine ?? 'isolated-vm', - scriptTranspile, + skipTranspile, type: 'webhook-incoming', channel: channels, overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled ?? false, @@ -113,7 +113,7 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn integration.script && integration.script.trim() !== '' ) { - const { script, error } = compileIntegrationScript(integration.script, { transpile: scriptTranspile }); + const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile }); if (error) { integrationData.scriptCompiled = undefined; integrationData.scriptError = error; diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index 0bbc386205706..894b5b31bd405 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -84,16 +84,16 @@ export const updateIncomingIntegration = async ( const isFrozen = isScriptEngineFrozen(scriptEngine); // Default to transpiling with Babel for backwards compatibility; integrations - // can opt-out per-record by setting `scriptTranspile: false` (removed in 9.0.0). - const scriptTranspile = integration.scriptTranspile !== false; + // can opt-out per-record by setting `skipTranspile: true` (removed in 9.0.0). + const skipTranspile = integration.skipTranspile === true; if (!isFrozen && integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { - const { script, error } = compileIntegrationScript(integration.script, { transpile: scriptTranspile }); + const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile }); if (error) { await Integrations.updateOne( { _id: integrationId }, { - $set: { scriptError: error, scriptTranspile }, + $set: { scriptError: error, skipTranspile }, $unset: { scriptCompiled: 1 as const }, }, ); @@ -101,7 +101,7 @@ export const updateIncomingIntegration = async ( await Integrations.updateOne( { _id: integrationId }, { - $set: { scriptCompiled: script, scriptTranspile }, + $set: { scriptCompiled: script, skipTranspile }, $unset: { scriptError: 1 as const }, }, ); @@ -170,7 +170,7 @@ export const updateIncomingIntegration = async ( ...(typeof integration.script !== 'undefined' && { script: integration.script }), scriptEnabled: integration.scriptEnabled, ...(scriptEngine && { scriptEngine }), - scriptTranspile, + skipTranspile, }), ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index 804183eb8b6c6..aee22f252efab 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -87,7 +87,7 @@ export const updateOutgoingIntegration = async ( script: integration.script, scriptEnabled: integration.scriptEnabled, scriptEngine, - scriptTranspile: integration.scriptTranspile, + skipTranspile: integration.skipTranspile, ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), }), triggerWords: integration.triggerWords, 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..b5fea2220298f 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -488,6 +488,68 @@ describe('[Incoming Integrations]', () => { }); }); + describe('skipTranspile flag', () => { + let withoutTranspile: IIntegration; + + before(async () => { + await updatePermission('manage-incoming-integrations', ['admin']); + + // This script uses an implicit global assignment (`msg = buildMessage(...)`) + // which works when Babel transpiles classes to functions (sloppy mode) but + // fails in native ES6 class methods (strict mode) when transpilation is off. + const res = await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test without transpile (sloppy-mode script)', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: true, + skipTranspile: true, + overrideDestinationChannelEnabled: false, + channel: '#general', + script: + 'const buildMessage = (obj) => {\n' + + ' const template = `[#VALUE](${ obj.test })`;\n' + + ' return { text: template };\n' + + '};\n' + + '\n' + + 'class Script {\n' + + ' process_incoming_request({ request }) {\n' + + ' msg = buildMessage(request.content);\n' + + ' return { content: { text: msg.text } };\n' + + ' }\n' + + '}\n', + }) + .expect(200); + withoutTranspile = res.body.integration; + }); + + after(async () => { + if (withoutTranspile) { + await removeIntegration(withoutTranspile._id, 'incoming'); + } + }); + + it('should create the integration with scriptCompiled and skipTranspile true', () => { + expect(withoutTranspile).to.have.property('scriptCompiled'); + expect(withoutTranspile).to.not.have.property('scriptError'); + expect(withoutTranspile).to.have.property('skipTranspile', true); + }); + + it('should fail to execute a sloppy-mode script when skipTranspile is true', async () => { + const payload = { test: 'test' }; + + await request + .post(`/hooks/${withoutTranspile._id}/${withoutTranspile.token}`) + .set('Content-Type', 'application/json') + .send(JSON.stringify(payload)) + .expect(500); + }); + }); + describe('With manage-own-incoming-integrations permission', () => { let integrationId: string; diff --git a/packages/core-typings/src/IIntegration.ts b/packages/core-typings/src/IIntegration.ts index 971f8e7ba8d83..a0b6e93b4c313 100644 --- a/packages/core-typings/src/IIntegration.ts +++ b/packages/core-typings/src/IIntegration.ts @@ -17,12 +17,13 @@ export interface IIncomingIntegration extends IRocketChatRecord { scriptCompiled?: string; scriptError?: Pick; /** - * Whether to transpile the script with Babel before storing it in - * `scriptCompiled`. Defaults to `true`. Set to `false` to run the script - * as-is inside `isolated-vm` (the 9.0.0 default). Deprecated field — - * removed in 9.0.0 together with the Babel transpilation path. + * When `true`, the integration script is stored as-is without Babel + * transpilation — matching the 9.0.0 default where Babel is removed + * entirely. Defaults to `false` (transpile with Babel for backwards + * compatibility). Deprecated field — removed in 9.0.0 together with + * the Babel transpilation path. */ - scriptTranspile?: boolean; + skipTranspile?: boolean; name: string; enabled: boolean; @@ -64,12 +65,13 @@ export interface IOutgoingIntegration extends IRocketChatRecord { scriptCompiled?: string; scriptError?: Pick; /** - * Whether to transpile the script with Babel before storing it in - * `scriptCompiled`. Defaults to `true`. Set to `false` to run the script - * as-is inside `isolated-vm` (the 9.0.0 default). Deprecated field — - * removed in 9.0.0 together with the Babel transpilation path. + * When `true`, the integration script is stored as-is without Babel + * transpilation — matching the 9.0.0 default where Babel is removed + * entirely. Defaults to `false` (transpile with Babel for backwards + * compatibility). Deprecated field — removed in 9.0.0 together with + * the Babel transpilation path. */ - scriptTranspile?: boolean; + skipTranspile?: boolean; runOnEdits?: boolean; retryFailedCalls?: boolean; diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts index 74ed8a156d0f9..ac06a647bee05 100644 --- a/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts +++ b/packages/rest-typings/src/v1/integrations/IntegrationsCreateProps.ts @@ -9,7 +9,7 @@ export type IntegrationsCreateProps = channel: string; overrideDestinationChannelEnabled?: boolean; scriptEnabled: boolean; - scriptTranspile?: boolean; + skipTranspile?: boolean; script?: string; name: string; enabled: boolean; @@ -31,7 +31,7 @@ export type IntegrationsCreateProps = token?: string; scriptEnabled: boolean; - scriptTranspile?: boolean; + skipTranspile?: boolean; script?: string; runOnEdits?: boolean; @@ -71,7 +71,7 @@ const integrationsCreateSchema = { type: 'boolean', nullable: false, }, - scriptTranspile: { + skipTranspile: { type: 'boolean', nullable: true, }, @@ -163,7 +163,7 @@ const integrationsCreateSchema = { type: 'boolean', nullable: false, }, - scriptTranspile: { + skipTranspile: { type: 'boolean', nullable: true, }, diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts index bc9ba8120e5ea..995290da50d7c 100644 --- a/packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts +++ b/packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts @@ -8,7 +8,7 @@ export type IntegrationsUpdateProps = integrationId: string; channel: string; scriptEnabled: boolean; - scriptTranspile?: boolean; + skipTranspile?: boolean; scriptEngine: 'isolated-vm'; overrideDestinationChannelEnabled?: boolean; script?: string; @@ -33,7 +33,7 @@ export type IntegrationsUpdateProps = token?: string; scriptEnabled: boolean; - scriptTranspile?: boolean; + skipTranspile?: boolean; scriptEngine: 'isolated-vm'; script?: string; runOnEdits?: boolean; @@ -73,7 +73,7 @@ const integrationsUpdateSchema = { type: 'boolean', nullable: false, }, - scriptTranspile: { + skipTranspile: { type: 'boolean', nullable: true, }, @@ -173,7 +173,7 @@ const integrationsUpdateSchema = { type: 'boolean', nullable: false, }, - scriptTranspile: { + skipTranspile: { type: 'boolean', nullable: true, }, From cf26d0dfd900e8b34cbcbbab9aeadabb437d32aa Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 17 Apr 2026 18:06:51 -0300 Subject: [PATCH 4/5] refactor: reuse same script for skipTranspile test Extract sloppyModeScript constant and reuse it for both the existing transpiled test and the new skipTranspile: true test, making it clear that the only difference is the flag. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../end-to-end/api/incoming-integrations.ts | 131 ++++++++---------- 1 file changed, 56 insertions(+), 75 deletions(-) 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 b5fea2220298f..a2b3245e2b907 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -367,6 +367,30 @@ describe('[Incoming Integrations]', () => { describe('Script integration tests', () => { let withScript: IIntegration; let withScriptDefaultContentType: IIntegration; + let withSkipTranspile: IIntegration; + + const sloppyModeScript = + 'const buildMessage = (obj) => {\n' + + ' \n' + + ' const template = `[#VALUE](${ obj.test })`;\n' + + ' \n' + + ' return {\n' + + ' text: template\n' + + ' };\n' + + ' };\n' + + ' \n' + + ' class Script {\n' + + ' process_incoming_request({ request }) {\n' + + ' msg = buildMessage(request.content);\n' + + ' \n' + + ' return {\n' + + ' content:{\n' + + ' text: msg.text\n' + + ' }\n' + + ' };\n' + + ' }\n' + + ' }\n' + + ' \n'; before(async () => { await updatePermission('manage-incoming-integrations', ['admin']); @@ -410,35 +434,38 @@ describe('[Incoming Integrations]', () => { scriptEnabled: true, overrideDestinationChannelEnabled: false, channel: '#general', - script: - 'const buildMessage = (obj) => {\n' + - ' \n' + - ' const template = `[#VALUE](${ obj.test })`;\n' + - ' \n' + - ' return {\n' + - ' text: template\n' + - ' };\n' + - ' };\n' + - ' \n' + - ' class Script {\n' + - ' process_incoming_request({ request }) {\n' + - ' msg = buildMessage(request.content);\n' + - ' \n' + - ' return {\n' + - ' content:{\n' + - ' text: msg.text\n' + - ' }\n' + - ' };\n' + - ' }\n' + - ' }\n' + - ' \n', + script: sloppyModeScript, }) .expect(200); withScriptDefaultContentType = res2.body.integration; + + // Same script but with skipTranspile: true — no Babel, class methods + // run in strict mode so `msg = buildMessage(...)` throws ReferenceError. + const res3 = await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test with skipTranspile', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: true, + skipTranspile: true, + overrideDestinationChannelEnabled: false, + channel: '#general', + script: sloppyModeScript, + }) + .expect(200); + withSkipTranspile = res3.body.integration; }); after(async () => { - await Promise.all([removeIntegration(withScript._id, 'incoming'), removeIntegration(withScriptDefaultContentType._id, 'incoming')]); + await Promise.all([ + removeIntegration(withScript._id, 'incoming'), + removeIntegration(withScriptDefaultContentType._id, 'incoming'), + removeIntegration(withSkipTranspile._id, 'incoming'), + ]); }); it('should send a message if the payload is a application/x-www-form-urlencoded JSON AND the integration has a valid script', async () => { @@ -486,64 +513,18 @@ describe('[Incoming Integrations]', () => { expect(messagesResult.body).to.have.property('messages').and.to.be.an('array'); expect(!!(messagesResult.body.messages as IMessage[]).find((m) => m.msg === '[#VALUE](test)')).to.be.true; }); - }); - - describe('skipTranspile flag', () => { - let withoutTranspile: IIntegration; - - before(async () => { - await updatePermission('manage-incoming-integrations', ['admin']); - - // This script uses an implicit global assignment (`msg = buildMessage(...)`) - // which works when Babel transpiles classes to functions (sloppy mode) but - // fails in native ES6 class methods (strict mode) when transpilation is off. - const res = await request - .post(api('integrations.create')) - .set(credentials) - .send({ - type: 'webhook-incoming', - name: 'Incoming test without transpile (sloppy-mode script)', - enabled: true, - alias: 'test', - username: 'rocket.cat', - scriptEnabled: true, - skipTranspile: true, - overrideDestinationChannelEnabled: false, - channel: '#general', - script: - 'const buildMessage = (obj) => {\n' + - ' const template = `[#VALUE](${ obj.test })`;\n' + - ' return { text: template };\n' + - '};\n' + - '\n' + - 'class Script {\n' + - ' process_incoming_request({ request }) {\n' + - ' msg = buildMessage(request.content);\n' + - ' return { content: { text: msg.text } };\n' + - ' }\n' + - '}\n', - }) - .expect(200); - withoutTranspile = res.body.integration; - }); - - after(async () => { - if (withoutTranspile) { - await removeIntegration(withoutTranspile._id, 'incoming'); - } - }); - it('should create the integration with scriptCompiled and skipTranspile true', () => { - expect(withoutTranspile).to.have.property('scriptCompiled'); - expect(withoutTranspile).to.not.have.property('scriptError'); - expect(withoutTranspile).to.have.property('skipTranspile', true); + it('should create the skipTranspile integration with scriptCompiled and no scriptError', () => { + expect(withSkipTranspile).to.have.property('scriptCompiled'); + expect(withSkipTranspile).to.not.have.property('scriptError'); + expect(withSkipTranspile).to.have.property('skipTranspile', true); }); - it('should fail to execute a sloppy-mode script when skipTranspile is true', async () => { + it('should fail to execute the same sloppy-mode script when skipTranspile is true', async () => { const payload = { test: 'test' }; await request - .post(`/hooks/${withoutTranspile._id}/${withoutTranspile.token}`) + .post(`/hooks/${withSkipTranspile._id}/${withSkipTranspile.token}`) .set('Content-Type', 'application/json') .send(JSON.stringify(payload)) .expect(500); From 508ff9779ff5ccaac5f010457e1cd689ce2ceb68 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 17 Apr 2026 19:19:26 -0300 Subject: [PATCH 5/5] fix: expect 400 instead of 500 for skipTranspile webhook failure Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/tests/end-to-end/api/incoming-integrations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a2b3245e2b907..69d609b9b8fb6 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -527,7 +527,7 @@ describe('[Incoming Integrations]', () => { .post(`/hooks/${withSkipTranspile._id}/${withSkipTranspile.token}`) .set('Content-Type', 'application/json') .send(JSON.stringify(payload)) - .expect(500); + .expect(400); }); });