11import {
2- type AuthBearerBootstrapResult ,
3- AuthBootstrapInput ,
4- AuthCreatePairingCredentialInput ,
5- AuthRevokeClientSessionInput ,
6- AuthRevokePairingLinkInput ,
7- type AuthWebSocketTokenResult ,
2+ EnvironmentHttpApi ,
3+ EnvironmentHttpBadRequestError ,
4+ EnvironmentHttpForbiddenError ,
5+ EnvironmentHttpInternalServerError ,
6+ EnvironmentHttpUnauthorizedError ,
87} from "@t3tools/contracts" ;
98import * as DateTime from "effect/DateTime" ;
109import * as Effect from "effect/Effect" ;
11- import * as Schema from "effect/Schema" ;
12- import { HttpRouter , HttpServerRequest , HttpServerResponse } from "effect/unstable/http" ;
10+ import * as Cookies from "effect/unstable/http/Cookies" ;
11+ import * as HttpEffect from "effect/unstable/http/HttpEffect" ;
12+ import { HttpServerRequest , HttpServerResponse } from "effect/unstable/http" ;
13+ import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder" ;
1314
1415import { AuthError , ServerAuth } from "./Services/ServerAuth.ts" ;
1516import { SessionCredentialService } from "./Services/SessionCredentialService.ts" ;
1617import { deriveAuthClientMetadata } from "./utils.ts" ;
17- import { browserApiCorsHeaders } from "../httpCors.ts" ;
1818
1919export const respondToAuthError = ( error : AuthError ) =>
2020 Effect . gen ( function * ( ) {
@@ -28,158 +28,30 @@ export const respondToAuthError = (error: AuthError) =>
2828 {
2929 error : error . message ,
3030 } ,
31- { status : error . status ?? 500 , headers : browserApiCorsHeaders } ,
31+ { status : error . status ?? 500 } ,
3232 ) ;
3333 } ) ;
3434
35- export const authSessionRouteLayer = HttpRouter . add (
36- "GET" ,
37- "/api/auth/session" ,
35+ export const failEnvironmentHttpAuthError = ( error : AuthError ) =>
3836 Effect . gen ( function * ( ) {
39- const request = yield * HttpServerRequest . HttpServerRequest ;
40- const serverAuth = yield * ServerAuth ;
41- const session = yield * serverAuth . getSessionState ( request ) ;
42- return HttpServerResponse . jsonUnsafe ( session , {
43- status : 200 ,
44- headers : browserApiCorsHeaders ,
45- } ) ;
46- } ) ,
47- ) ;
48-
49- const PairingCredentialRequestHeaders = Schema . Struct ( {
50- "content-length" : Schema . optionalKey ( Schema . String ) ,
51- "content-type" : Schema . optionalKey ( Schema . String ) ,
52- "transfer-encoding" : Schema . optionalKey ( Schema . String ) ,
53- } ) ;
54-
55- function hasRequestBody ( headers : typeof PairingCredentialRequestHeaders . Type ) {
56- const contentLengthHeader = headers [ "content-length" ] ;
57- if ( typeof contentLengthHeader === "string" ) {
58- const contentLength = Number . parseInt ( contentLengthHeader , 10 ) ;
59- if ( Number . isFinite ( contentLength ) ) {
60- return contentLength > 0 ;
37+ if ( ( error . status ?? 500 ) >= 500 ) {
38+ yield * Effect . logError ( "auth route failed" , {
39+ message : error . message ,
40+ cause : error . cause ,
41+ } ) ;
6142 }
62- }
63- return typeof headers [ "transfer-encoding" ] === "string" ;
64- }
65-
66- export const authBootstrapRouteLayer = HttpRouter . add (
67- "POST" ,
68- "/api/auth/bootstrap" ,
69- Effect . gen ( function * ( ) {
70- const request = yield * HttpServerRequest . HttpServerRequest ;
71- const serverAuth = yield * ServerAuth ;
72- const sessions = yield * SessionCredentialService ;
73- const payload = yield * HttpServerRequest . schemaBodyJson ( AuthBootstrapInput ) . pipe (
74- Effect . mapError (
75- ( cause ) =>
76- new AuthError ( {
77- message : "Invalid bootstrap payload." ,
78- status : 400 ,
79- cause,
80- } ) ,
81- ) ,
82- ) ;
83- const result = yield * serverAuth . exchangeBootstrapCredential (
84- payload . credential ,
85- deriveAuthClientMetadata ( { request } ) ,
86- ) ;
8743
88- return yield * HttpServerResponse . jsonUnsafe ( result . response , {
89- status : 200 ,
90- headers : browserApiCorsHeaders ,
91- } ) . pipe (
92- HttpServerResponse . setCookie ( sessions . cookieName , result . sessionToken , {
93- expires : DateTime . toDate ( result . response . expiresAt ) ,
94- httpOnly : true ,
95- path : "/" ,
96- sameSite : "lax" ,
97- } ) ,
98- ) ;
99- } ) . pipe ( Effect . catchTag ( "AuthError" , ( error ) => respondToAuthError ( error ) ) ) ,
100- ) ;
101-
102- export const authBearerBootstrapRouteLayer = HttpRouter . add (
103- "POST" ,
104- "/api/auth/bootstrap/bearer" ,
105- Effect . gen ( function * ( ) {
106- const request = yield * HttpServerRequest . HttpServerRequest ;
107- const serverAuth = yield * ServerAuth ;
108- const payload = yield * HttpServerRequest . schemaBodyJson ( AuthBootstrapInput ) . pipe (
109- Effect . mapError (
110- ( cause ) =>
111- new AuthError ( {
112- message : "Invalid bootstrap payload." ,
113- status : 400 ,
114- cause,
115- } ) ,
116- ) ,
117- ) ;
118- const result = yield * serverAuth . exchangeBootstrapCredentialForBearerSession (
119- payload . credential ,
120- deriveAuthClientMetadata ( { request } ) ,
121- ) ;
122- return HttpServerResponse . jsonUnsafe ( result satisfies AuthBearerBootstrapResult , {
123- status : 200 ,
124- headers : browserApiCorsHeaders ,
125- } ) ;
126- } ) . pipe ( Effect . catchTag ( "AuthError" , ( error ) => respondToAuthError ( error ) ) ) ,
127- ) ;
128-
129- export const authWebSocketTokenRouteLayer = HttpRouter . add (
130- "POST" ,
131- "/api/auth/ws-token" ,
132- Effect . gen ( function * ( ) {
133- const request = yield * HttpServerRequest . HttpServerRequest ;
134- const serverAuth = yield * ServerAuth ;
135- const session = yield * serverAuth . authenticateHttpRequest ( request ) ;
136- const result = yield * serverAuth . issueWebSocketToken ( session ) ;
137- return HttpServerResponse . jsonUnsafe ( result satisfies AuthWebSocketTokenResult , {
138- status : 200 ,
139- headers : browserApiCorsHeaders ,
140- } ) ;
141- } ) . pipe ( Effect . catchTag ( "AuthError" , ( error ) => respondToAuthError ( error ) ) ) ,
142- ) ;
143-
144- export const authPairingCredentialRouteLayer = HttpRouter . add (
145- "POST" ,
146- "/api/auth/pairing-token" ,
147- Effect . gen ( function * ( ) {
148- const serverAuth = yield * ServerAuth ;
149- const request = yield * HttpServerRequest . HttpServerRequest ;
150- const session = yield * serverAuth . authenticateHttpRequest ( request ) ;
151- if ( session . role !== "owner" ) {
152- return yield * new AuthError ( {
153- message : "Only owner sessions can create pairing credentials." ,
154- status : 403 ,
155- } ) ;
44+ switch ( error . status ) {
45+ case 400 :
46+ return yield * new EnvironmentHttpBadRequestError ( { message : error . message } ) ;
47+ case 401 :
48+ return yield * new EnvironmentHttpUnauthorizedError ( { message : error . message } ) ;
49+ case 403 :
50+ return yield * new EnvironmentHttpForbiddenError ( { message : error . message } ) ;
51+ default :
52+ return yield * new EnvironmentHttpInternalServerError ( { message : error . message } ) ;
15653 }
157- const headers = yield * HttpServerRequest . schemaHeaders ( PairingCredentialRequestHeaders ) . pipe (
158- Effect . mapError (
159- ( cause ) =>
160- new AuthError ( {
161- message : "Invalid pairing credential request headers." ,
162- status : 400 ,
163- cause,
164- } ) ,
165- ) ,
166- ) ;
167- const payload = hasRequestBody ( headers )
168- ? yield * HttpServerRequest . schemaBodyJson ( AuthCreatePairingCredentialInput ) . pipe (
169- Effect . mapError (
170- ( cause ) =>
171- new AuthError ( {
172- message : "Invalid pairing credential payload." ,
173- status : 400 ,
174- cause,
175- } ) ,
176- ) ,
177- )
178- : { } ;
179- const result = yield * serverAuth . issuePairingCredential ( payload ) ;
180- return HttpServerResponse . jsonUnsafe ( result , { status : 200 } ) ;
181- } ) . pipe ( Effect . catchTag ( "AuthError" , ( error ) => respondToAuthError ( error ) ) ) ,
182- ) ;
54+ } ) ;
18355
18456const authenticateOwnerSession = Effect . gen ( function * ( ) {
18557 const request = yield * HttpServerRequest . HttpServerRequest ;
@@ -194,72 +66,112 @@ const authenticateOwnerSession = Effect.gen(function* () {
19466 return { serverAuth, session } as const ;
19567} ) ;
19668
197- export const authPairingLinksRouteLayer = HttpRouter . add (
198- "GET" ,
199- "/api/auth/pairing-links" ,
200- Effect . gen ( function * ( ) {
201- const { serverAuth } = yield * authenticateOwnerSession ;
202- const pairingLinks = yield * serverAuth . listPairingLinks ( ) ;
203- return HttpServerResponse . jsonUnsafe ( pairingLinks , { status : 200 } ) ;
204- } ) . pipe ( Effect . catchTag ( "AuthError" , ( error ) => respondToAuthError ( error ) ) ) ,
205- ) ;
206-
207- export const authPairingLinksRevokeRouteLayer = HttpRouter . add (
208- "POST" ,
209- "/api/auth/pairing-links/revoke" ,
210- Effect . gen ( function * ( ) {
211- const { serverAuth } = yield * authenticateOwnerSession ;
212- const payload = yield * HttpServerRequest . schemaBodyJson ( AuthRevokePairingLinkInput ) . pipe (
213- Effect . mapError (
214- ( cause ) =>
215- new AuthError ( {
216- message : "Invalid revoke pairing link payload." ,
217- status : 400 ,
218- cause,
219- } ) ,
220- ) ,
221- ) ;
222- const revoked = yield * serverAuth . revokePairingLink ( payload . id ) ;
223- return HttpServerResponse . jsonUnsafe ( { revoked } , { status : 200 } ) ;
224- } ) . pipe ( Effect . catchTag ( "AuthError" , ( error ) => respondToAuthError ( error ) ) ) ,
225- ) ;
226-
227- export const authClientsRouteLayer = HttpRouter . add (
228- "GET" ,
229- "/api/auth/clients" ,
230- Effect . gen ( function * ( ) {
231- const { serverAuth, session } = yield * authenticateOwnerSession ;
232- const clients = yield * serverAuth . listClientSessions ( session . sessionId ) ;
233- return HttpServerResponse . jsonUnsafe ( clients , { status : 200 } ) ;
234- } ) . pipe ( Effect . catchTag ( "AuthError" , ( error ) => respondToAuthError ( error ) ) ) ,
235- ) ;
236-
237- export const authClientsRevokeRouteLayer = HttpRouter . add (
238- "POST" ,
239- "/api/auth/clients/revoke" ,
240- Effect . gen ( function * ( ) {
241- const { serverAuth, session } = yield * authenticateOwnerSession ;
242- const payload = yield * HttpServerRequest . schemaBodyJson ( AuthRevokeClientSessionInput ) . pipe (
243- Effect . mapError (
244- ( cause ) =>
245- new AuthError ( {
246- message : "Invalid revoke client payload." ,
247- status : 400 ,
248- cause,
69+ export const authHttpApiLayer = HttpApiBuilder . group ( EnvironmentHttpApi , "auth" , ( handlers ) =>
70+ handlers
71+ . handle ( "session" , ( ) =>
72+ Effect . gen ( function * ( ) {
73+ const request = yield * HttpServerRequest . HttpServerRequest ;
74+ const serverAuth = yield * ServerAuth ;
75+ return yield * serverAuth . getSessionState ( request ) ;
76+ } ) ,
77+ )
78+ . handle ( "bootstrap" , ( { payload } ) =>
79+ Effect . gen ( function * ( ) {
80+ const request = yield * HttpServerRequest . HttpServerRequest ;
81+ const serverAuth = yield * ServerAuth ;
82+ const sessions = yield * SessionCredentialService ;
83+ const result = yield * serverAuth . exchangeBootstrapCredential (
84+ payload . credential ,
85+ deriveAuthClientMetadata ( { request } ) ,
86+ ) ;
87+ const sessionCookies = yield * Effect . fromResult (
88+ Cookies . set ( Cookies . empty , sessions . cookieName , result . sessionToken , {
89+ expires : DateTime . toDate ( result . response . expiresAt ) ,
90+ httpOnly : true ,
91+ path : "/" ,
92+ sameSite : "lax" ,
24993 } ) ,
250- ) ,
251- ) ;
252- const revoked = yield * serverAuth . revokeClientSession ( session . sessionId , payload . sessionId ) ;
253- return HttpServerResponse . jsonUnsafe ( { revoked } , { status : 200 } ) ;
254- } ) . pipe ( Effect . catchTag ( "AuthError" , ( error ) => respondToAuthError ( error ) ) ) ,
255- ) ;
256-
257- export const authClientsRevokeOthersRouteLayer = HttpRouter . add (
258- "POST" ,
259- "/api/auth/clients/revoke-others" ,
260- Effect . gen ( function * ( ) {
261- const { serverAuth, session } = yield * authenticateOwnerSession ;
262- const revokedCount = yield * serverAuth . revokeOtherClientSessions ( session . sessionId ) ;
263- return HttpServerResponse . jsonUnsafe ( { revokedCount } , { status : 200 } ) ;
264- } ) . pipe ( Effect . catchTag ( "AuthError" , ( error ) => respondToAuthError ( error ) ) ) ,
94+ ) . pipe (
95+ Effect . mapError (
96+ ( cause ) =>
97+ new AuthError ( {
98+ message : "Failed to create browser session response." ,
99+ status : 500 ,
100+ cause,
101+ } ) ,
102+ ) ,
103+ ) ;
104+
105+ yield * HttpEffect . appendPreResponseHandler ( ( _request , response ) =>
106+ Effect . succeed ( HttpServerResponse . mergeCookies ( response , sessionCookies ) ) ,
107+ ) ;
108+ return result . response ;
109+ } ) . pipe ( Effect . catchTag ( "AuthError" , failEnvironmentHttpAuthError ) ) ,
110+ )
111+ . handle ( "bootstrapBearer" , ( { payload } ) =>
112+ Effect . gen ( function * ( ) {
113+ const request = yield * HttpServerRequest . HttpServerRequest ;
114+ const serverAuth = yield * ServerAuth ;
115+ const result = yield * serverAuth . exchangeBootstrapCredentialForBearerSession (
116+ payload . credential ,
117+ deriveAuthClientMetadata ( { request } ) ,
118+ ) ;
119+ return result ;
120+ } ) . pipe ( Effect . catchTag ( "AuthError" , failEnvironmentHttpAuthError ) ) ,
121+ )
122+ . handle ( "webSocketToken" , ( ) =>
123+ Effect . gen ( function * ( ) {
124+ const request = yield * HttpServerRequest . HttpServerRequest ;
125+ const serverAuth = yield * ServerAuth ;
126+ const session = yield * serverAuth . authenticateHttpRequest ( request ) ;
127+ return yield * serverAuth . issueWebSocketToken ( session ) ;
128+ } ) . pipe ( Effect . catchTag ( "AuthError" , failEnvironmentHttpAuthError ) ) ,
129+ )
130+ . handle ( "pairingCredential" , ( { payload } ) =>
131+ Effect . gen ( function * ( ) {
132+ const serverAuth = yield * ServerAuth ;
133+ const request = yield * HttpServerRequest . HttpServerRequest ;
134+ const session = yield * serverAuth . authenticateHttpRequest ( request ) ;
135+ if ( session . role !== "owner" ) {
136+ return yield * new AuthError ( {
137+ message : "Only owner sessions can create pairing credentials." ,
138+ status : 403 ,
139+ } ) ;
140+ }
141+ return yield * serverAuth . issuePairingCredential ( payload ) ;
142+ } ) . pipe ( Effect . catchTag ( "AuthError" , failEnvironmentHttpAuthError ) ) ,
143+ )
144+ . handle ( "pairingLinks" , ( ) =>
145+ Effect . gen ( function * ( ) {
146+ const { serverAuth } = yield * authenticateOwnerSession ;
147+ return yield * serverAuth . listPairingLinks ( ) ;
148+ } ) . pipe ( Effect . catchTag ( "AuthError" , failEnvironmentHttpAuthError ) ) ,
149+ )
150+ . handle ( "revokePairingLink" , ( { payload } ) =>
151+ Effect . gen ( function * ( ) {
152+ const { serverAuth } = yield * authenticateOwnerSession ;
153+ const revoked = yield * serverAuth . revokePairingLink ( payload . id ) ;
154+ return { revoked } ;
155+ } ) . pipe ( Effect . catchTag ( "AuthError" , failEnvironmentHttpAuthError ) ) ,
156+ )
157+ . handle ( "clients" , ( ) =>
158+ Effect . gen ( function * ( ) {
159+ const { serverAuth, session } = yield * authenticateOwnerSession ;
160+ return yield * serverAuth . listClientSessions ( session . sessionId ) ;
161+ } ) . pipe ( Effect . catchTag ( "AuthError" , failEnvironmentHttpAuthError ) ) ,
162+ )
163+ . handle ( "revokeClient" , ( { payload } ) =>
164+ Effect . gen ( function * ( ) {
165+ const { serverAuth, session } = yield * authenticateOwnerSession ;
166+ const revoked = yield * serverAuth . revokeClientSession ( session . sessionId , payload . sessionId ) ;
167+ return { revoked } ;
168+ } ) . pipe ( Effect . catchTag ( "AuthError" , failEnvironmentHttpAuthError ) ) ,
169+ )
170+ . handle ( "revokeOtherClients" , ( ) =>
171+ Effect . gen ( function * ( ) {
172+ const { serverAuth, session } = yield * authenticateOwnerSession ;
173+ const revokedCount = yield * serverAuth . revokeOtherClientSessions ( session . sessionId ) ;
174+ return { revokedCount } ;
175+ } ) . pipe ( Effect . catchTag ( "AuthError" , failEnvironmentHttpAuthError ) ) ,
176+ ) ,
265177) ;
0 commit comments