@@ -48,15 +48,15 @@ describe("handleControlUiHttpRequest", () => {
4848 expect ( params . end ) . toHaveBeenCalledWith ( "Not Found" ) ;
4949 }
5050
51- function runControlUiRequest ( params : {
51+ async function runControlUiRequest ( params : {
5252 url : string ;
5353 method : "GET" | "HEAD" | "POST" ;
5454 rootPath : string ;
5555 basePath ?: string ;
5656 rootKind ?: "resolved" | "bundled" ;
5757 } ) {
5858 const { res, end } = makeMockHttpResponse ( ) ;
59- const handled = handleControlUiHttpRequest (
59+ const handled = await handleControlUiHttpRequest (
6060 { url : params . url , method : params . method } as IncomingMessage ,
6161 res ,
6262 {
@@ -67,6 +67,33 @@ describe("handleControlUiHttpRequest", () => {
6767 return { res, end, handled } ;
6868 }
6969
70+ async function runBootstrapConfigRequest ( params : {
71+ rootPath : string ;
72+ basePath ?: string ;
73+ auth ?: ResolvedGatewayAuth ;
74+ headers ?: IncomingMessage [ "headers" ] ;
75+ } ) {
76+ const { res, end } = makeMockHttpResponse ( ) ;
77+ const url = params . basePath
78+ ? `${ params . basePath } ${ CONTROL_UI_BOOTSTRAP_CONFIG_PATH } `
79+ : CONTROL_UI_BOOTSTRAP_CONFIG_PATH ;
80+ const handled = await handleControlUiHttpRequest (
81+ {
82+ url,
83+ method : "GET" ,
84+ headers : params . headers ?? { } ,
85+ socket : { remoteAddress : "127.0.0.1" } ,
86+ } as IncomingMessage ,
87+ res ,
88+ {
89+ ...( params . basePath ? { basePath : params . basePath } : { } ) ,
90+ ...( params . auth ? { auth : params . auth } : { } ) ,
91+ root : { kind : "resolved" , path : params . rootPath } ,
92+ } ,
93+ ) ;
94+ return { res, end, handled } ;
95+ }
96+
7097 async function runAvatarRequest ( params : {
7198 url : string ;
7299 method : "GET" | "HEAD" ;
@@ -241,7 +268,7 @@ describe("handleControlUiHttpRequest", () => {
241268 await withControlUiRoot ( {
242269 fn : async ( tmp ) => {
243270 const { res, setHeader } = makeMockHttpResponse ( ) ;
244- const handled = handleControlUiHttpRequest (
271+ const handled = await handleControlUiHttpRequest (
245272 { url : "/" , method : "GET" } as IncomingMessage ,
246273 res ,
247274 {
@@ -405,7 +432,7 @@ describe("handleControlUiHttpRequest", () => {
405432 indexHtml : html ,
406433 fn : async ( tmp ) => {
407434 const { res, setHeader } = makeMockHttpResponse ( ) ;
408- handleControlUiHttpRequest ( { url : "/" , method : "GET" } as IncomingMessage , res , {
435+ await handleControlUiHttpRequest ( { url : "/" , method : "GET" } as IncomingMessage , res , {
409436 root : { kind : "resolved" , path : tmp } ,
410437 } ) ;
411438 const cspCalls = setHeader . mock . calls . filter (
@@ -424,7 +451,7 @@ describe("handleControlUiHttpRequest", () => {
424451 indexHtml : html ,
425452 fn : async ( tmp ) => {
426453 const { res, end } = makeMockHttpResponse ( ) ;
427- const handled = handleControlUiHttpRequest (
454+ const handled = await handleControlUiHttpRequest (
428455 { url : "/" , method : "GET" } as IncomingMessage ,
429456 res ,
430457 {
@@ -445,7 +472,7 @@ describe("handleControlUiHttpRequest", () => {
445472 await withControlUiRoot ( {
446473 fn : async ( tmp ) => {
447474 const { res, end } = makeMockHttpResponse ( ) ;
448- const handled = handleControlUiHttpRequest (
475+ const handled = await handleControlUiHttpRequest (
449476 { url : CONTROL_UI_BOOTSTRAP_CONFIG_PATH , method : "GET" } as IncomingMessage ,
450477 res ,
451478 {
@@ -467,11 +494,43 @@ describe("handleControlUiHttpRequest", () => {
467494 } ) ;
468495 } ) ;
469496
497+ it ( "rejects bootstrap config requests without a valid auth token when auth is enabled" , async ( ) => {
498+ await withControlUiRoot ( {
499+ fn : async ( tmp ) => {
500+ const { res, handled, end } = await runBootstrapConfigRequest ( {
501+ rootPath : tmp ,
502+ auth : { mode : "token" , token : "test-token" , allowTailscale : false } ,
503+ } ) ;
504+ expect ( handled ) . toBe ( true ) ;
505+ expect ( res . statusCode ) . toBe ( 401 ) ;
506+ expect ( String ( end . mock . calls [ 0 ] ?. [ 0 ] ?? "" ) ) . toContain ( "Unauthorized" ) ;
507+ } ,
508+ } ) ;
509+ } ) ;
510+
511+ it ( "serves bootstrap config JSON when auth is enabled and the token is valid" , async ( ) => {
512+ await withControlUiRoot ( {
513+ fn : async ( tmp ) => {
514+ const { res, handled, end } = await runBootstrapConfigRequest ( {
515+ rootPath : tmp ,
516+ auth : { mode : "token" , token : "test-token" , allowTailscale : false } ,
517+ headers : {
518+ authorization : "Bearer test-token" ,
519+ } ,
520+ } ) ;
521+ expect ( handled ) . toBe ( true ) ;
522+ expect ( res . statusCode ) . toBe ( 200 ) ;
523+ const parsed = parseBootstrapPayload ( end ) ;
524+ expect ( parsed . assistantAgentId ) . toBe ( "main" ) ;
525+ } ,
526+ } ) ;
527+ } ) ;
528+
470529 it ( "serves bootstrap config JSON under basePath" , async ( ) => {
471530 await withControlUiRoot ( {
472531 fn : async ( tmp ) => {
473532 const { res, end } = makeMockHttpResponse ( ) ;
474- const handled = handleControlUiHttpRequest (
533+ const handled = await handleControlUiHttpRequest (
475534 { url : `/openclaw${ CONTROL_UI_BOOTSTRAP_CONFIG_PATH } ` , method : "GET" } as IncomingMessage ,
476535 res ,
477536 {
@@ -613,7 +672,7 @@ describe("handleControlUiHttpRequest", () => {
613672 await fs . symlink ( outsideFile , path . join ( assetsDir , "leak.txt" ) ) ;
614673
615674 const { res, end } = makeMockHttpResponse ( ) ;
616- const handled = handleControlUiHttpRequest (
675+ const handled = await handleControlUiHttpRequest (
617676 { url : "/assets/leak.txt" , method : "GET" } as IncomingMessage ,
618677 res ,
619678 {
@@ -634,7 +693,7 @@ describe("handleControlUiHttpRequest", () => {
634693 const { assetsDir, filePath } = await writeAssetFile ( tmp , "actual.txt" , "inside-ok\n" ) ;
635694 await fs . symlink ( filePath , path . join ( assetsDir , "linked.txt" ) ) ;
636695
637- const { res, end, handled } = runControlUiRequest ( {
696+ const { res, end, handled } = await runControlUiRequest ( {
638697 url : "/assets/linked.txt" ,
639698 method : "GET" ,
640699 rootPath : tmp ,
@@ -652,7 +711,7 @@ describe("handleControlUiHttpRequest", () => {
652711 fn : async ( tmp ) => {
653712 await writeAssetFile ( tmp , "actual.txt" , "inside-ok\n" ) ;
654713
655- const { res, end, handled } = runControlUiRequest ( {
714+ const { res, end, handled } = await runControlUiRequest ( {
656715 url : "/assets/actual.txt" ,
657716 method : "HEAD" ,
658717 rootPath : tmp ,
@@ -675,7 +734,7 @@ describe("handleControlUiHttpRequest", () => {
675734 await fs . rm ( path . join ( tmp , "index.html" ) ) ;
676735 await fs . symlink ( outsideIndex , path . join ( tmp , "index.html" ) ) ;
677736
678- const { res, end, handled } = runControlUiRequest ( {
737+ const { res, end, handled } = await runControlUiRequest ( {
679738 url : "/app/route" ,
680739 method : "GET" ,
681740 rootPath : tmp ,
@@ -698,7 +757,7 @@ describe("handleControlUiHttpRequest", () => {
698757 await fs . rm ( path . join ( tmp , "index.html" ) ) ;
699758 await fs . link ( outsideIndex , path . join ( tmp , "index.html" ) ) ;
700759
701- const { res, end, handled } = runControlUiRequest ( {
760+ const { res, end, handled } = await runControlUiRequest ( {
702761 url : "/" ,
703762 method : "GET" ,
704763 rootPath : tmp ,
@@ -716,7 +775,7 @@ describe("handleControlUiHttpRequest", () => {
716775 fn : async ( tmp ) => {
717776 await createHardlinkedAssetFile ( tmp ) ;
718777
719- const { res, end, handled } = runControlUiRequest ( {
778+ const { res, end, handled } = await runControlUiRequest ( {
720779 url : "/assets/app.hl.js" ,
721780 method : "GET" ,
722781 rootPath : tmp ,
@@ -734,7 +793,7 @@ describe("handleControlUiHttpRequest", () => {
734793 fn : async ( tmp ) => {
735794 await createHardlinkedAssetFile ( tmp ) ;
736795
737- const { res, end, handled } = runControlUiRequest ( {
796+ const { res, end, handled } = await runControlUiRequest ( {
738797 url : "/assets/app.hl.js" ,
739798 method : "GET" ,
740799 rootPath : tmp ,
@@ -753,7 +812,7 @@ describe("handleControlUiHttpRequest", () => {
753812 fn : async ( tmp ) => {
754813 for ( const webhookPath of [ "/bluebubbles-webhook" , "/custom-webhook" , "/callback" ] ) {
755814 const { res } = makeMockHttpResponse ( ) ;
756- const handled = handleControlUiHttpRequest (
815+ const handled = await handleControlUiHttpRequest (
757816 { url : webhookPath , method : "POST" } as IncomingMessage ,
758817 res ,
759818 { root : { kind : "resolved" , path : tmp } } ,
@@ -770,7 +829,7 @@ describe("handleControlUiHttpRequest", () => {
770829 await withControlUiRoot ( {
771830 fn : async ( tmp ) => {
772831 const { res } = makeMockHttpResponse ( ) ;
773- const handled = handleControlUiHttpRequest (
832+ const handled = await handleControlUiHttpRequest (
774833 { url : "/bluebubbles-webhook" , method : "POST" } as IncomingMessage ,
775834 res ,
776835 { basePath : "/openclaw" , root : { kind : "resolved" , path : tmp } } ,
@@ -784,7 +843,7 @@ describe("handleControlUiHttpRequest", () => {
784843 await withControlUiRoot ( {
785844 fn : async ( tmp ) => {
786845 for ( const apiPath of [ "/api" , "/api/sessions" , "/api/channels/nostr" ] ) {
787- const { handled } = runControlUiRequest ( {
846+ const { handled } = await runControlUiRequest ( {
788847 url : apiPath ,
789848 method : "GET" ,
790849 rootPath : tmp ,
@@ -799,7 +858,7 @@ describe("handleControlUiHttpRequest", () => {
799858 await withControlUiRoot ( {
800859 fn : async ( tmp ) => {
801860 for ( const pluginPath of [ "/plugins" , "/plugins/diffs/view/abc/def" ] ) {
802- const { handled } = runControlUiRequest ( {
861+ const { handled } = await runControlUiRequest ( {
803862 url : pluginPath ,
804863 method : "GET" ,
805864 rootPath : tmp ,
@@ -813,7 +872,7 @@ describe("handleControlUiHttpRequest", () => {
813872 it ( "falls through POST requests when basePath is empty" , async ( ) => {
814873 await withControlUiRoot ( {
815874 fn : async ( tmp ) => {
816- const { handled, end } = runControlUiRequest ( {
875+ const { handled, end } = await runControlUiRequest ( {
817876 url : "/webhook/bluebubbles" ,
818877 method : "POST" ,
819878 rootPath : tmp ,
@@ -828,7 +887,7 @@ describe("handleControlUiHttpRequest", () => {
828887 await withControlUiRoot ( {
829888 fn : async ( tmp ) => {
830889 for ( const route of [ "/openclaw" , "/openclaw/" , "/openclaw/some-page" ] ) {
831- const { handled, end } = runControlUiRequest ( {
890+ const { handled, end } = await runControlUiRequest ( {
832891 url : route ,
833892 method : "POST" ,
834893 rootPath : tmp ,
@@ -850,7 +909,7 @@ describe("handleControlUiHttpRequest", () => {
850909
851910 const secretPathUrl = secretPath . split ( path . sep ) . join ( "/" ) ;
852911 const absolutePathUrl = secretPathUrl . startsWith ( "/" ) ? secretPathUrl : `/${ secretPathUrl } ` ;
853- const { res, end, handled } = runControlUiRequest ( {
912+ const { res, end, handled } = await runControlUiRequest ( {
854913 url : `/openclaw/${ absolutePathUrl } ` ,
855914 method : "GET" ,
856915 rootPath : root ,
@@ -879,7 +938,7 @@ describe("handleControlUiHttpRequest", () => {
879938 throw error ;
880939 }
881940
882- const { res, end, handled } = runControlUiRequest ( {
941+ const { res, end, handled } = await runControlUiRequest ( {
883942 url : "/openclaw/assets/leak.txt" ,
884943 method : "GET" ,
885944 rootPath : root ,
0 commit comments