Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/integration-scripts-no-babel.md
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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') {
Expand Down
37 changes: 37 additions & 0 deletions apps/meteor/app/integrations/server/lib/validateScriptSyntax.ts
Original file line number Diff line number Diff line change
@@ -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<Error, 'name' | 'message' | 'stack'> } {
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;
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
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';
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 = ['@', '#'];

Expand Down Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 = ['@', '#'];

Expand Down Expand Up @@ -84,49 +83,26 @@ export const updateIncomingIntegration = async (

const isFrozen = isScriptEngineFrozen(scriptEngine);

if (!isFrozen) {
let scriptCompiled: string | undefined;
let scriptError: Pick<Error, 'name' | 'message' | 'stack'> | 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 },
},
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/utils/rocketchat.info
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "8.4.0-develop"
"version": "9.0.0-develop"
}
4 changes: 1 addition & 3 deletions apps/meteor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rocket.chat/meteor",
"version": "8.4.0-develop",
"version": "9.0.0-develop",
"private": true,
"description": "The Ultimate Open Source WebChat Platform",
"keywords": [
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' +
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rocket.chat",
"version": "8.4.0-develop",
"version": "9.0.0-develop",
"private": true,
"description": "Rocket.Chat Monorepo",
"homepage": "https://github.com/RocketChat/Rocket.Chat#readme",
Expand Down
3 changes: 2 additions & 1 deletion packages/core-typings/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@rocket.chat/core-typings",
"version": "8.4.0-develop",
"version": "9.0.0-develop",
"private": true,
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion packages/rest-typings/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rocket.chat/rest-typings",
"version": "8.4.0-develop",
"version": "9.0.0-develop",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
Expand Down
Loading