@@ -159,14 +159,18 @@ import {
159159 startTerminalSession
160160} from "./services/terminal-sessions.js"
161161import {
162+ connectSkillerWeb ,
162163 openSkiller ,
163164 openSkillerForTerminalSession ,
164165 parseSkillerRoute ,
165166 proxySkillerTrpc ,
166167 readSkillerProjectContext ,
167168 serveSkillerApp
168169} from "./services/skiller.js"
169- import { resolveDockerGitSkillerBackendUrl } from "./services/skiller-core.js"
170+ import {
171+ isSkillerWebCorsOriginAllowed ,
172+ resolveDockerGitSkillerBackendUrl
173+ } from "./services/skiller-core.js"
170174import {
171175 commitStateFromRequest ,
172176 initStateFromRequest ,
@@ -230,6 +234,11 @@ const AuthTerminalSessionParamsSchema = Schema.Struct({
230234 sessionId : Schema . String
231235} )
232236
237+ const SkillerConnectRequestSchema = Schema . Struct ( {
238+ projectKey : Schema . String ,
239+ sessionId : Schema . optional ( Schema . String )
240+ } )
241+
233242type ApiError =
234243 | ApiAuthRequiredError
235244 | ApiBadRequestError
@@ -448,6 +457,7 @@ const readCodexAuthLoginRequest = () => HttpServerRequest.schemaBodyJson(CodexAu
448457const readGrokAuthLogoutRequest = ( ) => HttpServerRequest . schemaBodyJson ( GrokAuthLogoutRequestSchema )
449458const readCodexAuthLogoutRequest = ( ) => HttpServerRequest . schemaBodyJson ( CodexAuthLogoutRequestSchema )
450459const readProjectAuthRequest = ( ) => HttpServerRequest . schemaBodyJson ( ProjectAuthRequestSchema )
460+ const readSkillerConnectRequest = ( ) => HttpServerRequest . schemaBodyJson ( SkillerConnectRequestSchema )
451461const readProjectPromptUpdateRequest = ( ) => HttpServerRequest . schemaBodyJson ( ProjectPromptUpdateRequestSchema )
452462const readProjectSkillUpdateRequest = ( ) => HttpServerRequest . schemaBodyJson ( ProjectSkillUpdateRequestSchema )
453463const readActiveProjectTerminalSessionRequest = ( ) =>
@@ -601,6 +611,66 @@ const resolveRequestOrigin = (request: HttpServerRequest.HttpServerRequest): str
601611const resolveSkillerBackendUrl = ( request : HttpServerRequest . HttpServerRequest ) : string =>
602612 resolveDockerGitSkillerBackendUrl ( process . env , resolveRequestOrigin ( request ) )
603613
614+ const skillerCorsHeaders = (
615+ request : HttpServerRequest . HttpServerRequest
616+ ) : Record < string , string > => {
617+ const origin = readHeader ( request , "origin" )
618+ if ( origin === undefined || ! isSkillerWebCorsOriginAllowed ( origin , process . env ) ) {
619+ return { }
620+ }
621+ return {
622+ "access-control-allow-credentials" : "true" ,
623+ "access-control-allow-headers" : readHeader ( request , "access-control-request-headers" ) ??
624+ "content-type,trpc-accept,x-trpc-source" ,
625+ "access-control-allow-methods" : "GET,POST,OPTIONS" ,
626+ "access-control-allow-origin" : origin ,
627+ "access-control-max-age" : "600" ,
628+ "access-control-expose-headers" : "content-type" ,
629+ vary : "origin"
630+ }
631+ }
632+
633+ const withSkillerCors = (
634+ request : HttpServerRequest . HttpServerRequest ,
635+ response : HttpServerResponse . HttpServerResponse
636+ ) : HttpServerResponse . HttpServerResponse => {
637+ const headers = skillerCorsHeaders ( request )
638+ return Object . keys ( headers ) . length === 0 ? response : HttpServerResponse . setHeaders ( response , headers )
639+ }
640+
641+ const skillerJsonResponse = (
642+ request : HttpServerRequest . HttpServerRequest ,
643+ data : unknown ,
644+ status : number
645+ ) =>
646+ jsonResponse ( data , status ) . pipe (
647+ Effect . map ( ( response ) => withSkillerCors ( request , response ) )
648+ )
649+
650+ const skillerErrorResponse = (
651+ request : HttpServerRequest . HttpServerRequest ,
652+ error : unknown
653+ ) =>
654+ errorResponse ( error ) . pipe (
655+ Effect . map ( ( response ) => withSkillerCors ( request , response ) )
656+ )
657+
658+ const isSkillerCorsPath = ( pathname : string ) : boolean => {
659+ const normalized = pathname . startsWith ( "/api/" ) ? pathname . slice ( "/api" . length ) : pathname
660+ return normalized === "/skiller/connect" || parseSkillerRoute ( pathname ) !== null
661+ }
662+
663+ const skillerCorsPreflightResponse = (
664+ request : HttpServerRequest . HttpServerRequest
665+ ) => {
666+ const origin = readHeader ( request , "origin" )
667+ const allowed = origin === undefined || isSkillerWebCorsOriginAllowed ( origin , process . env )
668+ return Effect . succeed ( HttpServerResponse . empty ( {
669+ headers : allowed ? skillerCorsHeaders ( request ) : noStoreHeaders ,
670+ status : allowed ? 204 : 403
671+ } ) )
672+ }
673+
604674const resolveFederationContext = (
605675 request : HttpServerRequest . HttpServerRequest ,
606676 requestedDomain ?: string | undefined
@@ -776,11 +846,25 @@ const terminalWebSocketUpgradeResponse = Effect.gen(function*(_) {
776846const projectProxyResponse = Effect . gen ( function * ( _ ) {
777847 const request = yield * _ ( HttpServerRequest . HttpServerRequest )
778848 const pathname = new URL ( request . url , "http://localhost" ) . pathname
849+ if ( request . method === "OPTIONS" && isSkillerCorsPath ( pathname ) ) {
850+ return yield * _ ( skillerCorsPreflightResponse ( request ) )
851+ }
779852 const skillerRoute = parseSkillerRoute ( pathname )
780853 if ( skillerRoute !== null ) {
781- return skillerRoute . _tag === "App"
782- ? yield * _ ( serveSkillerApp ( skillerRoute ) )
783- : yield * _ ( proxySkillerTrpc ( request , skillerRoute ) )
854+ if ( skillerRoute . _tag === "App" ) {
855+ return yield * _ (
856+ serveSkillerApp ( skillerRoute ) . pipe (
857+ Effect . map ( ( response ) => withSkillerCors ( request , response ) ) ,
858+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
859+ )
860+ )
861+ }
862+ return yield * _ (
863+ proxySkillerTrpc ( request , skillerRoute ) . pipe (
864+ Effect . map ( ( response ) => withSkillerCors ( request , response ) ) ,
865+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
866+ )
867+ )
784868 }
785869 const browserTarget = parseProjectBrowserProxyPath ( pathname )
786870 if ( browserTarget !== null ) {
@@ -805,6 +889,38 @@ const projectProxyResponse = Effect.gen(function*(_) {
805889 return yield * _ ( proxyProjectPortForward ( request , target ) )
806890} )
807891
892+ const normalizedOptionalString = ( value : string | undefined ) : string | undefined => {
893+ const trimmed = value ?. trim ( )
894+ return trimmed === undefined || trimmed . length === 0 ? undefined : trimmed
895+ }
896+
897+ const skillerConnectInfoResponse = (
898+ request : HttpServerRequest . HttpServerRequest
899+ ) =>
900+ listProjects ( ) . pipe (
901+ Effect . flatMap ( ( projects ) => skillerJsonResponse ( request , { ok : true , projects } , 200 ) ) ,
902+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
903+ )
904+
905+ const skillerConnectResponse = (
906+ request : HttpServerRequest . HttpServerRequest
907+ ) =>
908+ Effect . gen ( function * ( _ ) {
909+ const body = yield * _ ( readSkillerConnectRequest ( ) )
910+ const projectKey = body . projectKey . trim ( )
911+ if ( projectKey . length === 0 ) {
912+ return yield * _ ( Effect . fail ( new ApiBadRequestError ( { message : "projectKey is required." } ) ) )
913+ }
914+ const connection = yield * _ ( connectSkillerWeb (
915+ projectKey ,
916+ normalizedOptionalString ( body . sessionId ) ,
917+ resolveSkillerBackendUrl ( request )
918+ ) )
919+ return yield * _ ( skillerJsonResponse ( request , { ok : true , ...connection } , 202 ) )
920+ } ) . pipe (
921+ Effect . catchAll ( ( error ) => skillerErrorResponse ( request , error ) )
922+ )
923+
808924export const makeRouter = ( ) => {
809925 const withCoreRoutes = HttpRouter . empty . pipe (
810926 HttpRouter . get (
@@ -815,6 +931,34 @@ export const makeRouter = () => {
815931 return yield * _ ( jsonResponse ( { ok : true , revision : controllerRevision , cwd, projectsRoot } , 200 ) )
816932 } ) . pipe ( Effect . catchAll ( errorResponse ) )
817933 ) ,
934+ HttpRouter . get (
935+ "/skiller/connect" ,
936+ Effect . gen ( function * ( _ ) {
937+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
938+ return yield * _ ( skillerConnectInfoResponse ( request ) )
939+ } )
940+ ) ,
941+ HttpRouter . get (
942+ "/api/skiller/connect" ,
943+ Effect . gen ( function * ( _ ) {
944+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
945+ return yield * _ ( skillerConnectInfoResponse ( request ) )
946+ } )
947+ ) ,
948+ HttpRouter . post (
949+ "/skiller/connect" ,
950+ Effect . gen ( function * ( _ ) {
951+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
952+ return yield * _ ( skillerConnectResponse ( request ) )
953+ } )
954+ ) ,
955+ HttpRouter . post (
956+ "/api/skiller/connect" ,
957+ Effect . gen ( function * ( _ ) {
958+ const request = yield * _ ( HttpServerRequest . HttpServerRequest )
959+ return yield * _ ( skillerConnectResponse ( request ) )
960+ } )
961+ ) ,
818962 HttpRouter . post (
819963 "/skiller/open" ,
820964 Effect . gen ( function * ( _ ) {
0 commit comments