Skip to content

Commit fd38bda

Browse files
committed
chore!: stop transpiling webhook integration scripts with Babel (#40142)
1 parent 77a6df6 commit fd38bda

7 files changed

Lines changed: 69 additions & 60 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@rocket.chat/meteor': major
3+
---
4+
5+
**Breaking:** Stopped transpiling webhook integration scripts with Babel. Scripts now run as-is inside `isolated-vm` (modern V8).
6+
7+
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:
8+
9+
- **Implicit globals**`msg = buildMessage(...)` inside a class method now throws `ReferenceError`. Add `let`, `const`, or `var`.
10+
- **`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.
11+
- **`arguments.callee`** — Throws `TypeError`. Use a named function expression instead.
12+
- **Octal literals**`0777` is now a `SyntaxError`. Use `0o777`.
13+
- **Duplicate parameter names**`function(a, a) {}` is now a `SyntaxError`.

apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { Subscriptions, Users, Rooms } from '@rocket.chat/models';
33
import { Match } from 'meteor/check';
44
import { Meteor } from 'meteor/meteor';
55

6-
import { compileIntegrationScript } from './compileIntegrationScript';
76
import { isScriptEngineFrozen } from './validateScriptEngine';
7+
import { validateScriptSyntax } from './validateScriptSyntax';
88
import { parseCSV } from '../../../../lib/utils/parseCSV';
99
import { hasPermissionAsync, hasAllPermissionAsync } from '../../../authorization/server/functions/hasPermission';
1010
import { outgoingEvents } from '../../lib/outgoingEvents';
@@ -181,7 +181,9 @@ export const validateOutgoingIntegration = async function (
181181
integration.script &&
182182
integration.script.trim() !== ''
183183
) {
184-
const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile });
184+
// isolated-vm embeds modern V8 and runs the script natively, so no
185+
// transpilation is needed. Syntax is still validated at save time.
186+
const { script, error } = validateScriptSyntax(integration.script);
185187
integrationData.scriptCompiled = script;
186188
integrationData.scriptError = error;
187189
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import vm from 'node:vm';
2+
3+
/**
4+
* Validate the syntax of a user-supplied integration script and return it
5+
* as-is for storage in `scriptCompiled`.
6+
*
7+
* Integration scripts run inside `isolated-vm`, which embeds modern V8 and
8+
* handles ES2023+ natively. Transpilation via Babel is no longer performed.
9+
*
10+
* ⚠️ **Breaking change (9.0.0):** Class method bodies are now in strict
11+
* mode per the ES2015 spec. Scripts that relied on sloppy-mode behaviors
12+
* (e.g. implicit globals, `arguments.callee`, `this === globalThis` inside
13+
* regular nested functions) must be updated. See the migration guide in the
14+
* PR description.
15+
*
16+
* Returns `{ script }` on success or `{ error }` when the input has a
17+
* syntax error. `error` has the same `{ name, message, stack }` shape the
18+
* previous flow persisted in `scriptError`.
19+
*/
20+
export function validateScriptSyntax(
21+
script: string,
22+
): { script: string; error?: undefined } | { script?: undefined; error: Pick<Error, 'name' | 'message' | 'stack'> } {
23+
try {
24+
// Wrap so top-level return/declarations parse the same way as in
25+
// getCompatibilityScript at runtime. vm.Script only parses — it does
26+
// not execute the code here.
27+
// eslint-disable-next-line no-new
28+
new vm.Script(`(function(){${script}})`);
29+
return { script };
30+
} catch (e) {
31+
if (e instanceof SyntaxError) {
32+
const { name, message, stack } = e;
33+
return { error: { name, message, stack } };
34+
}
35+
throw e;
36+
}
37+
}

apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'
1010
import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
1111
import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger';
1212
import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener';
13-
import { compileIntegrationScript } from '../../lib/compileIntegrationScript';
1413
import { validateScriptEngine, isScriptEngineFrozen } from '../../lib/validateScriptEngine';
14+
import { validateScriptSyntax } from '../../lib/validateScriptSyntax';
1515

1616
const validChannelChars = ['@', '#'];
1717

@@ -108,13 +108,16 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn
108108
_createdBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }),
109109
};
110110

111+
// Validate the script syntax if it is enabled and using a sandbox that is
112+
// not frozen. isolated-vm embeds modern V8 and runs the script natively, so
113+
// no transpilation is needed.
111114
if (
112115
!isScriptEngineFrozen(integrationData.scriptEngine) &&
113116
integration.scriptEnabled === true &&
114117
integration.script &&
115118
integration.script.trim() !== ''
116119
) {
117-
const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile });
120+
const { script, error } = validateScriptSyntax(integration.script);
118121
if (error) {
119122
integrationData.scriptCompiled = undefined;
120123
integrationData.scriptError = error;

apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles'
88
import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
99
import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger';
1010
import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener';
11-
import { compileIntegrationScript } from '../../lib/compileIntegrationScript';
1211
import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine';
12+
import { validateScriptSyntax } from '../../lib/validateScriptSyntax';
1313

1414
const validChannelChars = ['@', '#'];
1515

@@ -84,25 +84,23 @@ export const updateIncomingIntegration = async (
8484

8585
const isFrozen = isScriptEngineFrozen(scriptEngine);
8686

87-
// Default to transpiling with Babel for backwards compatibility; integrations
88-
// can opt-out per-record by setting `skipTranspile: true` (removed in 9.0.0).
89-
const skipTranspile = integration.skipTranspile === true;
90-
9187
if (!isFrozen && integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') {
92-
const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile });
88+
// isolated-vm embeds modern V8 and runs the script natively, so no
89+
// transpilation is needed. Syntax is still validated at save time.
90+
const { script, error } = validateScriptSyntax(integration.script);
9391
if (error) {
9492
await Integrations.updateOne(
9593
{ _id: integrationId },
9694
{
97-
$set: { scriptError: error, skipTranspile },
95+
$set: { scriptError: error },
9896
$unset: { scriptCompiled: 1 as const },
9997
},
10098
);
10199
} else {
102100
await Integrations.updateOne(
103101
{ _id: integrationId },
104102
{
105-
$set: { scriptCompiled: script, skipTranspile },
103+
$set: { scriptCompiled: script },
106104
$unset: { scriptError: 1 as const },
107105
},
108106
);
@@ -171,7 +169,6 @@ export const updateIncomingIntegration = async (
171169
...(typeof integration.script !== 'undefined' && { script: integration.script }),
172170
scriptEnabled: integration.scriptEnabled,
173171
...(scriptEngine && { scriptEngine }),
174-
skipTranspile,
175172
}),
176173
...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && {
177174
overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled,

apps/meteor/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@
6868
"@aws-sdk/client-s3": "^3.862.0",
6969
"@aws-sdk/lib-storage": "^3.862.0",
7070
"@aws-sdk/s3-request-presigner": "^3.862.0",
71-
"@babel/core": "~7.29.0",
72-
"@babel/preset-env": "~7.29.5",
7371
"@babel/runtime": "~7.29.2",
7472
"@bugsnag/js": "~7.20.2",
7573
"@bugsnag/plugin-react": "~7.19.0",

apps/meteor/tests/end-to-end/api/incoming-integrations.ts

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,8 @@ describe('[Incoming Integrations]', () => {
367367
describe('Script integration tests', () => {
368368
let withScript: IIntegration;
369369
let withScriptDefaultContentType: IIntegration;
370-
let withSkipTranspile: IIntegration;
371370

372-
const sloppyModeScript =
371+
const strictModeScript =
373372
'const buildMessage = (obj) => {\n' +
374373
' \n' +
375374
' const template = `[#VALUE](${ obj.test })`;\n' +
@@ -381,7 +380,7 @@ describe('[Incoming Integrations]', () => {
381380
' \n' +
382381
' class Script {\n' +
383382
' process_incoming_request({ request }) {\n' +
384-
' msg = buildMessage(request.content);\n' +
383+
' const msg = buildMessage(request.content);\n' +
385384
' \n' +
386385
' return {\n' +
387386
' content:{\n' +
@@ -434,38 +433,14 @@ describe('[Incoming Integrations]', () => {
434433
scriptEnabled: true,
435434
overrideDestinationChannelEnabled: false,
436435
channel: '#general',
437-
script: sloppyModeScript,
436+
script: strictModeScript,
438437
})
439438
.expect(200);
440439
withScriptDefaultContentType = res2.body.integration;
441-
442-
// Same script but with skipTranspile: true — no Babel, class methods
443-
// run in strict mode so `msg = buildMessage(...)` throws ReferenceError.
444-
const res3 = await request
445-
.post(api('integrations.create'))
446-
.set(credentials)
447-
.send({
448-
type: 'webhook-incoming',
449-
name: 'Incoming test with skipTranspile',
450-
enabled: true,
451-
alias: 'test',
452-
username: 'rocket.cat',
453-
scriptEnabled: true,
454-
skipTranspile: true,
455-
overrideDestinationChannelEnabled: false,
456-
channel: '#general',
457-
script: sloppyModeScript,
458-
})
459-
.expect(200);
460-
withSkipTranspile = res3.body.integration;
461440
});
462441

463442
after(async () => {
464-
await Promise.all([
465-
removeIntegration(withScript._id, 'incoming'),
466-
removeIntegration(withScriptDefaultContentType._id, 'incoming'),
467-
removeIntegration(withSkipTranspile._id, 'incoming'),
468-
]);
443+
await Promise.all([removeIntegration(withScript._id, 'incoming'), removeIntegration(withScriptDefaultContentType._id, 'incoming')]);
469444
});
470445

471446
it('should send a message if the payload is a application/x-www-form-urlencoded JSON AND the integration has a valid script', async () => {
@@ -513,22 +488,6 @@ describe('[Incoming Integrations]', () => {
513488
expect(messagesResult.body).to.have.property('messages').and.to.be.an('array');
514489
expect(!!(messagesResult.body.messages as IMessage[]).find((m) => m.msg === '[#VALUE](test)')).to.be.true;
515490
});
516-
517-
it('should create the skipTranspile integration with scriptCompiled and no scriptError', () => {
518-
expect(withSkipTranspile).to.have.property('scriptCompiled');
519-
expect(withSkipTranspile).to.not.have.property('scriptError');
520-
expect(withSkipTranspile).to.have.property('skipTranspile', true);
521-
});
522-
523-
it('should fail to execute the same sloppy-mode script when skipTranspile is true', async () => {
524-
const payload = { test: 'test' };
525-
526-
await request
527-
.post(`/hooks/${withSkipTranspile._id}/${withSkipTranspile.token}`)
528-
.set('Content-Type', 'application/json')
529-
.send(JSON.stringify(payload))
530-
.expect(400);
531-
});
532491
});
533492

534493
describe('With manage-own-incoming-integrations permission', () => {

0 commit comments

Comments
 (0)