@@ -24,6 +24,10 @@ function createHarness(params?: {
2424 cryptoAvailable ?: boolean ;
2525 selfUserId ?: string ;
2626 selfUserIdError ?: Error ;
27+ allowFrom ?: string [ ] ;
28+ dmEnabled ?: boolean ;
29+ dmPolicy ?: "open" | "pairing" | "allowlist" | "disabled" ;
30+ storeAllowFrom ?: string [ ] ;
2731 joinedMembersByRoom ?: Record < string , string [ ] > ;
2832 verifications ?: Array < {
2933 id : string ;
@@ -67,6 +71,7 @@ function createHarness(params?: {
6771 const logger = { info : vi . fn ( ) , warn : vi . fn ( ) , error : vi . fn ( ) } ;
6872 const formatNativeDependencyHint = vi . fn ( ( ) => "install hint" ) ;
6973 const logVerboseMessage = vi . fn ( ) ;
74+ const readStoreAllowFrom = vi . fn ( async ( ) => params ?. storeAllowFrom ?? [ ] ) ;
7075 const client = {
7176 on : vi . fn ( ( eventName : string , listener : ( ...args : unknown [ ] ) => void ) => {
7277 listeners . set ( eventName , listener ) ;
@@ -101,6 +106,10 @@ function createHarness(params?: {
101106 accountId : params ?. accountId ?? "default" ,
102107 encryption : params ?. authEncryption ?? true ,
103108 } as MatrixAuth ,
109+ allowFrom : params ?. allowFrom ?? [ ] ,
110+ dmEnabled : params ?. dmEnabled ?? true ,
111+ dmPolicy : params ?. dmPolicy ?? "open" ,
112+ readStoreAllowFrom,
104113 directTracker : {
105114 invalidateRoom,
106115 } ,
@@ -123,6 +132,7 @@ function createHarness(params?: {
123132 invalidateRoom,
124133 roomEventListener,
125134 listVerifications,
135+ readStoreAllowFrom,
126136 logger,
127137 formatNativeDependencyHint,
128138 logVerboseMessage,
@@ -255,6 +265,112 @@ describe("registerMatrixMonitorEvents verification routing", () => {
255265 expect ( body ) . toContain ( 'Open "Verify by emoji"' ) ;
256266 } ) ;
257267
268+ it ( "blocks verification request notices when dmPolicy pairing would block the sender" , async ( ) => {
269+ const { onRoomMessage, sendMessage, roomMessageListener, logVerboseMessage } = createHarness ( {
270+ dmPolicy : "pairing" ,
271+ } ) ;
272+ if ( ! roomMessageListener ) {
273+ throw new Error ( "room.message listener was not registered" ) ;
274+ }
275+
276+ roomMessageListener ( "!room:example.org" , {
277+ event_id : "$req-pairing-blocked" ,
278+ sender : "@alice:example.org" ,
279+ type : EventType . RoomMessage ,
280+ origin_server_ts : Date . now ( ) ,
281+ content : {
282+ msgtype : "m.key.verification.request" ,
283+ body : "verification request" ,
284+ } ,
285+ } ) ;
286+
287+ await vi . waitFor ( ( ) => {
288+ expect ( logVerboseMessage ) . toHaveBeenCalledWith (
289+ expect . stringContaining ( "blocked verification sender @alice:example.org" ) ,
290+ ) ;
291+ } ) ;
292+ expect ( sendMessage ) . not . toHaveBeenCalled ( ) ;
293+ expect ( onRoomMessage ) . not . toHaveBeenCalled ( ) ;
294+ } ) ;
295+
296+ it ( "allows verification notices for pairing-authorized DM senders from the allow store" , async ( ) => {
297+ const { sendMessage, roomMessageListener, readStoreAllowFrom } = createHarness ( {
298+ dmPolicy : "pairing" ,
299+ storeAllowFrom : [ "@alice:example.org" ] ,
300+ } ) ;
301+ if ( ! roomMessageListener ) {
302+ throw new Error ( "room.message listener was not registered" ) ;
303+ }
304+
305+ roomMessageListener ( "!room:example.org" , {
306+ event_id : "$req-pairing-allowed" ,
307+ sender : "@alice:example.org" ,
308+ type : EventType . RoomMessage ,
309+ origin_server_ts : Date . now ( ) ,
310+ content : {
311+ msgtype : "m.key.verification.request" ,
312+ body : "verification request" ,
313+ } ,
314+ } ) ;
315+
316+ await vi . waitFor ( ( ) => {
317+ expect ( sendMessage ) . toHaveBeenCalledTimes ( 1 ) ;
318+ } ) ;
319+ expect ( readStoreAllowFrom ) . toHaveBeenCalled ( ) ;
320+ } ) ;
321+
322+ it ( "does not consult the allow store when dmPolicy is open" , async ( ) => {
323+ const { sendMessage, roomMessageListener, readStoreAllowFrom } = createHarness ( {
324+ dmPolicy : "open" ,
325+ } ) ;
326+ if ( ! roomMessageListener ) {
327+ throw new Error ( "room.message listener was not registered" ) ;
328+ }
329+
330+ roomMessageListener ( "!room:example.org" , {
331+ event_id : "$req-open-policy" ,
332+ sender : "@alice:example.org" ,
333+ type : EventType . RoomMessage ,
334+ origin_server_ts : Date . now ( ) ,
335+ content : {
336+ msgtype : "m.key.verification.request" ,
337+ body : "verification request" ,
338+ } ,
339+ } ) ;
340+
341+ await vi . waitFor ( ( ) => {
342+ expect ( sendMessage ) . toHaveBeenCalledTimes ( 1 ) ;
343+ } ) ;
344+ expect ( readStoreAllowFrom ) . not . toHaveBeenCalled ( ) ;
345+ } ) ;
346+
347+ it ( "blocks verification notices when Matrix DMs are disabled" , async ( ) => {
348+ const { sendMessage, roomMessageListener, logVerboseMessage } = createHarness ( {
349+ dmEnabled : false ,
350+ } ) ;
351+ if ( ! roomMessageListener ) {
352+ throw new Error ( "room.message listener was not registered" ) ;
353+ }
354+
355+ roomMessageListener ( "!room:example.org" , {
356+ event_id : "$req-dm-disabled" ,
357+ sender : "@alice:example.org" ,
358+ type : EventType . RoomMessage ,
359+ origin_server_ts : Date . now ( ) ,
360+ content : {
361+ msgtype : "m.key.verification.request" ,
362+ body : "verification request" ,
363+ } ,
364+ } ) ;
365+
366+ await vi . waitFor ( ( ) => {
367+ expect ( logVerboseMessage ) . toHaveBeenCalledWith (
368+ expect . stringContaining ( "blocked verification sender @alice:example.org" ) ,
369+ ) ;
370+ } ) ;
371+ expect ( sendMessage ) . not . toHaveBeenCalled ( ) ;
372+ } ) ;
373+
258374 it ( "posts ready-stage guidance for emoji verification" , async ( ) => {
259375 const { sendMessage, roomEventListener } = createHarness ( ) ;
260376 roomEventListener ( "!room:example.org" , {
@@ -423,6 +539,51 @@ describe("registerMatrixMonitorEvents verification routing", () => {
423539 expect ( body ) . toContain ( "SAS decimal: 6158 1986 3513" ) ;
424540 } ) ;
425541
542+ it ( "blocks summary SAS notices when dmPolicy allowlist would block the sender" , async ( ) => {
543+ const { sendMessage, verificationSummaryListener, logVerboseMessage } = createHarness ( {
544+ dmPolicy : "allowlist" ,
545+ joinedMembersByRoom : {
546+ "!dm:example.org" : [ "@alice:example.org" , "@bot:example.org" ] ,
547+ } ,
548+ } ) ;
549+ if ( ! verificationSummaryListener ) {
550+ throw new Error ( "verification.summary listener was not registered" ) ;
551+ }
552+
553+ verificationSummaryListener ( {
554+ id : "verification-blocked-summary" ,
555+ roomId : "!dm:example.org" ,
556+ otherUserId : "@alice:example.org" ,
557+ isSelfVerification : false ,
558+ initiatedByMe : false ,
559+ phase : 3 ,
560+ phaseName : "started" ,
561+ pending : true ,
562+ methods : [ "m.sas.v1" ] ,
563+ canAccept : false ,
564+ hasSas : true ,
565+ sas : {
566+ decimal : [ 6158 , 1986 , 3513 ] ,
567+ emoji : [
568+ [ "🎁" , "Gift" ] ,
569+ [ "🌍" , "Globe" ] ,
570+ [ "🐴" , "Horse" ] ,
571+ ] ,
572+ } ,
573+ hasReciprocateQr : false ,
574+ completed : false ,
575+ createdAt : new Date ( "2026-02-25T21:42:54.000Z" ) . toISOString ( ) ,
576+ updatedAt : new Date ( "2026-02-25T21:42:55.000Z" ) . toISOString ( ) ,
577+ } ) ;
578+
579+ await vi . waitFor ( ( ) => {
580+ expect ( logVerboseMessage ) . toHaveBeenCalledWith (
581+ expect . stringContaining ( "blocked verification sender @alice:example.org" ) ,
582+ ) ;
583+ } ) ;
584+ expect ( sendMessage ) . not . toHaveBeenCalled ( ) ;
585+ } ) ;
586+
426587 it ( "posts SAS notices from summary updates using the room mapped by earlier flow events" , async ( ) => {
427588 const { sendMessage, roomEventListener, verificationSummaryListener } = createHarness ( {
428589 joinedMembersByRoom : {
0 commit comments