@@ -159,12 +159,18 @@ import {
159159 startTerminalSession
160160} from "./services/terminal-sessions.js"
161161import {
162+ connectSkillerWeb ,
162163 openSkiller ,
163164 openSkillerForTerminalSession ,
164165 parseSkillerRoute ,
165166 proxySkillerTrpc ,
167+ readSkillerProjectContext ,
166168 serveSkillerApp
167169} from "./services/skiller.js"
170+ import {
171+ isSkillerWebCorsOriginAllowed ,
172+ resolveDockerGitSkillerBackendUrl
173+ } from "./services/skiller-core.js"
168174import {
169175 commitStateFromRequest ,
170176 initStateFromRequest ,
@@ -228,6 +234,11 @@ const AuthTerminalSessionParamsSchema = Schema.Struct({
228234 sessionId : Schema . String
229235} )
230236
237+ const SkillerConnectRequestSchema = Schema . Struct ( {
238+ projectKey : Schema . String ,
239+ sessionId : Schema . optional ( Schema . String )
240+ } )
241+
231242type ApiError =
232243 | ApiAuthRequiredError
233244 | ApiBadRequestError
@@ -446,6 +457,7 @@ const readCodexAuthLoginRequest = () => HttpServerRequest.schemaBodyJson(CodexAu
446457const readGrokAuthLogoutRequest = ( ) => HttpServerRequest . schemaBodyJson ( GrokAuthLogoutRequestSchema )
447458const readCodexAuthLogoutRequest = ( ) => HttpServerRequest . schemaBodyJson ( CodexAuthLogoutRequestSchema )
448459const readProjectAuthRequest = ( ) => HttpServerRequest . schemaBodyJson ( ProjectAuthRequestSchema )
460+ const readSkillerConnectRequest = ( ) => HttpServerRequest . schemaBodyJson ( SkillerConnectRequestSchema )
449461const readProjectPromptUpdateRequest = ( ) => HttpServerRequest . schemaBodyJson ( ProjectPromptUpdateRequestSchema )
450462const readProjectSkillUpdateRequest = ( ) => HttpServerRequest . schemaBodyJson ( ProjectSkillUpdateRequestSchema )
451463const readActiveProjectTerminalSessionRequest = ( ) =>
@@ -596,6 +608,80 @@ const resolveRequestOrigin = (request: HttpServerRequest.HttpServerRequest): str
596608 return `${ proto } ://${ host } `
597609}
598610
611+ const resolveSkillerBackendUrl = ( request : HttpServerRequest . HttpServerRequest ) : string =>
612+ resolveDockerGitSkillerBackendUrl ( process . env , resolveRequestOrigin ( request ) )
613+
614+ const isPrivateNetworkCorsRequest = (
615+ request : HttpServerRequest . HttpServerRequest
616+ ) : boolean =>
617+ readHeader ( request , "access-control-request-private-network" ) ?. toLowerCase ( ) === "true"
618+
619+ const skillerCorsHeaders = (
620+ request : HttpServerRequest . HttpServerRequest
621+ ) : Record < string , string > => {
622+ const origin = readHeader ( request , "origin" )
623+ if ( origin === undefined || ! isSkillerWebCorsOriginAllowed ( origin , process . env ) ) {
624+ return { }
625+ }
626+ const privateNetworkHeaders = isPrivateNetworkCorsRequest ( request )
627+ ? { "access-control-allow-private-network" : "true" }
628+ : { }
629+ return {
630+ ...privateNetworkHeaders ,
631+ "access-control-allow-credentials" : "true" ,
632+ "access-control-allow-headers" : readHeader ( request , "access-control-request-headers" ) ??
633+ "content-type,trpc-accept,x-trpc-source" ,
634+ "access-control-allow-methods" : "GET,POST,OPTIONS" ,
635+ "access-control-allow-origin" : origin ,
636+ "access-control-max-age" : "600" ,
637+ "access-control-expose-headers" : "content-type" ,
638+ vary : "origin, access-control-request-private-network"
639+ }
640+ }
641+
642+ const withSkillerCors = (
643+ request : HttpServerRequest . HttpServerRequest ,
644+ response : HttpServerResponse . HttpServerResponse
645+ ) : HttpServerResponse . HttpServerResponse => {
646+ const headers = skillerCorsHeaders ( request )
647+ return Object . keys ( headers ) . length === 0 ? response : HttpServerResponse . setHeaders ( response , headers )
648+ }
649+
650+ const skillerJsonResponse = (
651+ request : HttpServerRequest . HttpServerRequest ,
652+ data : unknown ,
653+ status : number
654+ ) =>
655+ jsonResponse ( data , status ) . pipe (
656+ Effect . map ( ( response ) => withSkillerCors ( request , response ) )
657+ )
658+
659+ const skillerErrorResponse = (
660+ request : HttpServerRequest . HttpServerRequest ,
661+ error : unknown
662+ ) =>
663+ errorResponse ( error ) . pipe (
664+ Effect . map ( ( response ) => withSkillerCors ( request , response ) )
665+ )
666+
667+ const isSkillerCorsPath = ( pathname : string ) : boolean => {
668+ const normalized = pathname . startsWith ( "/api/" ) ? pathname . slice ( "/api" . length ) : pathname
669+ return normalized === "/skiller/connect" ||
670+ / ^ \/ p r o j e c t s \/ b y - k e y \/ [ ^ / ] + (?: \/ t e r m i n a l - s e s s i o n s \/ [ ^ / ] + ) ? \/ s k i l l e r \/ c o n t e x t $ / u. test ( normalized ) ||
671+ parseSkillerRoute ( pathname ) !== null
672+ }
673+
674+ const skillerCorsPreflightResponse = (
675+ request : HttpServerRequest . HttpServerRequest
676+ ) => {
677+ const origin = readHeader ( request , "origin" )
678+ const allowed = origin === undefined || isSkillerWebCorsOriginAllowed ( origin , process . env )
679+ return Effect . succeed ( HttpServerResponse . empty ( {
680+ headers : allowed ? skillerCorsHeaders ( request ) : noStoreHeaders ,
681+ status : allowed ? 204 : 403
682+ } ) )
683+ }
684+
599685const resolveFederationContext = (
600686 request : HttpServerRequest . HttpServerRequest ,
601687 requestedDomain ?: string | undefined
@@ -771,11 +857,25 @@ const terminalWebSocketUpgradeResponse = Effect.gen(function*(_) {
771857const projectProxyResponse = Effect . gen ( function * ( _ ) {
772858 const request = yield * _ ( HttpServerRequest . HttpServerRequest )
773859 const pathname = new URL ( request . url , "http://localhost" ) . pathname
860+ if ( request . method === "OPTIONS" && isSkillerCorsPath ( pathname ) ) {
861+ return yield * _ ( skillerCorsPreflightResponse ( request ) )
862+ }
774863 const skillerRoute = parseSkillerRoute ( pathname )
775864 if ( skillerRoute !== null ) {
776- return skillerRoute . _tag === "App"
777- ? yield * _ ( serveSkillerApp ( skillerRoute ) )
778- : yield * _ ( proxySkillerTrpc ( request , skillerRoute ) )
865+ if ( skillerRoute . _tag === "App" ) {
866+ return yield * _ (
867+ serveSkillerApp ( skillerRoute ) . pipe (
868+ Effect . map ( ( response ) => withSkillerCors ( request , response ) ) ,
869+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
870+ )
871+ )
872+ }
873+ return yield * _ (
874+ proxySkillerTrpc ( request , skillerRoute ) . pipe (
875+ Effect . map ( ( response ) => withSkillerCors ( request , response ) ) ,
876+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
877+ )
878+ )
779879 }
780880 const browserTarget = parseProjectBrowserProxyPath ( pathname )
781881 if ( browserTarget !== null ) {
@@ -800,6 +900,38 @@ const projectProxyResponse = Effect.gen(function*(_) {
800900 return yield * _ ( proxyProjectPortForward ( request , target ) )
801901} )
802902
903+ const normalizedOptionalString = ( value : string | undefined ) : string | undefined => {
904+ const trimmed = value ?. trim ( )
905+ return trimmed === undefined || trimmed . length === 0 ? undefined : trimmed
906+ }
907+
908+ const skillerConnectInfoResponse = (
909+ request : HttpServerRequest . HttpServerRequest
910+ ) =>
911+ listProjects ( ) . pipe (
912+ Effect . flatMap ( ( projects ) => skillerJsonResponse ( request , { ok : true , projects } , 200 ) ) ,
913+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
914+ )
915+
916+ const skillerConnectResponse = (
917+ request : HttpServerRequest . HttpServerRequest
918+ ) =>
919+ Effect . gen ( function * ( _ ) {
920+ const body = yield * _ ( readSkillerConnectRequest ( ) )
921+ const projectKey = body . projectKey . trim ( )
922+ if ( projectKey . length === 0 ) {
923+ return yield * _ ( Effect . fail ( new ApiBadRequestError ( { message : "projectKey is required." } ) ) )
924+ }
925+ const connection = yield * _ ( connectSkillerWeb (
926+ projectKey ,
927+ normalizedOptionalString ( body . sessionId ) ,
928+ resolveSkillerBackendUrl ( request )
929+ ) )
930+ return yield * _ ( skillerJsonResponse ( request , { ok : true , ...connection } , 202 ) )
931+ } ) . pipe (
932+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
933+ )
934+
803935export const makeRouter = ( ) => {
804936 const withCoreRoutes = HttpRouter . empty . pipe (
805937 HttpRouter . get (
@@ -810,28 +942,85 @@ export const makeRouter = () => {
810942 return yield * _ ( jsonResponse ( { ok : true , revision : controllerRevision , cwd, projectsRoot } , 200 ) )
811943 } ) . pipe ( Effect . catchAll ( errorResponse ) )
812944 ) ,
945+ HttpRouter . get (
946+ "/skiller/connect" ,
947+ Effect . gen ( function * ( _ ) {
948+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
949+ return yield * _ ( skillerConnectInfoResponse ( request ) )
950+ } )
951+ ) ,
952+ HttpRouter . get (
953+ "/api/skiller/connect" ,
954+ Effect . gen ( function * ( _ ) {
955+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
956+ return yield * _ ( skillerConnectInfoResponse ( request ) )
957+ } )
958+ ) ,
959+ HttpRouter . post (
960+ "/skiller/connect" ,
961+ Effect . gen ( function * ( _ ) {
962+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
963+ return yield * _ ( skillerConnectResponse ( request ) )
964+ } )
965+ ) ,
966+ HttpRouter . post (
967+ "/api/skiller/connect" ,
968+ Effect . gen ( function * ( _ ) {
969+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
970+ return yield * _ ( skillerConnectResponse ( request ) )
971+ } )
972+ ) ,
813973 HttpRouter . post (
814974 "/skiller/open" ,
815- openSkiller ( ) . pipe (
816- Effect . flatMap ( ( launch ) => jsonResponse ( { ok : true , ...launch } , 202 ) ) ,
817- Effect . catchAll ( errorResponse )
818- )
975+ Effect . gen ( function * ( _ ) {
976+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
977+ const launch = yield * _ ( openSkiller ( undefined , undefined , resolveSkillerBackendUrl ( request ) ) )
978+ return yield * _ ( jsonResponse ( { ok : true , ...launch } , 202 ) )
979+ } ) . pipe ( Effect . catchAll ( errorResponse ) )
819980 ) ,
820981 HttpRouter . post (
821982 "/projects/by-key/:projectKey/skiller/open" ,
822- projectKeyParams . pipe (
823- Effect . flatMap ( ( { projectKey } ) => openSkiller ( projectKey ) ) ,
824- Effect . flatMap ( ( launch ) => jsonResponse ( { ok : true , ...launch } , 202 ) ) ,
825- Effect . catchAll ( errorResponse )
826- )
983+ Effect . gen ( function * ( _ ) {
984+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
985+ const { projectKey } = yield * _ ( projectKeyParams )
986+ const launch = yield * _ ( openSkiller ( projectKey , undefined , resolveSkillerBackendUrl ( request ) ) )
987+ return yield * _ ( jsonResponse ( { ok : true , ...launch } , 202 ) )
988+ } ) . pipe ( Effect . catchAll ( errorResponse ) )
827989 ) ,
828990 HttpRouter . post (
829991 "/projects/by-key/:projectKey/terminal-sessions/:sessionId/skiller/open" ,
830- terminalSessionByProjectKeyParams . pipe (
831- Effect . flatMap ( ( { projectKey, sessionId } ) => openSkillerForTerminalSession ( projectKey , sessionId ) ) ,
832- Effect . flatMap ( ( launch ) => jsonResponse ( { ok : true , ...launch } , 202 ) ) ,
833- Effect . catchAll ( errorResponse )
834- )
992+ Effect . gen ( function * ( _ ) {
993+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
994+ const { projectKey, sessionId } = yield * _ ( terminalSessionByProjectKeyParams )
995+ const launch = yield * _ ( openSkillerForTerminalSession ( projectKey , sessionId , resolveSkillerBackendUrl ( request ) ) )
996+ return yield * _ ( jsonResponse ( { ok : true , ...launch } , 202 ) )
997+ } ) . pipe ( Effect . catchAll ( errorResponse ) )
998+ ) ,
999+ HttpRouter . get (
1000+ "/projects/by-key/:projectKey/skiller/context" ,
1001+ Effect . gen ( function * ( _ ) {
1002+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
1003+ return yield * _ (
1004+ projectKeyParams . pipe (
1005+ Effect . flatMap ( ( { projectKey } ) => readSkillerProjectContext ( projectKey , null ) ) ,
1006+ Effect . flatMap ( ( context ) => skillerJsonResponse ( request , { ok : true , ...context } , 200 ) ) ,
1007+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
1008+ )
1009+ )
1010+ } )
1011+ ) ,
1012+ HttpRouter . get (
1013+ "/projects/by-key/:projectKey/terminal-sessions/:sessionId/skiller/context" ,
1014+ Effect . gen ( function * ( _ ) {
1015+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
1016+ return yield * _ (
1017+ terminalSessionByProjectKeyParams . pipe (
1018+ Effect . flatMap ( ( { projectKey, sessionId } ) => readSkillerProjectContext ( projectKey , sessionId ) ) ,
1019+ Effect . flatMap ( ( context ) => skillerJsonResponse ( request , { ok : true , ...context } , 200 ) ) ,
1020+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
1021+ )
1022+ )
1023+ } )
8351024 ) ,
8361025 HttpRouter . get (
8371026 "/cloudflare-tunnels/panel" ,
0 commit comments