From fbfca8202ef3fbff1d90adc0c3930a7499e07c57 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Tue, 18 Nov 2025 15:40:17 +0530 Subject: [PATCH 01/15] Graduate getUserRoomIds to user bridge --- .../definition/accessors/IExperimentalRead.ts | 10 +----- .../src/definition/accessors/IUserRead.ts | 9 ++++++ .../src/server/accessors/ExperimentalRead.ts | 8 ++--- .../src/server/accessors/UserRead.ts | 4 +++ .../src/server/bridges/ExperimentalBridge.ts | 32 +------------------ .../src/server/bridges/UserBridge.ts | 23 +++++++++++++ .../src/server/permissions/AppPermissions.ts | 4 +-- .../tests/server/accessors/UserRead.spec.ts | 8 +++++ .../test-data/bridges/experimentalBridge.ts | 6 +--- .../tests/test-data/bridges/userBridge.ts | 4 +++ packages/i18n/src/locales/en.i18n.json | 3 +- 11 files changed, 56 insertions(+), 55 deletions(-) diff --git a/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts index 5c202911d1424..124e27af8eaf7 100644 --- a/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts +++ b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts @@ -5,12 +5,4 @@ * team evaluates the proper signature, underlying implementation and performance * impact of candidates for future APIs */ -export interface IExperimentalRead { - /** - * Fetches the IDs of the rooms that the user is a member of. - * - * @returns an array of room ids or undefined if the app doesn't have the proper permission - * @experimental - */ - getUserRoomIds(userId: string): Promise; -} +export interface IExperimentalRead {} diff --git a/packages/apps-engine/src/definition/accessors/IUserRead.ts b/packages/apps-engine/src/definition/accessors/IUserRead.ts index cd4ad3f91b2ef..21a3122baa831 100644 --- a/packages/apps-engine/src/definition/accessors/IUserRead.ts +++ b/packages/apps-engine/src/definition/accessors/IUserRead.ts @@ -19,4 +19,13 @@ export interface IUserRead { * @param uid user's id */ getUserUnreadMessageCount(uid: string): Promise; + + /** + * Fetches the IDs of the rooms that the user is a member of. + * + * Requires the `user.getUserRoomIds` permission. + * + * @param userId the user whose memberships should be returned + */ + getUserRoomIds(userId: string): Promise; } diff --git a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts index e94254d40ed9b..56962a1b625ad 100644 --- a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts +++ b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts @@ -3,11 +3,7 @@ import type { ExperimentalBridge } from '../bridges'; export class ExperimentalRead implements IExperimentalRead { constructor( - private experimentalBridge: ExperimentalBridge, - private appId: string, + private readonly experimentalBridge: ExperimentalBridge, + private readonly appId: string, ) {} - - public async getUserRoomIds(userId: string): Promise { - return this.experimentalBridge.doGetUserRoomIds(userId, this.appId); - } } diff --git a/packages/apps-engine/src/server/accessors/UserRead.ts b/packages/apps-engine/src/server/accessors/UserRead.ts index 275750ef8064c..c3c916bef1172 100644 --- a/packages/apps-engine/src/server/accessors/UserRead.ts +++ b/packages/apps-engine/src/server/accessors/UserRead.ts @@ -23,4 +23,8 @@ export class UserRead implements IUserRead { public getUserUnreadMessageCount(uid: string): Promise { return this.userBridge.doGetUserUnreadMessageCount(uid, this.appId); } + + public getUserRoomIds(userId: string): Promise { + return this.userBridge.doGetUserRoomIds(userId, this.appId); + } } diff --git a/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts b/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts index d808df5790409..806b0d66fef37 100644 --- a/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts +++ b/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts @@ -1,7 +1,4 @@ import { BaseBridge } from './BaseBridge'; -import { PermissionDeniedError } from '../errors/PermissionDeniedError'; -import { AppPermissionManager } from '../managers/AppPermissionManager'; -import { AppPermissions } from '../permissions/AppPermissions'; /** * @description @@ -10,31 +7,4 @@ import { AppPermissions } from '../permissions/AppPermissions'; * team evaluates the proper signature, underlying implementation and performance * impact of candidates for future APIs */ -export abstract class ExperimentalBridge extends BaseBridge { - /** - * - * Candidate bridge: User bridge - */ - public async doGetUserRoomIds(userId: string, appId: string): Promise { - if (this.hasPermission('getUserRoomIds', appId)) { - return this.getUserRoomIds(userId, appId); - } - } - - protected abstract getUserRoomIds(userId: string, appId: string): Promise; - - private hasPermission(feature: keyof typeof AppPermissions.experimental, appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.experimental[feature])) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.experimental[feature]], - }), - ); - - return false; - } -} +export abstract class ExperimentalBridge extends BaseBridge {} diff --git a/packages/apps-engine/src/server/bridges/UserBridge.ts b/packages/apps-engine/src/server/bridges/UserBridge.ts index 7112a5a056eca..835a968d01fae 100644 --- a/packages/apps-engine/src/server/bridges/UserBridge.ts +++ b/packages/apps-engine/src/server/bridges/UserBridge.ts @@ -45,6 +45,12 @@ export abstract class UserBridge extends BaseBridge { } } + public async doGetUserRoomIds(userId: string, appId: string): Promise { + if (this.hasGetUserRoomIdsPermission(appId)) { + return this.getUserRoomIds(userId, appId); + } + } + public async doDeleteUsersCreatedByApp(appId: string, type: UserType.BOT | UserType.APP): Promise { if (this.hasWritePermission(appId)) { return this.deleteUsersCreatedByApp(appId, type); @@ -67,6 +73,8 @@ export abstract class UserBridge extends BaseBridge { protected abstract getUserUnreadMessageCount(uid: string, appId: string): Promise; + protected abstract getUserRoomIds(userId: string, appId: string): Promise; + /** * Creates a user. * @param data the essential data for creating a user @@ -146,4 +154,19 @@ export abstract class UserBridge extends BaseBridge { return false; } + + private hasGetUserRoomIdsPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.user.getUserRoomIds)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.user.getUserRoomIds], + }), + ); + + return false; + } } diff --git a/packages/apps-engine/src/server/permissions/AppPermissions.ts b/packages/apps-engine/src/server/permissions/AppPermissions.ts index 92e6c518e3ed8..a6aa25f0c993b 100644 --- a/packages/apps-engine/src/server/permissions/AppPermissions.ts +++ b/packages/apps-engine/src/server/permissions/AppPermissions.ts @@ -25,6 +25,7 @@ export const AppPermissions = { 'user': { read: { name: 'user.read' }, write: { name: 'user.write' }, + getUserRoomIds: { name: 'user.getUserRoomIds' }, }, 'upload': { read: { name: 'upload.read' }, @@ -122,9 +123,6 @@ export const AppPermissions = { 'outboundComms': { provide: { name: 'outbound-communication.provide' }, }, - 'experimental': { - getUserRoomIds: { name: 'experimental.getUserRoomIds' }, - }, }; /** diff --git a/packages/apps-engine/tests/server/accessors/UserRead.spec.ts b/packages/apps-engine/tests/server/accessors/UserRead.spec.ts index 47e652871f822..4937a3dce61b4 100644 --- a/packages/apps-engine/tests/server/accessors/UserRead.spec.ts +++ b/packages/apps-engine/tests/server/accessors/UserRead.spec.ts @@ -12,11 +12,15 @@ export class UserReadAccessorTestFixture { private mockAppId: 'test-appId'; + private roomIds: Array; + @SetupFixture public setupFixture() { this.user = TestData.getUser(); + this.roomIds = ['room-1', 'room-2']; const theUser = this.user; + const roomIds = this.roomIds; this.mockUserBridge = { doGetById(id, appId): Promise { return Promise.resolve(theUser); @@ -27,6 +31,9 @@ export class UserReadAccessorTestFixture { doGetAppUser(appId?: string): Promise { return Promise.resolve(theUser); }, + doGetUserRoomIds(userId: string): Promise> { + return Promise.resolve(roomIds); + }, } as UserBridge; } @@ -45,5 +52,6 @@ export class UserReadAccessorTestFixture { Expect(await ur.getAppUser(this.mockAppId)).toBeDefined(); Expect(await ur.getAppUser(this.mockAppId)).toEqual(this.user); Expect(await ur.getAppUser()).toEqual(this.user); + Expect(await ur.getUserRoomIds(this.user.id)).toEqual(this.roomIds); } } diff --git a/packages/apps-engine/tests/test-data/bridges/experimentalBridge.ts b/packages/apps-engine/tests/test-data/bridges/experimentalBridge.ts index 350411e20890d..c30da179c5c98 100644 --- a/packages/apps-engine/tests/test-data/bridges/experimentalBridge.ts +++ b/packages/apps-engine/tests/test-data/bridges/experimentalBridge.ts @@ -1,7 +1,3 @@ import { ExperimentalBridge } from '../../../src/server/bridges'; -export class TestExperimentalBridge extends ExperimentalBridge { - protected getUserRoomIds(userId: string, appId: string): Promise { - throw new Error('Method not implemented.'); - } -} +export class TestExperimentalBridge extends ExperimentalBridge {} diff --git a/packages/apps-engine/tests/test-data/bridges/userBridge.ts b/packages/apps-engine/tests/test-data/bridges/userBridge.ts index 1bba51003b139..d5b845d9b01b6 100644 --- a/packages/apps-engine/tests/test-data/bridges/userBridge.ts +++ b/packages/apps-engine/tests/test-data/bridges/userBridge.ts @@ -38,6 +38,10 @@ export class TestsUserBridge extends UserBridge { throw new Error('Method not implemented.'); } + protected getUserRoomIds(userId: string, appId: string): Promise { + throw new Error('Method not implemented.'); + } + protected deactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise { throw new Error('Method not implemented.'); } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 7991ab6c17650..cb7d50d5393df 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -642,6 +642,7 @@ "Apps_Permissions_upload_read": "Access files uploaded to this server", "Apps_Permissions_upload_write": "Upload files to this server", "Apps_Permissions_user_read": "Access user information", + "Apps_Permissions_user_getUserRoomIds": "List the rooms a user is a member of", "Apps_Permissions_user_write": "Modify user information", "Apps_Private_App_Is_Exempt": "{{appName}} is already installed and exempt from the app limit policy.\nExempted apps cannot be updated.", "Apps_Settings": "App's Settings", @@ -7092,4 +7093,4 @@ "timestamps.fullDateTimeDescription": "December 31, 2020 12:00 AM", "timestamps.fullDateTimeLongDescription": "Thursday, December 31, 2020 12:00:00 AM", "timestamps.relativeTimeDescription": "1 year ago" -} \ No newline at end of file +} From ece633a43f5c5317abeae5d13ac55cc56d6ff826 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Tue, 18 Nov 2025 15:40:43 +0530 Subject: [PATCH 02/15] Update Meteor user bridge room access --- .../app/apps/server/bridges/experimental.ts | 23 ------------------- apps/meteor/app/apps/server/bridges/users.ts | 8 +++++++ 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/experimental.ts b/apps/meteor/app/apps/server/bridges/experimental.ts index 203097afeea7a..13613417c6099 100644 --- a/apps/meteor/app/apps/server/bridges/experimental.ts +++ b/apps/meteor/app/apps/server/bridges/experimental.ts @@ -1,31 +1,8 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { ExperimentalBridge } from '@rocket.chat/apps-engine/server/bridges'; -import { Subscriptions } from '@rocket.chat/models'; - -import { metrics } from '../../../metrics/server/lib/metrics'; export class AppExperimentalBridge extends ExperimentalBridge { constructor(private readonly orch: IAppServerOrchestrator) { super(); } - - protected async getUserRoomIds(userId: string, appId: string): Promise { - const stopTimer = metrics.appBridgeMethods.startTimer({ - bridge: 'experimental', - method: 'getUserRoomIds', - app_id: appId, - }); - - try { - this.orch.debugLog(`The App ${appId} is getting the room ids for the user: "${userId}"`); - - const subscriptions = await Subscriptions.findByUserId(userId, { projection: { rid: 1 } }).toArray(); - - const result = subscriptions.map((subscription) => subscription.rid); - - return result; - } finally { - stopTimer(); - } - } } diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 13d2436c768c9..531d8cda1962a 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -168,4 +168,12 @@ export class AppUserBridge extends UserBridge { protected async getUserUnreadMessageCount(uid: string): Promise { return Subscriptions.getBadgeCount(uid); } + + protected async getUserRoomIds(userId: string, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is getting the room ids for the user: "${userId}"`); + + const subscriptions = await Subscriptions.findByUserId(userId, { projection: { rid: 1 } }).toArray(); + + return subscriptions.map((subscription) => subscription.rid); + } } From b7379d7c8c2f5aef07cecacf557675e178254979 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Tue, 18 Nov 2025 15:48:55 +0530 Subject: [PATCH 03/15] Reuse existing user read permission --- .../src/definition/accessors/IUserRead.ts | 2 -- .../src/server/bridges/UserBridge.ts | 17 +---------------- .../src/server/permissions/AppPermissions.ts | 1 - packages/i18n/src/locales/en.i18n.json | 1 - 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/packages/apps-engine/src/definition/accessors/IUserRead.ts b/packages/apps-engine/src/definition/accessors/IUserRead.ts index 21a3122baa831..deaf5b908a96f 100644 --- a/packages/apps-engine/src/definition/accessors/IUserRead.ts +++ b/packages/apps-engine/src/definition/accessors/IUserRead.ts @@ -23,8 +23,6 @@ export interface IUserRead { /** * Fetches the IDs of the rooms that the user is a member of. * - * Requires the `user.getUserRoomIds` permission. - * * @param userId the user whose memberships should be returned */ getUserRoomIds(userId: string): Promise; diff --git a/packages/apps-engine/src/server/bridges/UserBridge.ts b/packages/apps-engine/src/server/bridges/UserBridge.ts index 835a968d01fae..a4b8c1b118a4a 100644 --- a/packages/apps-engine/src/server/bridges/UserBridge.ts +++ b/packages/apps-engine/src/server/bridges/UserBridge.ts @@ -46,7 +46,7 @@ export abstract class UserBridge extends BaseBridge { } public async doGetUserRoomIds(userId: string, appId: string): Promise { - if (this.hasGetUserRoomIdsPermission(appId)) { + if (this.hasReadPermission(appId)) { return this.getUserRoomIds(userId, appId); } } @@ -154,19 +154,4 @@ export abstract class UserBridge extends BaseBridge { return false; } - - private hasGetUserRoomIdsPermission(appId: string): boolean { - if (AppPermissionManager.hasPermission(appId, AppPermissions.user.getUserRoomIds)) { - return true; - } - - AppPermissionManager.notifyAboutError( - new PermissionDeniedError({ - appId, - missingPermissions: [AppPermissions.user.getUserRoomIds], - }), - ); - - return false; - } } diff --git a/packages/apps-engine/src/server/permissions/AppPermissions.ts b/packages/apps-engine/src/server/permissions/AppPermissions.ts index a6aa25f0c993b..04e4acfa0612a 100644 --- a/packages/apps-engine/src/server/permissions/AppPermissions.ts +++ b/packages/apps-engine/src/server/permissions/AppPermissions.ts @@ -25,7 +25,6 @@ export const AppPermissions = { 'user': { read: { name: 'user.read' }, write: { name: 'user.write' }, - getUserRoomIds: { name: 'user.getUserRoomIds' }, }, 'upload': { read: { name: 'upload.read' }, diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index cb7d50d5393df..49d2fea7398d9 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -642,7 +642,6 @@ "Apps_Permissions_upload_read": "Access files uploaded to this server", "Apps_Permissions_upload_write": "Upload files to this server", "Apps_Permissions_user_read": "Access user information", - "Apps_Permissions_user_getUserRoomIds": "List the rooms a user is a member of", "Apps_Permissions_user_write": "Modify user information", "Apps_Private_App_Is_Exempt": "{{appName}} is already installed and exempt from the app limit policy.\nExempted apps cannot be updated.", "Apps_Settings": "App's Settings", From ac72cca187970046c7737eb66280d57788ca9f64 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Tue, 18 Nov 2025 15:59:30 +0530 Subject: [PATCH 04/15] Fix experimental accessor build --- .../apps-engine/src/server/accessors/ExperimentalRead.ts | 8 +------- .../apps-engine/src/server/managers/AppAccessorManager.ts | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts index 56962a1b625ad..e339734e92cfa 100644 --- a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts +++ b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts @@ -1,9 +1,3 @@ import type { IExperimentalRead } from '../../definition/accessors'; -import type { ExperimentalBridge } from '../bridges'; -export class ExperimentalRead implements IExperimentalRead { - constructor( - private readonly experimentalBridge: ExperimentalBridge, - private readonly appId: string, - ) {} -} +export class ExperimentalRead implements IExperimentalRead {} diff --git a/packages/apps-engine/src/server/managers/AppAccessorManager.ts b/packages/apps-engine/src/server/managers/AppAccessorManager.ts index 9247d83195c86..d14974e73e51f 100644 --- a/packages/apps-engine/src/server/managers/AppAccessorManager.ts +++ b/packages/apps-engine/src/server/managers/AppAccessorManager.ts @@ -190,7 +190,7 @@ export class AppAccessorManager { const contactReader = new ContactRead(this.bridges, appId); const thread = new ThreadRead(this.bridges.getThreadBridge(), appId); const role = new RoleRead(this.bridges.getRoleBridge(), appId); - const experimental = new ExperimentalRead(this.bridges.getExperimentalBridge(), appId); + const experimental = new ExperimentalRead(); this.readers.set( appId, From 2eb6ed8a4a244d9459b982fe53ad7da286324b70 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Tue, 18 Nov 2025 16:10:37 +0530 Subject: [PATCH 05/15] Add changeset --- .changeset/calm-falcons-gather.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/calm-falcons-gather.md diff --git a/.changeset/calm-falcons-gather.md b/.changeset/calm-falcons-gather.md new file mode 100644 index 0000000000000..71762fc9f5003 --- /dev/null +++ b/.changeset/calm-falcons-gather.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/apps-engine": minor +"@rocket.chat/meteor": minor +--- + +Graduates the `getUserRoomIds` accessor from the experimental bridge to the stable user bridge after the experiment proved successful so apps can rely on the API under the standard user read permission. From 369e53ebb6516152638e16a4f6485cb47e454b57 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Tue, 18 Nov 2025 16:18:31 +0530 Subject: [PATCH 06/15] Fix missing newline at end of file in en.i18n.json --- packages/i18n/src/locales/en.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 49d2fea7398d9..7991ab6c17650 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -7092,4 +7092,4 @@ "timestamps.fullDateTimeDescription": "December 31, 2020 12:00 AM", "timestamps.fullDateTimeLongDescription": "Thursday, December 31, 2020 12:00:00 AM", "timestamps.relativeTimeDescription": "1 year ago" -} +} \ No newline at end of file From 36dc4696c5f06a4e3f50a62fb62f03cd5ee68e2b Mon Sep 17 00:00:00 2001 From: Dnouv Date: Tue, 18 Nov 2025 16:24:02 +0530 Subject: [PATCH 07/15] Fix lint warnings --- .../apps-engine/src/definition/accessors/IExperimentalRead.ts | 2 +- packages/apps-engine/tests/server/accessors/UserRead.spec.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts index 124e27af8eaf7..3c09026a327d1 100644 --- a/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts +++ b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts @@ -5,4 +5,4 @@ * team evaluates the proper signature, underlying implementation and performance * impact of candidates for future APIs */ -export interface IExperimentalRead {} +export type IExperimentalRead = Record; diff --git a/packages/apps-engine/tests/server/accessors/UserRead.spec.ts b/packages/apps-engine/tests/server/accessors/UserRead.spec.ts index 4937a3dce61b4..16c933b6cc984 100644 --- a/packages/apps-engine/tests/server/accessors/UserRead.spec.ts +++ b/packages/apps-engine/tests/server/accessors/UserRead.spec.ts @@ -19,8 +19,7 @@ export class UserReadAccessorTestFixture { this.user = TestData.getUser(); this.roomIds = ['room-1', 'room-2']; - const theUser = this.user; - const roomIds = this.roomIds; + const { user: theUser, roomIds } = this; this.mockUserBridge = { doGetById(id, appId): Promise { return Promise.resolve(theUser); From b2106d84cd0c74e1f618172c22a051266bdbbae9 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Tue, 18 Nov 2025 16:38:26 +0530 Subject: [PATCH 08/15] Make experimental read interface non-empty --- .../src/definition/accessors/IExperimentalRead.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts index 3c09026a327d1..cacdec26cb7d5 100644 --- a/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts +++ b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts @@ -5,4 +5,9 @@ * team evaluates the proper signature, underlying implementation and performance * impact of candidates for future APIs */ -export type IExperimentalRead = Record; +export interface IExperimentalRead { + /** + * @internal placeholder so the interface is not empty while we evaluate new APIs. + */ + readonly __brand?: never; +} From 4f8f518b3e2712ebb0b01350cae3ac10afa7aacd Mon Sep 17 00:00:00 2001 From: Dnouv Date: Tue, 18 Nov 2025 17:07:33 +0530 Subject: [PATCH 09/15] AppExperimentalBridge remove dependency on orchestrator --- apps/meteor/app/apps/server/bridges/bridges.js | 2 +- apps/meteor/app/apps/server/bridges/experimental.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js index 3b49cd91394e9..b1e35bf213db6 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.js @@ -60,7 +60,7 @@ export class RealAppBridges extends AppBridges { this._emailBridge = new AppEmailBridge(orch); this._contactBridge = new AppContactBridge(orch); this._outboundMessageBridge = new OutboundCommunicationBridge(orch); - this._experimentalBridge = new AppExperimentalBridge(orch); + this._experimentalBridge = new AppExperimentalBridge(); } getCommandBridge() { diff --git a/apps/meteor/app/apps/server/bridges/experimental.ts b/apps/meteor/app/apps/server/bridges/experimental.ts index 13613417c6099..98a4488bcb4e0 100644 --- a/apps/meteor/app/apps/server/bridges/experimental.ts +++ b/apps/meteor/app/apps/server/bridges/experimental.ts @@ -1,8 +1,7 @@ -import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { ExperimentalBridge } from '@rocket.chat/apps-engine/server/bridges'; export class AppExperimentalBridge extends ExperimentalBridge { - constructor(private readonly orch: IAppServerOrchestrator) { + constructor() { super(); } } From cc26c93f3abd81b0c92a5bd9e677c13a43222001 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Wed, 19 Nov 2025 00:00:56 +0530 Subject: [PATCH 10/15] Restore experimental bridge wiring --- apps/meteor/app/apps/server/bridges/bridges.js | 2 +- apps/meteor/app/apps/server/bridges/experimental.ts | 4 +++- .../src/definition/accessors/IExperimentalRead.ts | 8 ++------ .../src/server/accessors/ExperimentalRead.ts | 10 +++++++++- .../src/server/managers/AppAccessorManager.ts | 2 +- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js index b1e35bf213db6..3b49cd91394e9 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.js @@ -60,7 +60,7 @@ export class RealAppBridges extends AppBridges { this._emailBridge = new AppEmailBridge(orch); this._contactBridge = new AppContactBridge(orch); this._outboundMessageBridge = new OutboundCommunicationBridge(orch); - this._experimentalBridge = new AppExperimentalBridge(); + this._experimentalBridge = new AppExperimentalBridge(orch); } getCommandBridge() { diff --git a/apps/meteor/app/apps/server/bridges/experimental.ts b/apps/meteor/app/apps/server/bridges/experimental.ts index 98a4488bcb4e0..0fe219457e928 100644 --- a/apps/meteor/app/apps/server/bridges/experimental.ts +++ b/apps/meteor/app/apps/server/bridges/experimental.ts @@ -1,7 +1,9 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { ExperimentalBridge } from '@rocket.chat/apps-engine/server/bridges'; export class AppExperimentalBridge extends ExperimentalBridge { - constructor() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(private readonly orch: IAppServerOrchestrator) { super(); } } diff --git a/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts index cacdec26cb7d5..bb1ed9f2a273d 100644 --- a/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts +++ b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts @@ -5,9 +5,5 @@ * team evaluates the proper signature, underlying implementation and performance * impact of candidates for future APIs */ -export interface IExperimentalRead { - /** - * @internal placeholder so the interface is not empty while we evaluate new APIs. - */ - readonly __brand?: never; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IExperimentalRead {} diff --git a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts index e339734e92cfa..5cb1e3522d577 100644 --- a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts +++ b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts @@ -1,3 +1,11 @@ import type { IExperimentalRead } from '../../definition/accessors'; +import type { ExperimentalBridge } from '../bridges'; -export class ExperimentalRead implements IExperimentalRead {} +export class ExperimentalRead implements IExperimentalRead { + constructor( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private readonly experimentalBridge: ExperimentalBridge, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private readonly appId: string, + ) {} +} diff --git a/packages/apps-engine/src/server/managers/AppAccessorManager.ts b/packages/apps-engine/src/server/managers/AppAccessorManager.ts index d14974e73e51f..9247d83195c86 100644 --- a/packages/apps-engine/src/server/managers/AppAccessorManager.ts +++ b/packages/apps-engine/src/server/managers/AppAccessorManager.ts @@ -190,7 +190,7 @@ export class AppAccessorManager { const contactReader = new ContactRead(this.bridges, appId); const thread = new ThreadRead(this.bridges.getThreadBridge(), appId); const role = new RoleRead(this.bridges.getRoleBridge(), appId); - const experimental = new ExperimentalRead(); + const experimental = new ExperimentalRead(this.bridges.getExperimentalBridge(), appId); this.readers.set( appId, From 4e6bd516a5eb3dd8ea6d769d414d1ef6dd23901b Mon Sep 17 00:00:00 2001 From: Dnouv Date: Wed, 19 Nov 2025 00:04:07 +0530 Subject: [PATCH 11/15] Ensure context is maintained in ExperimentalRead constructor --- .../src/server/accessors/ExperimentalRead.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts index 5cb1e3522d577..1e2c4c14ab02f 100644 --- a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts +++ b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts @@ -3,9 +3,14 @@ import type { ExperimentalBridge } from '../bridges'; export class ExperimentalRead implements IExperimentalRead { constructor( - // eslint-disable-next-line @typescript-eslint/no-unused-vars private readonly experimentalBridge: ExperimentalBridge, - // eslint-disable-next-line @typescript-eslint/no-unused-vars private readonly appId: string, - ) {} + ) { + this.keepContext(); + } + + private keepContext(): void { + void this.experimentalBridge; + void this.appId; + } } From 036e677463a8089b39b09854cf4e426a0b67697d Mon Sep 17 00:00:00 2001 From: Dnouv Date: Wed, 19 Nov 2025 00:30:03 +0530 Subject: [PATCH 12/15] AppExperimentalBridge to ensure context is maintained in constructor --- apps/meteor/app/apps/server/bridges/experimental.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/apps/server/bridges/experimental.ts b/apps/meteor/app/apps/server/bridges/experimental.ts index 0fe219457e928..ddeb565108a0c 100644 --- a/apps/meteor/app/apps/server/bridges/experimental.ts +++ b/apps/meteor/app/apps/server/bridges/experimental.ts @@ -2,8 +2,13 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { ExperimentalBridge } from '@rocket.chat/apps-engine/server/bridges'; export class AppExperimentalBridge extends ExperimentalBridge { - // eslint-disable-next-line @typescript-eslint/no-unused-vars constructor(private readonly orch: IAppServerOrchestrator) { super(); + + this.keepContext(); + } + + private keepContext(): void { + void this.orch; } } From bd241fe77a1656e5438ad6fd17146d791e1d3835 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Wed, 19 Nov 2025 10:48:02 +0530 Subject: [PATCH 13/15] Relax experimental bridge visibility --- apps/meteor/app/apps/server/bridges/experimental.ts | 8 +------- .../src/server/accessors/ExperimentalRead.ts | 13 +++---------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/experimental.ts b/apps/meteor/app/apps/server/bridges/experimental.ts index ddeb565108a0c..d505a54d5665d 100644 --- a/apps/meteor/app/apps/server/bridges/experimental.ts +++ b/apps/meteor/app/apps/server/bridges/experimental.ts @@ -2,13 +2,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { ExperimentalBridge } from '@rocket.chat/apps-engine/server/bridges'; export class AppExperimentalBridge extends ExperimentalBridge { - constructor(private readonly orch: IAppServerOrchestrator) { + constructor(protected readonly orch: IAppServerOrchestrator) { super(); - - this.keepContext(); - } - - private keepContext(): void { - void this.orch; } } diff --git a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts index 1e2c4c14ab02f..20922502163ec 100644 --- a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts +++ b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts @@ -3,14 +3,7 @@ import type { ExperimentalBridge } from '../bridges'; export class ExperimentalRead implements IExperimentalRead { constructor( - private readonly experimentalBridge: ExperimentalBridge, - private readonly appId: string, - ) { - this.keepContext(); - } - - private keepContext(): void { - void this.experimentalBridge; - void this.appId; - } + protected readonly experimentalBridge: ExperimentalBridge, + protected readonly appId: string, + ) {} } From 7441191b9759dd20525e4d73f3775576a6b8dd5e Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 19 Nov 2025 08:49:40 -0300 Subject: [PATCH 14/15] Update .changeset/calm-falcons-gather.md --- .changeset/calm-falcons-gather.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/calm-falcons-gather.md b/.changeset/calm-falcons-gather.md index 71762fc9f5003..563fe2ad3a948 100644 --- a/.changeset/calm-falcons-gather.md +++ b/.changeset/calm-falcons-gather.md @@ -3,4 +3,4 @@ "@rocket.chat/meteor": minor --- -Graduates the `getUserRoomIds` accessor from the experimental bridge to the stable user bridge after the experiment proved successful so apps can rely on the API under the standard user read permission. +Adds the `getUserRoomIds` method to the `UserRead` accessor in the Apps-Engine, graduating it from the experimental bridge to the stable user bridge. From 45a749e03bb4dd2bf0c43b5b14d4d89a04b45921 Mon Sep 17 00:00:00 2001 From: Dnouv Date: Wed, 19 Nov 2025 17:36:56 +0530 Subject: [PATCH 15/15] Address review feedback --- apps/meteor/app/apps/server/bridges/users.ts | 2 +- packages/apps-engine/src/definition/accessors/IUserRead.ts | 2 +- packages/apps-engine/src/server/accessors/UserRead.ts | 2 +- packages/apps-engine/src/server/bridges/UserBridge.ts | 4 ++-- packages/apps-engine/src/server/permissions/AppPermissions.ts | 3 +++ packages/apps-engine/tests/test-data/bridges/userBridge.ts | 2 +- packages/i18n/src/locales/en.i18n.json | 1 + 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 531d8cda1962a..3c27eab902521 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -169,7 +169,7 @@ export class AppUserBridge extends UserBridge { return Subscriptions.getBadgeCount(uid); } - protected async getUserRoomIds(userId: string, appId: string): Promise { + protected async getUserRoomIds(userId: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the room ids for the user: "${userId}"`); const subscriptions = await Subscriptions.findByUserId(userId, { projection: { rid: 1 } }).toArray(); diff --git a/packages/apps-engine/src/definition/accessors/IUserRead.ts b/packages/apps-engine/src/definition/accessors/IUserRead.ts index deaf5b908a96f..1106a2d046dc4 100644 --- a/packages/apps-engine/src/definition/accessors/IUserRead.ts +++ b/packages/apps-engine/src/definition/accessors/IUserRead.ts @@ -25,5 +25,5 @@ export interface IUserRead { * * @param userId the user whose memberships should be returned */ - getUserRoomIds(userId: string): Promise; + getUserRoomIds(userId: string): Promise; } diff --git a/packages/apps-engine/src/server/accessors/UserRead.ts b/packages/apps-engine/src/server/accessors/UserRead.ts index c3c916bef1172..a147a51b3bfaa 100644 --- a/packages/apps-engine/src/server/accessors/UserRead.ts +++ b/packages/apps-engine/src/server/accessors/UserRead.ts @@ -24,7 +24,7 @@ export class UserRead implements IUserRead { return this.userBridge.doGetUserUnreadMessageCount(uid, this.appId); } - public getUserRoomIds(userId: string): Promise { + public getUserRoomIds(userId: string): Promise { return this.userBridge.doGetUserRoomIds(userId, this.appId); } } diff --git a/packages/apps-engine/src/server/bridges/UserBridge.ts b/packages/apps-engine/src/server/bridges/UserBridge.ts index a4b8c1b118a4a..72cf52eb17c1d 100644 --- a/packages/apps-engine/src/server/bridges/UserBridge.ts +++ b/packages/apps-engine/src/server/bridges/UserBridge.ts @@ -45,7 +45,7 @@ export abstract class UserBridge extends BaseBridge { } } - public async doGetUserRoomIds(userId: string, appId: string): Promise { + public async doGetUserRoomIds(userId: string, appId: string): Promise { if (this.hasReadPermission(appId)) { return this.getUserRoomIds(userId, appId); } @@ -73,7 +73,7 @@ export abstract class UserBridge extends BaseBridge { protected abstract getUserUnreadMessageCount(uid: string, appId: string): Promise; - protected abstract getUserRoomIds(userId: string, appId: string): Promise; + protected abstract getUserRoomIds(userId: string, appId: string): Promise; /** * Creates a user. diff --git a/packages/apps-engine/src/server/permissions/AppPermissions.ts b/packages/apps-engine/src/server/permissions/AppPermissions.ts index 04e4acfa0612a..483235286aa29 100644 --- a/packages/apps-engine/src/server/permissions/AppPermissions.ts +++ b/packages/apps-engine/src/server/permissions/AppPermissions.ts @@ -122,6 +122,9 @@ export const AppPermissions = { 'outboundComms': { provide: { name: 'outbound-communication.provide' }, }, + 'experimental': { + default: { name: 'experimental.default' }, + }, }; /** diff --git a/packages/apps-engine/tests/test-data/bridges/userBridge.ts b/packages/apps-engine/tests/test-data/bridges/userBridge.ts index d5b845d9b01b6..0153487c392f8 100644 --- a/packages/apps-engine/tests/test-data/bridges/userBridge.ts +++ b/packages/apps-engine/tests/test-data/bridges/userBridge.ts @@ -38,7 +38,7 @@ export class TestsUserBridge extends UserBridge { throw new Error('Method not implemented.'); } - protected getUserRoomIds(userId: string, appId: string): Promise { + protected getUserRoomIds(userId: string, appId: string): Promise { throw new Error('Method not implemented.'); } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 7991ab6c17650..65f40d13338fe 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -631,6 +631,7 @@ "Apps_Permissions_message_read": "Access messages", "Apps_Permissions_message_write": "Send and modify messages", "Apps_Permissions_networking": "Access to this server network", + "Apps_Permissions_experimental_default": "Use experimental APIs", "Apps_Permissions_persistence": "Store internal data in the database", "Apps_Permissions_room_read": "Access room information", "Apps_Permissions_room_write": "Create and modify rooms",