diff --git a/packages/twenty-server/src/engine/core-modules/server-route-trigger/__tests__/server-route-trigger.service.spec.ts b/packages/twenty-server/src/engine/core-modules/server-route-trigger/__tests__/server-route-trigger.service.spec.ts index b61ef4479e..a791cea707 100644 --- a/packages/twenty-server/src/engine/core-modules/server-route-trigger/__tests__/server-route-trigger.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/server-route-trigger/__tests__/server-route-trigger.service.spec.ts @@ -67,7 +67,7 @@ describe('ServerRouteTriggerService', () => { workspaceId: 'owner-ws', serverRouteTriggerSettings: { forwardedRequestHeaders: ['x-test'] }, application: { - applicationRegistration: { ownerWorkspaceId: 'owner-ws' }, + applicationRegistration: { id: 'reg-1', ownerWorkspaceId: 'owner-ws' }, }, ...overrides, }); @@ -159,14 +159,20 @@ describe('ServerRouteTriggerService', () => { id: 'tenant-copy', workspaceId: 'tenant-ws', application: { - applicationRegistration: { ownerWorkspaceId: 'owner-ws' }, + applicationRegistration: { + id: 'reg-1', + ownerWorkspaceId: 'owner-ws', + }, }, }), buildResolverRow({ id: 'owner-copy', workspaceId: 'owner-ws', application: { - applicationRegistration: { ownerWorkspaceId: 'owner-ws' }, + applicationRegistration: { + id: 'reg-1', + ownerWorkspaceId: 'owner-ws', + }, }, }), // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -310,4 +316,53 @@ describe('ServerRouteTriggerService', () => { code: ServerRouteTriggerExceptionCode.RATE_LIMIT_EXCEEDED, }); }); + + it('scopes the target lookup to the resolver application registration', async () => { + await handle(); + + expect(logicFunctionRepository.findOne).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + where: expect.objectContaining({ + universalIdentifier: TARGET_UID, + workspaceId: 'target-ws', + application: { applicationRegistrationId: 'reg-1' }, + }), + }), + ); + }); + + it('throws LOGIC_FUNCTION_NOT_FOUND when the resolver is not linked to an application registration', async () => { + logicFunctionRepository.find.mockResolvedValue([ + buildResolverRow({ + application: { + applicationRegistration: { ownerWorkspaceId: 'owner-ws' }, + }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any); + + await expect(handle()).rejects.toMatchObject({ + code: ServerRouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND, + }); + }); + + it('does not leak the raw executor error message to the caller', async () => { + logicFunctionExecutorService.execute.mockReset(); + logicFunctionExecutorService.execute + .mockResolvedValueOnce( + buildExecuteResult({ + workspaceId: 'target-ws', + targetLogicFunctionUniversalIdentifier: TARGET_UID, + }), + ) + .mockRejectedValueOnce( + new Error('internal: connection to lambda-internal:5000 refused'), + ); + + await expect(handle()).rejects.toMatchObject({ + code: ServerRouteTriggerExceptionCode.SERVER_ROUTE_PLATFORM_ERROR, + message: 'An unexpected error occurred while handling the server route', + }); + }); }); diff --git a/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.service.ts b/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.service.ts index 82c8b8ecf8..99bb8c0859 100644 --- a/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.service.ts +++ b/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.service.ts @@ -57,6 +57,16 @@ export class ServerRouteTriggerService { ); } + const applicationRegistrationId = + resolver.application?.applicationRegistration?.id; + + if (!isDefined(applicationRegistrationId)) { + throw new ServerRouteTriggerException( + `Server resolver function ${resolverLogicFunctionUniversalIdentifier} is not linked to an application registration`, + ServerRouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND, + ); + } + const event = buildLogicFunctionEvent({ request, pathParameters: {}, @@ -77,6 +87,7 @@ export class ServerRouteTriggerService { resolved.targetLogicFunctionUniversalIdentifier, workspaceId: resolved.workspaceId, payload: resolved.payload ?? event, + applicationRegistrationId, }); if (isDefined(targetResult.error)) { @@ -151,16 +162,24 @@ export class ServerRouteTriggerService { logicFunctionUniversalIdentifier, workspaceId, payload, + applicationRegistrationId, }: { logicFunctionUniversalIdentifier: string; workspaceId: string; payload: object; + applicationRegistrationId?: string; }): Promise<{ data: object | null; error?: { errorMessage: string } }> { const logicFunction = await this.logicFunctionRepository.findOne({ where: { universalIdentifier: logicFunctionUniversalIdentifier, workspaceId, + ...(isDefined(applicationRegistrationId) + ? { application: { applicationRegistrationId } } + : {}), }, + ...(isDefined(applicationRegistrationId) + ? { relations: { application: true } } + : {}), }); if (!isDefined(logicFunction)) { @@ -181,13 +200,28 @@ export class ServerRouteTriggerService { `Server logic function ${logicFunction.id} failed in workspace ${workspaceId}: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : undefined, ); + const code = this.mapExecutorErrorToServerRouteCode(error); + throw new ServerRouteTriggerException( - error instanceof Error ? error.message : String(error), - this.mapExecutorErrorToServerRouteCode(error), + this.getPublicErrorMessageForCode(code), + code, ); } } + private getPublicErrorMessageForCode( + code: ServerRouteTriggerExceptionCode, + ): string { + switch (code) { + case ServerRouteTriggerExceptionCode.RATE_LIMIT_EXCEEDED: + return 'Rate limit exceeded'; + case ServerRouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND: + return 'Logic function not found'; + default: + return 'An unexpected error occurred while handling the server route'; + } + } + private mapExecutorErrorToServerRouteCode( error: unknown, ): ServerRouteTriggerExceptionCode {