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/.changeset/stale-meals-check.md b/.changeset/stale-meals-check.md new file mode 100644 index 0000000000000..650848aa1bb5e --- /dev/null +++ b/.changeset/stale-meals-check.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes insertOrUpdateSound and uploadCustomSound Meteor methods diff --git a/apps/meteor/app/custom-sounds/server/index.ts b/apps/meteor/app/custom-sounds/server/index.ts index 6a14ccff15656..1825fa7e7dc40 100644 --- a/apps/meteor/app/custom-sounds/server/index.ts +++ b/apps/meteor/app/custom-sounds/server/index.ts @@ -1,5 +1,3 @@ import './startup/custom-sounds'; import './methods/deleteCustomSound'; -import './methods/insertOrUpdateSound'; import './methods/listCustomSounds'; -import './methods/uploadCustomSound'; diff --git a/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts b/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts index 9170e7fccff43..c36eb22ec4c3e 100644 --- a/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts +++ b/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts @@ -2,9 +2,17 @@ import { api } from '@rocket.chat/core-services'; import { CustomSounds } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import type { ICustomSoundData } from '../methods/insertOrUpdateSound'; import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; +export type ICustomSoundData = { + _id?: string; + name: string; + extension: string; + previousName?: string; + previousExtension?: string; + newFile?: boolean; +}; + export const insertOrUpdateSound = async (soundData: ICustomSoundData): Promise => { // silently strip colon; this allows for uploading :soundname: as soundname soundData.name = (soundData.name || '').replace(/:/g, ''); diff --git a/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts b/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts index 6d2ef506f1ff3..2d0bf4f344439 100644 --- a/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts +++ b/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts @@ -1,8 +1,8 @@ import { api } from '@rocket.chat/core-services'; import type { RequiredField } from '@rocket.chat/core-typings'; +import type { ICustomSoundData } from './insertOrUpdateSound'; import { RocketChatFile } from '../../../file/server'; -import type { ICustomSoundData } from '../methods/insertOrUpdateSound'; import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; export const uploadCustomSound = async ( diff --git a/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts b/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts deleted file mode 100644 index a2881ad13ef2d..0000000000000 --- a/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { insertOrUpdateSound } from '../lib/insertOrUpdateSound'; - -export type ICustomSoundData = { - _id?: string; - name: string; - extension: string; - previousName?: string; - previousSound?: { - extension?: string; - }; - previousExtension?: string; - newFile?: boolean; - random?: number; -}; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - insertOrUpdateSound(soundData: ICustomSoundData): Promise; - } -} - -Meteor.methods({ - async insertOrUpdateSound(soundData) { - methodDeprecationLogger.method('insertOrUpdateSound', '9.0.0', ['/v1/custom-sounds.create', '/v1/custom-sounds.update']); - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) { - throw new Meteor.Error('not_authorized'); - } - if (soundData._id) { - check(soundData._id, String); - } - return insertOrUpdateSound(soundData); - }, -}); diff --git a/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts b/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts deleted file mode 100644 index e979d66110ba9..0000000000000 --- a/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { RequiredField } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import type { ICustomSoundData } from './insertOrUpdateSound'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { uploadCustomSound } from '../lib/uploadCustomSound'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - uploadCustomSound(binaryContent: string, contentType: string, soundData: RequiredField): void; - } -} - -Meteor.methods({ - async uploadCustomSound(binaryContent, contentType, soundData) { - methodDeprecationLogger.method('uploadCustomSound', '9.0.0', ['/v1/custom-sounds.create', '/v1/custom-sounds.update']); - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) { - throw new Meteor.Error('not_authorized'); - } - const buffer = Buffer.from(binaryContent, 'binary'); - await uploadCustomSound(buffer, contentType, soundData); - }, -}); diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index ec08a5d835a74..3b6ba8800f39b 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -8,7 +8,6 @@ import type { IMessage, FileProp, } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms, Uploads, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -18,7 +17,6 @@ import { getFileExtension } from '../../../../lib/utils/getFileExtension'; import { callbacks } from '../../../../server/lib/callbacks'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; import { FileUpload } from '../lib/FileUpload'; @@ -163,13 +161,6 @@ export const parseFileIntoMessageAttachments = async ( return { files, attachments }; }; -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - sendFileMessage: (roomId: string, _store: string, file: Partial, msgData?: Record) => boolean; - } -} - export const sendFileMessage = async ( userId: string, { @@ -239,17 +230,3 @@ export const sendFileMessage = async ( return msg; }; - -Meteor.methods({ - async sendFileMessage(roomId, _store, file, msgData = {}) { - methodDeprecationLogger.method('sendFileMessage', '9.0.0', '/v1/rooms.mediaConfirm/:rid/:fileId'); - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'sendFileMessage', - } as any); - } - - return sendFileMessage(userId, { roomId, file, msgData }); - }, -}); diff --git a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts index b4656f629d834..d51ff76bb82ba 100644 --- a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts @@ -3,8 +3,8 @@ import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { compileIntegrationScript } from './compileIntegrationScript'; 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'; @@ -181,7 +181,9 @@ export const validateOutgoingIntegration = async function ( integration.script && integration.script.trim() !== '' ) { - const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile }); + // 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; } 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 49116d1335ee8..d34cd2a66890e 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -9,8 +9,8 @@ import { Meteor } from 'meteor/meteor'; 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'; +import { validateScriptSyntax } from '../../lib/validateScriptSyntax'; const validChannelChars = ['@', '#']; @@ -107,13 +107,16 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn _createdBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }), }; + // 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() !== '' ) { - const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile }); + const { script, error } = validateScriptSyntax(integration.script); 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 894b5b31bd405..4971b930c2368 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -7,8 +7,8 @@ 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'; +import { validateScriptSyntax } from '../../lib/validateScriptSyntax'; const validChannelChars = ['@', '#']; @@ -83,17 +83,15 @@ export const updateIncomingIntegration = async ( const isFrozen = isScriptEngineFrozen(scriptEngine); - // Default to transpiling with Babel for backwards compatibility; integrations - // 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: !skipTranspile }); + // 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, skipTranspile }, + $set: { scriptError: error }, $unset: { scriptCompiled: 1 as const }, }, ); @@ -101,7 +99,7 @@ export const updateIncomingIntegration = async ( await Integrations.updateOne( { _id: integrationId }, { - $set: { scriptCompiled: script, skipTranspile }, + $set: { scriptCompiled: script }, $unset: { scriptError: 1 as const }, }, ); @@ -170,7 +168,6 @@ export const updateIncomingIntegration = async ( ...(typeof integration.script !== 'undefined' && { script: integration.script }), scriptEnabled: integration.scriptEnabled, ...(scriptEngine && { scriptEngine }), - skipTranspile, }), ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 277ec8a3fd71f..daeddd14cccff 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "8.5.0-develop" + "version": "9.0.0-develop" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 65d6b071d8783..d715555a573e8 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/meteor", - "version": "8.5.0-develop", + "version": "9.0.0-develop", "private": true, "description": "The Ultimate Open Source WebChat Platform", "keywords": [ @@ -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.29.0", - "@babel/preset-env": "~7.29.5", "@babel/runtime": "~7.29.2", "@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 69d609b9b8fb6..20496a521aa94 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -367,9 +367,8 @@ describe('[Incoming Integrations]', () => { describe('Script integration tests', () => { let withScript: IIntegration; let withScriptDefaultContentType: IIntegration; - let withSkipTranspile: IIntegration; - const sloppyModeScript = + const strictModeScript = 'const buildMessage = (obj) => {\n' + ' \n' + ' const template = `[#VALUE](${ obj.test })`;\n' + @@ -381,7 +380,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' + @@ -434,38 +433,14 @@ describe('[Incoming Integrations]', () => { scriptEnabled: true, overrideDestinationChannelEnabled: false, channel: '#general', - script: sloppyModeScript, + script: strictModeScript, }) .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'), - removeIntegration(withSkipTranspile._id, 'incoming'), - ]); + await Promise.all([removeIntegration(withScript._id, 'incoming'), removeIntegration(withScriptDefaultContentType._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 () => { @@ -513,22 +488,6 @@ 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; }); - - 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 the same sloppy-mode script when skipTranspile is true', async () => { - const payload = { test: 'test' }; - - await request - .post(`/hooks/${withSkipTranspile._id}/${withSkipTranspile.token}`) - .set('Content-Type', 'application/json') - .send(JSON.stringify(payload)) - .expect(400); - }); }); describe('With manage-own-incoming-integrations permission', () => { diff --git a/package.json b/package.json index 958185bc0faff..8ea9a1ae97b46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "8.5.0-develop", + "version": "9.0.0-develop", "private": true, "description": "Rocket.Chat Monorepo", "homepage": "https://github.com/RocketChat/Rocket.Chat#readme", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 7bfb454b59f4d..dc2022b1dc1d3 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,8 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "8.5.0-develop", + "version": "9.0.0-develop", + "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", "files": [ diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 7af85e25846d7..08ea92f47b614 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "8.5.0-develop", + "version": "9.0.0-develop", "main": "./dist/index.js", "typings": "./dist/index.d.ts", "files": [