@@ -813,14 +813,20 @@ const bootstrapBrowserSession = (
813813 } ;
814814 } ) ;
815815
816- const bootstrapBearerSession = ( credential = defaultDesktopBootstrapToken ) =>
816+ const bootstrapBearerSession = (
817+ credential = defaultDesktopBootstrapToken ,
818+ options ?: {
819+ readonly headers ?: Record < string , string > ;
820+ } ,
821+ ) =>
817822 Effect . gen ( function * ( ) {
818823 const bootstrapUrl = yield * getHttpServerUrl ( "/api/auth/bootstrap/bearer" ) ;
819824 const response = yield * Effect . promise ( ( ) =>
820825 fetch ( bootstrapUrl , {
821826 method : "POST" ,
822827 headers : {
823828 "content-type" : "application/json" ,
829+ ...options ?. headers ,
824830 } ,
825831 body : JSON . stringify ( {
826832 credential,
@@ -896,6 +902,22 @@ const splitHeaderTokens = (value: string | null) =>
896902 . filter ( ( token ) => token . length > 0 )
897903 . toSorted ( ) ;
898904
905+ const assertBrowserApiCorsHeaders = ( headers : Headers ) => {
906+ assert . equal ( headers . get ( "access-control-allow-origin" ) , "*" ) ;
907+ assert . deepEqual ( splitHeaderTokens ( headers . get ( "access-control-allow-methods" ) ) , [
908+ "GET" ,
909+ "OPTIONS" ,
910+ "POST" ,
911+ ] ) ;
912+ assert . deepEqual ( splitHeaderTokens ( headers . get ( "access-control-allow-headers" ) ) , [
913+ "authorization" ,
914+ "b3" ,
915+ "content-type" ,
916+ "traceparent" ,
917+ ] ) ;
918+ } ;
919+ const crossOriginClientOrigin = "http://remote-client.test:3773" ;
920+
899921const getWsServerUrl = (
900922 pathname = "" ,
901923 options ?: { authenticated ?: boolean ; credential ?: string } ,
@@ -1017,6 +1039,28 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
10171039 } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
10181040 ) ;
10191041
1042+ it . effect ( "includes CORS headers on public environment descriptor responses" , ( ) =>
1043+ Effect . gen ( function * ( ) {
1044+ yield * buildAppUnderTest ( ) ;
1045+
1046+ const url = yield * getHttpServerUrl ( "/.well-known/t3/environment" ) ;
1047+ const response = yield * Effect . promise ( ( ) =>
1048+ fetch ( url , {
1049+ headers : {
1050+ origin : crossOriginClientOrigin ,
1051+ } ,
1052+ } ) ,
1053+ ) ;
1054+ const body = ( yield * Effect . promise ( ( ) =>
1055+ response . json ( ) ,
1056+ ) ) as typeof testEnvironmentDescriptor ;
1057+
1058+ assert . equal ( response . status , 200 ) ;
1059+ assertBrowserApiCorsHeaders ( response . headers ) ;
1060+ assert . deepEqual ( body , testEnvironmentDescriptor ) ;
1061+ } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
1062+ ) ;
1063+
10201064 it . effect ( "reports unauthenticated session state without requiring auth" , ( ) =>
10211065 Effect . gen ( function * ( ) {
10221066 yield * buildAppUnderTest ( ) ;
@@ -1140,6 +1184,62 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
11401184 } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
11411185 ) ;
11421186
1187+ it . effect ( "includes CORS headers on remote auth success responses" , ( ) =>
1188+ Effect . gen ( function * ( ) {
1189+ yield * buildAppUnderTest ( ) ;
1190+
1191+ const origin = crossOriginClientOrigin ;
1192+ const { response : bootstrapResponse , body : bootstrapBody } = yield * bootstrapBearerSession (
1193+ defaultDesktopBootstrapToken ,
1194+ {
1195+ headers : { origin } ,
1196+ } ,
1197+ ) ;
1198+
1199+ assert . equal ( bootstrapResponse . status , 200 ) ;
1200+ assertBrowserApiCorsHeaders ( bootstrapResponse . headers ) ;
1201+ assert . equal ( bootstrapBody . authenticated , true ) ;
1202+ assert . equal ( typeof bootstrapBody . sessionToken , "string" ) ;
1203+
1204+ const sessionUrl = yield * getHttpServerUrl ( "/api/auth/session" ) ;
1205+ const sessionResponse = yield * Effect . promise ( ( ) =>
1206+ fetch ( sessionUrl , {
1207+ headers : {
1208+ authorization : `Bearer ${ bootstrapBody . sessionToken ?? "" } ` ,
1209+ origin,
1210+ } ,
1211+ } ) ,
1212+ ) ;
1213+ const sessionBody = ( yield * Effect . promise ( ( ) => sessionResponse . json ( ) ) ) as {
1214+ readonly authenticated : boolean ;
1215+ readonly sessionMethod ?: string ;
1216+ } ;
1217+
1218+ assert . equal ( sessionResponse . status , 200 ) ;
1219+ assertBrowserApiCorsHeaders ( sessionResponse . headers ) ;
1220+ assert . equal ( sessionBody . authenticated , true ) ;
1221+ assert . equal ( sessionBody . sessionMethod , "bearer-session-token" ) ;
1222+
1223+ const wsTokenUrl = yield * getHttpServerUrl ( "/api/auth/ws-token" ) ;
1224+ const wsTokenResponse = yield * Effect . promise ( ( ) =>
1225+ fetch ( wsTokenUrl , {
1226+ method : "POST" ,
1227+ headers : {
1228+ authorization : `Bearer ${ bootstrapBody . sessionToken ?? "" } ` ,
1229+ origin,
1230+ } ,
1231+ } ) ,
1232+ ) ;
1233+ const wsTokenBody = ( yield * Effect . promise ( ( ) => wsTokenResponse . json ( ) ) ) as {
1234+ readonly token : string ;
1235+ } ;
1236+
1237+ assert . equal ( wsTokenResponse . status , 200 ) ;
1238+ assertBrowserApiCorsHeaders ( wsTokenResponse . headers ) ;
1239+ assert . equal ( typeof wsTokenBody . token , "string" ) ;
1240+ } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
1241+ ) ;
1242+
11431243 it . effect (
11441244 "responds to remote auth websocket-token preflight requests with authorization CORS headers" ,
11451245 ( ) =>
@@ -1151,26 +1251,15 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
11511251 fetch ( wsTokenUrl , {
11521252 method : "OPTIONS" ,
11531253 headers : {
1154- origin : "http://192.168.86.35:3773" ,
1254+ origin : crossOriginClientOrigin ,
11551255 "access-control-request-method" : "POST" ,
11561256 "access-control-request-headers" : "authorization" ,
11571257 } ,
11581258 } ) ,
11591259 ) ;
11601260
11611261 assert . equal ( response . status , 204 ) ;
1162- assert . equal ( response . headers . get ( "access-control-allow-origin" ) , "*" ) ;
1163- assert . deepEqual ( splitHeaderTokens ( response . headers . get ( "access-control-allow-methods" ) ) , [
1164- "GET" ,
1165- "OPTIONS" ,
1166- "POST" ,
1167- ] ) ;
1168- assert . deepEqual ( splitHeaderTokens ( response . headers . get ( "access-control-allow-headers" ) ) , [
1169- "authorization" ,
1170- "b3" ,
1171- "content-type" ,
1172- "traceparent" ,
1173- ] ) ;
1262+ assertBrowserApiCorsHeaders ( response . headers ) ;
11741263 } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
11751264 ) ;
11761265
@@ -1183,7 +1272,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
11831272 fetch ( wsTokenUrl , {
11841273 method : "POST" ,
11851274 headers : {
1186- origin : "http://192.168.86.35:3773" ,
1275+ origin : crossOriginClientOrigin ,
11871276 } ,
11881277 } ) ,
11891278 ) ;
@@ -1192,7 +1281,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
11921281 } ;
11931282
11941283 assert . equal ( response . status , 401 ) ;
1195- assert . equal ( response . headers . get ( "access-control-allow-origin" ) , "*" ) ;
1284+ assertBrowserApiCorsHeaders ( response . headers ) ;
11961285 assert . equal ( body . error , "Authentication required." ) ;
11971286 } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
11981287 ) ;
0 commit comments