11// ---------------------------------------------------------------------------
2- // Cloud API — core handlers from @executor/api + cloud-specific plugins + auth
2+ // Cloud API — protected core API + public auth endpoints
33// ---------------------------------------------------------------------------
44
55import {
6+ HttpApi ,
67 HttpApiBuilder ,
78 HttpApiSwagger ,
89 HttpMiddleware ,
910 HttpServer ,
1011} from "@effect/platform" ;
1112import { Effect , Layer } from "effect" ;
13+ import { setCookie } from "@tanstack/react-start/server" ;
1214
1315import { addGroup , CoreHandlers , ExecutorService , ExecutionEngineService } from "@executor/api" ;
1416import { createExecutionEngine } from "@executor/execution" ;
1517import { OpenApiGroup , OpenApiExtensionService , OpenApiHandlers } from "@executor/plugin-openapi/api" ;
1618import { McpGroup , McpExtensionService , McpHandlers } from "@executor/plugin-mcp/api" ;
17- import { GoogleDiscoveryGroup , GoogleDiscoveryExtensionService , GoogleDiscoveryHandlers } from "@executor/plugin-google-discovery/api" ;
19+ import {
20+ GoogleDiscoveryGroup ,
21+ GoogleDiscoveryExtensionService ,
22+ GoogleDiscoveryHandlers ,
23+ } from "@executor/plugin-google-discovery/api" ;
1824import { GraphqlGroup , GraphqlExtensionService , GraphqlHandlers } from "@executor/plugin-graphql/api" ;
1925
20- import { createTeamExecutor } from "./services/executor" ;
21- import { CloudAuthApi } from "./auth/api" ;
22- import { CloudAuthHandlers } from "./auth/handlers" ;
26+ import { CloudAuthApi , CloudAuthPublicApi } from "./auth/api" ;
2327import { AuthContext , UserStoreService } from "./auth/context" ;
28+ import { CloudAuthHandlers , CloudAuthPublicHandlers } from "./auth/handlers" ;
2429import { WorkOSAuth } from "./auth/workos" ;
25- import type { DrizzleDb } from "./services/db" ;
26-
27- // ---------------------------------------------------------------------------
28- // Cloud API — core + cloud plugins + cloud auth (no onepassword)
29- // ---------------------------------------------------------------------------
30+ import { DbService } from "./services/db" ;
31+ import { createTeamExecutor } from "./services/executor" ;
3032
31- const CloudApi = addGroup ( OpenApiGroup )
33+ const ProtectedCloudApi = addGroup ( OpenApiGroup )
3234 . add ( McpGroup )
3335 . add ( GoogleDiscoveryGroup )
3436 . add ( GraphqlGroup )
3537 . add ( CloudAuthApi ) ;
3638
37- const CloudApiBase = HttpApiBuilder . api ( CloudApi ) . pipe (
39+ const PublicCloudApi = HttpApi . make ( "cloudPublic" )
40+ . add ( CloudAuthPublicApi ) ;
41+
42+ const ProtectedCloudApiLive = HttpApiBuilder . api ( ProtectedCloudApi ) . pipe (
3843 Layer . provide ( CoreHandlers ) ,
3944 Layer . provide ( Layer . mergeAll (
4045 OpenApiHandlers ,
@@ -45,100 +50,180 @@ const CloudApiBase = HttpApiBuilder.api(CloudApi).pipe(
4550 ) ) ,
4651) ;
4752
48- // ---------------------------------------------------------------------------
49- // Cookie parser
50- // ---------------------------------------------------------------------------
53+ const PublicCloudApiLive = HttpApiBuilder . api ( PublicCloudApi ) . pipe (
54+ Layer . provide ( CloudAuthPublicHandlers ) ,
55+ ) ;
56+
57+ const DbLive = DbService . Live ;
58+ const UserStoreLive = UserStoreService . Live . pipe (
59+ Layer . provide ( DbLive ) ,
60+ ) ;
61+
62+ const SharedServices = Layer . mergeAll (
63+ DbLive ,
64+ UserStoreLive ,
65+ WorkOSAuth . Default ,
66+ HttpServer . layerContext ,
67+ ) ;
68+
69+ const publicApiHandler = HttpApiBuilder . toWebHandler (
70+ PublicCloudApiLive . pipe (
71+ Layer . provideMerge ( SharedServices ) ,
72+ ) ,
73+ { middleware : HttpMiddleware . logger } ,
74+ ) ;
5175
5276const parseCookie = ( cookieHeader : string | null , name : string ) : string | null => {
5377 if ( ! cookieHeader ) return null ;
5478 const match = cookieHeader
5579 . split ( ";" )
56- . map ( ( c ) => c . trim ( ) )
57- . find ( ( c ) => c . startsWith ( `${ name } =` ) ) ;
80+ . map ( ( value ) => value . trim ( ) )
81+ . find ( ( value ) => value . startsWith ( `${ name } =` ) ) ;
5882 if ( ! match ) return null ;
5983 return match . slice ( name . length + 1 ) || null ;
6084} ;
6185
62- // ---------------------------------------------------------------------------
63- // Create API handler with auth-based executor resolution
64- // ---------------------------------------------------------------------------
86+ const isPublicPath = ( pathname : string ) : boolean =>
87+ pathname === "/auth/login" || pathname === "/auth/callback" ;
6588
66- export const createCloudApiHandler = ( db : DrizzleDb , encryptionKey : string ) => {
67- return async ( request : Request ) : Promise < Response > => {
68- // Authenticate via WorkOS sealed session
69- const auth = await Effect . runPromise (
70- Effect . gen ( function * ( ) {
71- const workos = yield * WorkOSAuth ;
72- return yield * workos . authenticateRequest ( request ) ;
73- } ) . pipe ( Effect . provide ( WorkOSAuth . Default ) ) ,
74- ) ;
89+ const unauthorized = ( message : string ) : Response =>
90+ Response . json ( { error : message } , { status : 401 } ) ;
7591
76- if ( ! auth ) {
77- return Response . json ( { error : "Unauthorized" } , { status : 401 } ) ;
78- }
92+ const COOKIE_OPTIONS = {
93+ path : "/" ,
94+ httpOnly : true ,
95+ sameSite : "lax" as const ,
96+ maxAge : 60 * 60 * 24 * 7 ,
97+ secure : process . env . NODE_ENV === "production" ,
98+ } ;
7999
80- const teamId = parseCookie ( request . headers . get ( "cookie" ) , "executor_team" ) ;
81- if ( ! teamId ) {
82- return Response . json ( { error : "No team selected" } , { status : 401 } ) ;
83- }
100+ const resolveAuth = ( request : Request ) =>
101+ Effect . gen ( function * ( ) {
102+ const workos = yield * WorkOSAuth ;
103+ return yield * workos . authenticateRequest ( request ) ;
104+ } ) . pipe (
105+ Effect . provide ( SharedServices ) ,
106+ Effect . runPromise ,
107+ ) ;
108+
109+ const resolveTeamId = (
110+ auth : {
111+ readonly userId : string ;
112+ readonly email : string ;
113+ readonly firstName : string | null | undefined ;
114+ readonly lastName : string | null | undefined ;
115+ readonly avatarUrl : string | null | undefined ;
116+ } ,
117+ cookieTeamId : string | null ,
118+ ) =>
119+ Effect . gen ( function * ( ) {
120+ if ( cookieTeamId ) return cookieTeamId ;
121+
122+ const users = yield * UserStoreService ;
123+ const teams = yield * users . use ( ( store ) => store . getTeamsForUser ( auth . userId ) ) ;
124+ if ( teams . length > 0 ) return teams [ 0 ] ! . teamId ;
125+
126+ const user = yield * users . use ( ( store ) =>
127+ store . upsertUser ( {
128+ id : auth . userId ,
129+ email : auth . email ,
130+ name : `${ auth . firstName ?? "" } ${ auth . lastName ?? "" } ` . trim ( ) || undefined ,
131+ avatarUrl : auth . avatarUrl ?? undefined ,
132+ } ) ,
133+ ) ;
84134
85- // Resolve team name
86- const userStore = UserStoreService . layer ( db ) ;
87- const team = await Effect . runPromise (
88- Effect . gen ( function * ( ) {
89- const users = yield * UserStoreService ;
90- return yield * users . use ( ( s ) => s . getTeam ( teamId ) ) ;
91- } ) . pipe ( Effect . provide ( userStore ) ) ,
135+ const team = yield * users . use ( ( store ) =>
136+ store . createTeam ( `${ user . name ?? user . email } 's Team` ) ,
92137 ) ;
138+ yield * users . use ( ( store ) => store . addMember ( team . id , user . id , "owner" ) ) ;
139+ return team . id ;
140+ } ) . pipe (
141+ Effect . provide ( SharedServices ) ,
142+ Effect . runPromise ,
143+ ) ;
144+
145+ const resolveExecutor = ( teamId : string ) =>
146+ Effect . gen ( function * ( ) {
147+ const users = yield * UserStoreService ;
148+ const team = yield * users . use ( ( store ) => store . getTeam ( teamId ) ) ;
93149 const teamName = team ?. name ?? "Unknown Team" ;
150+ const encryptionKey = process . env . ENCRYPTION_KEY ?? "local-dev-encryption-key" ;
151+ return yield * createTeamExecutor ( teamId , teamName , encryptionKey ) ;
152+ } ) . pipe (
153+ Effect . provide ( SharedServices ) ,
154+ Effect . runPromise ,
155+ ) ;
156+
157+ type TeamExecutor = Awaited < ReturnType < typeof resolveExecutor > > ;
158+
159+ const createProtectedHandler = (
160+ auth : {
161+ readonly userId : string ;
162+ readonly email : string ;
163+ readonly firstName : string | null | undefined ;
164+ readonly lastName : string | null | undefined ;
165+ readonly avatarUrl : string | null | undefined ;
166+ } ,
167+ teamId : string ,
168+ executor : TeamExecutor ,
169+ ) => {
170+ const engine = createExecutionEngine ( { executor } ) ;
171+
172+ const requestServices = Layer . mergeAll (
173+ Layer . succeed ( AuthContext , {
174+ userId : auth . userId ,
175+ teamId,
176+ email : auth . email ,
177+ name : `${ auth . firstName ?? "" } ${ auth . lastName ?? "" } ` . trim ( ) || null ,
178+ avatarUrl : auth . avatarUrl ?? null ,
179+ } ) ,
180+ Layer . succeed ( ExecutorService , executor ) ,
181+ Layer . succeed ( ExecutionEngineService , engine ) ,
182+ Layer . succeed ( OpenApiExtensionService , executor . openapi ) ,
183+ Layer . succeed ( McpExtensionService , executor . mcp ) ,
184+ Layer . succeed ( GoogleDiscoveryExtensionService , executor . googleDiscovery ) ,
185+ Layer . succeed ( GraphqlExtensionService , executor . graphql ) ,
186+ ) ;
187+
188+ return HttpApiBuilder . toWebHandler (
189+ HttpApiSwagger . layer ( { path : "/docs" } ) . pipe (
190+ Layer . provideMerge ( HttpApiBuilder . middlewareOpenApi ( ) ) ,
191+ Layer . provideMerge ( ProtectedCloudApiLive ) ,
192+ Layer . provideMerge ( requestServices ) ,
193+ Layer . provideMerge ( SharedServices ) ,
194+ ) ,
195+ { middleware : HttpMiddleware . logger } ,
196+ ) ;
197+ } ;
94198
95- const executor = await Effect . runPromise (
96- createTeamExecutor ( db , teamId , teamName , encryptionKey ) ,
97- ) ;
199+ export const handleApiRequest = async ( request : Request ) : Promise < Response > => {
200+ const pathname = new URL ( request . url ) . pathname ;
98201
99- const pluginExtensions = Layer . mergeAll (
100- Layer . succeed ( OpenApiExtensionService , executor . openapi ) ,
101- Layer . succeed ( McpExtensionService , executor . mcp ) ,
102- Layer . succeed ( GoogleDiscoveryExtensionService , executor . googleDiscovery ) ,
103- Layer . succeed ( GraphqlExtensionService , executor . graphql ) ,
104- ) ;
202+ if ( isPublicPath ( pathname ) ) {
203+ return publicApiHandler . handler ( request ) ;
204+ }
105205
106- const engine = createExecutionEngine ( { executor } ) ;
107-
108- const handler = HttpApiBuilder . toWebHandler (
109- HttpApiSwagger . layer ( ) . pipe (
110- Layer . provideMerge ( HttpApiBuilder . middlewareOpenApi ( ) ) ,
111- Layer . provideMerge ( CloudApiBase ) ,
112- Layer . provideMerge ( pluginExtensions ) ,
113- Layer . provideMerge ( Layer . succeed ( ExecutorService , executor ) ) ,
114- Layer . provideMerge ( Layer . succeed ( ExecutionEngineService , engine ) ) ,
115- Layer . provideMerge ( Layer . succeed ( AuthContext , {
116- userId : auth . userId ,
117- email : auth . email ,
118- teamId,
119- name : `${ auth . firstName ?? "" } ${ auth . lastName ?? "" } ` . trim ( ) || null ,
120- avatarUrl : auth . avatarUrl ,
121- } ) ) ,
122- Layer . provideMerge ( UserStoreService . layer ( db ) ) ,
123- Layer . provideMerge ( WorkOSAuth . Default ) ,
124- Layer . provideMerge ( HttpServer . layerContext ) ,
125- ) ,
126- { middleware : HttpMiddleware . logger } ,
127- ) ;
206+ const auth = await resolveAuth ( request ) ;
207+ if ( ! auth ) return unauthorized ( "Unauthorized" ) ;
208+
209+ const cookieTeamId = parseCookie ( request . headers . get ( "cookie" ) , "executor_team" ) ;
210+ const teamId = await resolveTeamId ( auth , cookieTeamId ) ;
128211
129- try {
130- const response = await handler . handler ( request ) ;
212+ const executor = await resolveExecutor ( teamId ) ;
213+ const handler = createProtectedHandler ( auth , teamId , executor ) ;
131214
132- if ( auth . refreshedCookie ) {
133- const newResponse = new Response ( response . body , response ) ;
134- newResponse . headers . append ( "Set-Cookie" , auth . refreshedCookie ) ;
135- return newResponse ;
136- }
215+ try {
216+ const response = await handler . handler ( request ) ;
137217
138- return response ;
139- } finally {
140- await Effect . runPromise ( executor . close ( ) ) . catch ( ( ) => undefined ) ;
141- handler . dispose ( ) ;
218+ if ( auth . refreshedSession ) {
219+ setCookie ( "wos-session" , auth . refreshedSession , COOKIE_OPTIONS ) ;
220+ }
221+ if ( ! cookieTeamId ) {
222+ setCookie ( "executor_team" , teamId , COOKIE_OPTIONS ) ;
142223 }
143- } ;
224+ return response ;
225+ } finally {
226+ await Effect . runPromise ( executor . close ( ) ) . catch ( ( ) => undefined ) ;
227+ await handler . dispose ( ) . catch ( ( ) => undefined ) ;
228+ }
144229} ;
0 commit comments