@@ -187,6 +187,7 @@ function attachGatewayHarness(options: {
187187 refreshHealthSnapshot ?: GatewayRequestContext [ "refreshHealthSnapshot" ] ;
188188 requestOrigin ?: string ;
189189 requestHost ?: string ;
190+ headers ?: Record < string , string > ;
190191 remoteAddr ?: string ;
191192 localAddr ?: string ;
192193 resolvedAuth ?: ResolvedGatewayAuth ;
@@ -220,12 +221,14 @@ function attachGatewayHarness(options: {
220221 allowTailscale : false ,
221222 } ;
222223 const advanceHandshakePhase = vi . fn ( ) ;
224+ const logWsControl = createLogger ( ) ;
223225 attachGatewayWsMessageHandler ( {
224226 socket,
225227 upgradeReq : {
226228 headers : {
227229 host : requestHost ,
228230 ...( options . requestOrigin ? { origin : options . requestOrigin } : { } ) ,
231+ ...options . headers ,
229232 } ,
230233 socket : { localAddress : localAddr , remoteAddress : remoteAddr } ,
231234 } as unknown as IncomingMessage ,
@@ -259,14 +262,15 @@ function attachGatewayHarness(options: {
259262 originCheckMetrics : { hostHeaderFallbackAccepted : 0 } ,
260263 logGateway : createLogger ( ) as never ,
261264 logHealth : createLogger ( ) as never ,
262- logWsControl : createLogger ( ) as never ,
265+ logWsControl : logWsControl as never ,
263266 } ) ;
264267 if ( onMessage === undefined ) {
265268 throw new Error ( "expected websocket message handler" ) ;
266269 }
267270 const sendMessage = onMessage ;
268271 return {
269272 advanceHandshakePhase,
273+ logWsControl,
270274 send,
271275 socketSend,
272276 sendRequest : ( id : string , method : string , params : Record < string , unknown > = { } ) => {
@@ -529,6 +533,131 @@ describe("attachGatewayWsMessageHandler post-connect health refresh", () => {
529533 resolveRefresh ?.( ) ;
530534 } ) ;
531535
536+ it ( "projects trusted-proxy identity into presence and the connected client" , async ( ) => {
537+ loadConfigMock . mockImplementationOnce ( ( ) => ( {
538+ gateway : {
539+ auth : {
540+ mode : "trusted-proxy" ,
541+ trustedProxy : {
542+ userHeader : "x-forwarded-user" ,
543+ requiredHeaders : [ "x-forwarded-proto" ] ,
544+ } ,
545+ } ,
546+ trustedProxies : [ "10.0.0.1" ] ,
547+ controlUi : {
548+ allowedOrigins : [ "http://127.0.0.1:19001" ] ,
549+ dangerouslyDisableDeviceAuth : true ,
550+ } ,
551+ } ,
552+ } ) ) ;
553+ const harness = attachGatewayHarness ( {
554+ connId : "conn-trusted-proxy-user" ,
555+ connectNonce : "nonce-trusted-proxy-user" ,
556+ requestHost : "gateway.example.com:18789" ,
557+ requestOrigin : "http://127.0.0.1:19001" ,
558+ remoteAddr : "10.0.0.1" ,
559+ resolvedAuth : {
560+ mode : "trusted-proxy" ,
561+ allowTailscale : false ,
562+ trustedProxy : {
563+ userHeader : "x-forwarded-user" ,
564+ requiredHeaders : [ "x-forwarded-proto" ] ,
565+ } ,
566+ } ,
567+ headers : {
568+ "x-forwarded-user" :
"[email protected] " , 569+ "x-forwarded-proto" : "https" ,
570+ } ,
571+ } ) ;
572+
573+ harness . sendConnect ( "connect-trusted-proxy-user" , {
574+ minProtocol : PROTOCOL_VERSION ,
575+ maxProtocol : PROTOCOL_VERSION ,
576+ client : {
577+ id : "openclaw-control-ui" ,
578+ version : "dev" ,
579+ platform : "test" ,
580+ mode : "ui" ,
581+ } ,
582+ role : "operator" ,
583+ caps : [ ] ,
584+ } ) ;
585+
586+ await waitForFast ( ( ) => {
587+ expect ( harness . socketSend . mock . calls . length + harness . send . mock . calls . length ) . toBeGreaterThan (
588+ 0 ,
589+ ) ;
590+ } ) ;
591+ const trustedProxyHello = harness . socketSend . mock . calls . at ( 0 ) ?. [ 0 ] ;
592+ expect (
593+ typeof trustedProxyHello === "string"
594+ ? JSON . parse ( trustedProxyHello )
595+ : harness . send . mock . calls . at ( 0 ) ?. [ 0 ] ,
596+ ) . toMatchObject ( {
597+ ok : true ,
598+ } ) ;
599+ await waitForFast ( ( ) => {
600+ expect ( upsertPresenceMock ) . toHaveBeenCalledWith (
601+ "conn-trusted-proxy-user" ,
602+ expect . objectContaining ( {
603+ 604+ } ) ,
605+ ) ;
606+ } ) ;
607+ expect ( harness . client ) . toMatchObject ( { authenticatedUserId :
"[email protected] " } ) ; 608+ expect ( harness . logWsControl . info ) . toHaveBeenCalledWith (
609+ "authenticated user connected conn=conn-trusted-proxy-user [email protected] " , 610+ ) ;
611+ } ) ;
612+
613+ it ( "keeps token-authenticated presence free of user identity" , async ( ) => {
614+ const harness = attachGatewayHarness ( {
615+ connId : "conn-token-userless" ,
616+ connectNonce : "nonce-token-userless" ,
617+ requestHost : "gateway.example.com:18789" ,
618+ requestOrigin : "http://127.0.0.1:19001" ,
619+ remoteAddr : "203.0.113.50" ,
620+ resolvedAuth : {
621+ mode : "token" ,
622+ token : "gateway-token" ,
623+ allowTailscale : false ,
624+ } ,
625+ } ) ;
626+
627+ harness . sendConnect ( "connect-token-userless" , {
628+ minProtocol : PROTOCOL_VERSION ,
629+ maxProtocol : PROTOCOL_VERSION ,
630+ client : {
631+ id : "openclaw-control-ui" ,
632+ version : "dev" ,
633+ platform : "test" ,
634+ mode : "ui" ,
635+ } ,
636+ role : "operator" ,
637+ caps : [ ] ,
638+ auth : { token : "gateway-token" } ,
639+ } ) ;
640+
641+ await waitForFast ( ( ) => {
642+ expect ( harness . socketSend . mock . calls . length + harness . send . mock . calls . length ) . toBeGreaterThan (
643+ 0 ,
644+ ) ;
645+ } ) ;
646+ const tokenHello = harness . socketSend . mock . calls . at ( 0 ) ?. [ 0 ] ;
647+ expect (
648+ typeof tokenHello === "string" ? JSON . parse ( tokenHello ) : harness . send . mock . calls . at ( 0 ) ?. [ 0 ] ,
649+ ) . toMatchObject ( {
650+ ok : true ,
651+ } ) ;
652+ await waitForFast ( ( ) => {
653+ expect ( upsertPresenceMock ) . toHaveBeenCalledWith (
654+ "conn-token-userless" ,
655+ expect . not . objectContaining ( { user : expect . anything ( ) } ) ,
656+ ) ;
657+ } ) ;
658+ expect ( harness . client ) . not . toMatchObject ( { authenticatedUserId : expect . anything ( ) } ) ;
659+ } ) ;
660+
532661 it ( "emits a security event for rejected gateway auth" , async ( ) => {
533662 const close = createCloseMock ( ) ;
534663 const harness = attachGatewayHarness ( {
0 commit comments