@@ -7,8 +7,13 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
77import { jsonResult } from "../../agents/tools/common.js" ;
88import type { ChannelPlugin } from "../../channels/plugins/types.js" ;
99import { setActivePluginRegistry } from "../../plugins/runtime.js" ;
10+ import {
11+ getPluginRuntimeGatewayRequestScope ,
12+ withPluginRuntimeGatewayRequestScope ,
13+ } from "../../plugins/runtime/gateway-request-scope.js" ;
1014import { createTestRegistry } from "../../test-utils/channel-plugins.js" ;
1115import { captureEnv , setTestEnvValue } from "../../test-utils/env.js" ;
16+ import { GATEWAY_CLIENT_MODES , GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js" ;
1217import type { GatewayRequestContext } from "./types.js" ;
1318
1419type ResolveOutboundTarget = typeof import ( "../../infra/outbound/targets.js" ) . resolveOutboundTarget ;
@@ -199,7 +204,12 @@ function createDeferred<T>() {
199204
200205async function runMessageActionRequest (
201206 params : Record < string , unknown > ,
202- client ?: { connect ?: { scopes ?: string [ ] } } | null ,
207+ client ?: {
208+ connect ?: {
209+ scopes ?: string [ ] ;
210+ client ?: { id ?: string ; mode ?: string } ;
211+ } ;
212+ } | null ,
203213) {
204214 const respond = vi . fn ( ) ;
205215 await sendHandlers [ "message.action" ] ( {
@@ -1605,29 +1615,40 @@ describe("gateway send mirroring", () => {
16051615 } ) ,
16061616 ) ;
16071617
1608- const { respond } = await runMessageActionRequest ( {
1609- channel : "whatsapp" ,
1610- action : "react" ,
1611- params : {
1612- chatJid : "+15551234567" ,
1613- messageId : "wamid.1" ,
1614- emoji : "✅" ,
1618+ const { respond } = await runMessageActionRequest (
1619+ {
1620+ channel : "whatsapp" ,
1621+ action : "react" ,
1622+ params : {
1623+ chatJid : "+15551234567" ,
1624+ messageId : "wamid.1" ,
1625+ emoji : "✅" ,
1626+ } ,
1627+ requesterAccountId : "default" ,
1628+ requesterSenderId : "trusted-user" ,
1629+ inboundTurnKind : "room_event" ,
1630+ toolContext : {
1631+ currentMessagingTarget : "user:15551234567" ,
1632+ currentGraphChannelId : "graph:team/chan" ,
1633+ currentChannelProvider : "whatsapp" ,
1634+ currentMessageId : "wamid.1" ,
1635+ replyToMode : "first" ,
1636+ hasRepliedRef : { value : true } ,
1637+ sameChannelThreadRequired : true ,
1638+ skipCrossContextDecoration : true ,
1639+ } ,
1640+ idempotencyKey : "idem-message-action" ,
16151641 } ,
1616- requesterAccountId : "default" ,
1617- requesterSenderId : "trusted-user" ,
1618- inboundTurnKind : "room_event" ,
1619- toolContext : {
1620- currentMessagingTarget : "user:15551234567" ,
1621- currentGraphChannelId : "graph:team/chan" ,
1622- currentChannelProvider : "whatsapp" ,
1623- currentMessageId : "wamid.1" ,
1624- replyToMode : "first" ,
1625- hasRepliedRef : { value : true } ,
1626- sameChannelThreadRequired : true ,
1627- skipCrossContextDecoration : true ,
1642+ {
1643+ connect : {
1644+ scopes : [ "operator.admin" ] ,
1645+ client : {
1646+ id : GATEWAY_CLIENT_NAMES . GATEWAY_CLIENT ,
1647+ mode : GATEWAY_CLIENT_MODES . BACKEND ,
1648+ } ,
1649+ } ,
16281650 } ,
1629- idempotencyKey : "idem-message-action" ,
1630- } ) ;
1651+ ) ;
16311652
16321653 expect ( respond ) . toHaveBeenCalledWith (
16331654 true ,
@@ -1651,6 +1672,142 @@ describe("gateway send mirroring", () => {
16511672 expect . objectContaining ( {
16521673 inboundEventKind : "room_event" ,
16531674 requesterAccountId : "default" ,
1675+ requesterSenderId : "trusted-user" ,
1676+ gatewayClientScopes : [ "operator.write" ] ,
1677+ } ) ,
1678+ ) ;
1679+ } ) ;
1680+
1681+ it ( "drops caller-supplied requester identity from operator.write message actions" , async ( ) => {
1682+ const reactPlugin : ChannelPlugin = {
1683+ id : "whatsapp" ,
1684+ meta : {
1685+ id : "whatsapp" ,
1686+ label : "WhatsApp" ,
1687+ selectionLabel : "WhatsApp" ,
1688+ docsPath : "/channels/whatsapp" ,
1689+ blurb : "WhatsApp action dispatch test plugin." ,
1690+ } ,
1691+ capabilities : { chatTypes : [ "direct" ] , reactions : true } ,
1692+ config : {
1693+ listAccountIds : ( ) => [ "default" ] ,
1694+ resolveAccount : ( ) => ( { enabled : true } ) ,
1695+ isConfigured : ( ) => true ,
1696+ } ,
1697+ actions : {
1698+ describeMessageTool : ( ) => ( { actions : [ "react" ] } ) ,
1699+ supportsAction : ( { action } ) => action === "react" ,
1700+ handleAction : async ( ) => jsonResult ( { ok : true } ) ,
1701+ } ,
1702+ } ;
1703+ mocks . getChannelPlugin . mockReturnValue ( reactPlugin ) ;
1704+ setActivePluginRegistry (
1705+ createTestRegistry ( [ { pluginId : "whatsapp" , source : "test" , plugin : reactPlugin } ] ) ,
1706+ "send-test-message-action-untrusted-requester" ,
1707+ ) ;
1708+ mocks . dispatchChannelMessageAction . mockResolvedValueOnce ( jsonResult ( { ok : true } ) ) ;
1709+
1710+ const { respond } = await runMessageActionRequest (
1711+ {
1712+ channel : "whatsapp" ,
1713+ action : "react" ,
1714+ params : {
1715+ chatJid : "+15551234567" ,
1716+ messageId : "wamid.1" ,
1717+ emoji : "✅" ,
1718+ } ,
1719+ requesterAccountId : "default" ,
1720+ requesterSenderId : "spoofed-admin-user" ,
1721+ senderIsOwner : true ,
1722+ idempotencyKey : "idem-message-action-untrusted-requester" ,
1723+ } ,
1724+ { connect : { scopes : [ "operator.write" ] } } ,
1725+ ) ;
1726+
1727+ expect ( firstRespondCall ( respond ) [ 0 ] ) . toBe ( true ) ;
1728+ expect ( lastDispatchChannelMessageActionCall ( ) ) . toEqual (
1729+ expect . objectContaining ( {
1730+ requesterAccountId : undefined ,
1731+ requesterSenderId : undefined ,
1732+ senderIsOwner : false ,
1733+ gatewayClientScopes : [ "operator.write" ] ,
1734+ } ) ,
1735+ ) ;
1736+ } ) ;
1737+
1738+ it ( "down-scopes backend bridge actions that carry explicit non-owner provenance" , async ( ) => {
1739+ const reactPlugin : ChannelPlugin = {
1740+ id : "whatsapp" ,
1741+ meta : {
1742+ id : "whatsapp" ,
1743+ label : "WhatsApp" ,
1744+ selectionLabel : "WhatsApp" ,
1745+ docsPath : "/channels/whatsapp" ,
1746+ blurb : "WhatsApp action dispatch test plugin." ,
1747+ } ,
1748+ capabilities : { chatTypes : [ "direct" ] , reactions : true } ,
1749+ config : {
1750+ listAccountIds : ( ) => [ "default" ] ,
1751+ resolveAccount : ( ) => ( { enabled : true } ) ,
1752+ isConfigured : ( ) => true ,
1753+ } ,
1754+ actions : {
1755+ describeMessageTool : ( ) => ( { actions : [ "react" ] } ) ,
1756+ supportsAction : ( { action } ) => action === "react" ,
1757+ handleAction : async ( ) => jsonResult ( { ok : true } ) ,
1758+ } ,
1759+ } ;
1760+ mocks . getChannelPlugin . mockReturnValue ( reactPlugin ) ;
1761+ setActivePluginRegistry (
1762+ createTestRegistry ( [ { pluginId : "whatsapp" , source : "test" , plugin : reactPlugin } ] ) ,
1763+ "send-test-message-action-backend-non-owner" ,
1764+ ) ;
1765+ let scopedGatewayClientScopes : readonly string [ ] | undefined ;
1766+ mocks . dispatchChannelMessageAction . mockImplementationOnce ( async ( ) => {
1767+ scopedGatewayClientScopes =
1768+ getPluginRuntimeGatewayRequestScope ( ) ?. client ?. connect ?. scopes ?? [ ] ;
1769+ return jsonResult ( { ok : true } ) ;
1770+ } ) ;
1771+
1772+ const backendClient = {
1773+ connect : {
1774+ scopes : [ "operator.admin" ] ,
1775+ client : {
1776+ id : GATEWAY_CLIENT_NAMES . GATEWAY_CLIENT ,
1777+ mode : GATEWAY_CLIENT_MODES . BACKEND ,
1778+ } ,
1779+ } ,
1780+ } ;
1781+ const { respond } = await withPluginRuntimeGatewayRequestScope (
1782+ {
1783+ client : backendClient as never ,
1784+ isWebchatConnect : ( ) => false ,
1785+ } ,
1786+ async ( ) =>
1787+ await runMessageActionRequest (
1788+ {
1789+ channel : "whatsapp" ,
1790+ action : "react" ,
1791+ params : {
1792+ chatJid : "+15551234567" ,
1793+ messageId : "wamid.1" ,
1794+ emoji : "✅" ,
1795+ } ,
1796+ senderIsOwner : false ,
1797+ idempotencyKey : "idem-message-action-backend-non-owner" ,
1798+ } ,
1799+ backendClient ,
1800+ ) ,
1801+ ) ;
1802+
1803+ expect ( firstRespondCall ( respond ) [ 0 ] ) . toBe ( true ) ;
1804+ expect ( scopedGatewayClientScopes ) . toEqual ( [ "operator.write" ] ) ;
1805+ expect ( lastDispatchChannelMessageActionCall ( ) ) . toEqual (
1806+ expect . objectContaining ( {
1807+ requesterAccountId : undefined ,
1808+ requesterSenderId : undefined ,
1809+ senderIsOwner : false ,
1810+ gatewayClientScopes : [ "operator.write" ] ,
16541811 } ) ,
16551812 ) ;
16561813 } ) ;
0 commit comments