import { type AuthBearerBootstrapResult, AuthBootstrapInput, AuthCreatePairingCredentialInput, AuthRevokeClientSessionInput, AuthRevokePairingLinkInput, type AuthWebSocketTokenResult, } from "@t3tools/contracts"; import { DateTime, Effect, Schema } from "effect"; import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import { AuthError, ServerAuth } from "./Services/ServerAuth.ts"; import { SessionCredentialService } from "./Services/SessionCredentialService.ts"; import { deriveAuthClientMetadata } from "./utils.ts"; export const respondToAuthError = (error: AuthError) => Effect.gen(function* () { if ((error.status ?? 500) >= 500) { yield* Effect.logError("auth route failed", { message: error.message, cause: error.cause, }); } return HttpServerResponse.jsonUnsafe( { error: error.message, }, { status: error.status ?? 500 }, ); }); export const authSessionRouteLayer = HttpRouter.add( "GET", "/api/auth/session", Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; const serverAuth = yield* ServerAuth; const session = yield* serverAuth.getSessionState(request); return HttpServerResponse.jsonUnsafe(session, { status: 200 }); }), ); const PairingCredentialRequestHeaders = Schema.Struct({ "content-length": Schema.optionalKey(Schema.String), "content-type": Schema.optionalKey(Schema.String), "transfer-encoding": Schema.optionalKey(Schema.String), }); function hasRequestBody(headers: typeof PairingCredentialRequestHeaders.Type) { const contentLengthHeader = headers["content-length"]; if (typeof contentLengthHeader === "string") { const contentLength = Number.parseInt(contentLengthHeader, 10); if (Number.isFinite(contentLength)) { return contentLength > 0; } } return typeof headers["transfer-encoding"] === "string"; } export const authBootstrapRouteLayer = HttpRouter.add( "POST", "/api/auth/bootstrap", Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; const serverAuth = yield* ServerAuth; const sessions = yield* SessionCredentialService; const payload = yield* HttpServerRequest.schemaBodyJson(AuthBootstrapInput).pipe( Effect.mapError( (cause) => new AuthError({ message: "Invalid bootstrap payload.", status: 400, cause, }), ), ); const result = yield* serverAuth.exchangeBootstrapCredential( payload.credential, deriveAuthClientMetadata({ request }), ); return yield* HttpServerResponse.jsonUnsafe(result.response, { status: 200 }).pipe( HttpServerResponse.setCookie(sessions.cookieName, result.sessionToken, { expires: DateTime.toDate(result.response.expiresAt), httpOnly: true, path: "/", sameSite: "lax", }), ); }).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))), ); export const authBearerBootstrapRouteLayer = HttpRouter.add( "POST", "/api/auth/bootstrap/bearer", Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; const serverAuth = yield* ServerAuth; const payload = yield* HttpServerRequest.schemaBodyJson(AuthBootstrapInput).pipe( Effect.mapError( (cause) => new AuthError({ message: "Invalid bootstrap payload.", status: 400, cause, }), ), ); const result = yield* serverAuth.exchangeBootstrapCredentialForBearerSession( payload.credential, deriveAuthClientMetadata({ request }), ); return HttpServerResponse.jsonUnsafe(result satisfies AuthBearerBootstrapResult, { status: 200, }); }).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))), ); export const authWebSocketTokenRouteLayer = HttpRouter.add( "POST", "/api/auth/ws-token", Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; const serverAuth = yield* ServerAuth; const session = yield* serverAuth.authenticateHttpRequest(request); const result = yield* serverAuth.issueWebSocketToken(session); return HttpServerResponse.jsonUnsafe(result satisfies AuthWebSocketTokenResult, { status: 200, }); }).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))), ); export const authPairingCredentialRouteLayer = HttpRouter.add( "POST", "/api/auth/pairing-token", Effect.gen(function* () { const serverAuth = yield* ServerAuth; const request = yield* HttpServerRequest.HttpServerRequest; const session = yield* serverAuth.authenticateHttpRequest(request); if (session.role !== "owner") { return yield* new AuthError({ message: "Only owner sessions can create pairing credentials.", status: 403, }); } const headers = yield* HttpServerRequest.schemaHeaders(PairingCredentialRequestHeaders).pipe( Effect.mapError( (cause) => new AuthError({ message: "Invalid pairing credential request headers.", status: 400, cause, }), ), ); const payload = hasRequestBody(headers) ? yield* HttpServerRequest.schemaBodyJson(AuthCreatePairingCredentialInput).pipe( Effect.mapError( (cause) => new AuthError({ message: "Invalid pairing credential payload.", status: 400, cause, }), ), ) : {}; const result = yield* serverAuth.issuePairingCredential(payload); return HttpServerResponse.jsonUnsafe(result, { status: 200 }); }).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))), ); const authenticateOwnerSession = Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; const serverAuth = yield* ServerAuth; const session = yield* serverAuth.authenticateHttpRequest(request); if (session.role !== "owner") { return yield* new AuthError({ message: "Only owner sessions can manage network access.", status: 403, }); } return { serverAuth, session } as const; }); export const authPairingLinksRouteLayer = HttpRouter.add( "GET", "/api/auth/pairing-links", Effect.gen(function* () { const { serverAuth } = yield* authenticateOwnerSession; const pairingLinks = yield* serverAuth.listPairingLinks(); return HttpServerResponse.jsonUnsafe(pairingLinks, { status: 200 }); }).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))), ); export const authPairingLinksRevokeRouteLayer = HttpRouter.add( "POST", "/api/auth/pairing-links/revoke", Effect.gen(function* () { const { serverAuth } = yield* authenticateOwnerSession; const payload = yield* HttpServerRequest.schemaBodyJson(AuthRevokePairingLinkInput).pipe( Effect.mapError( (cause) => new AuthError({ message: "Invalid revoke pairing link payload.", status: 400, cause, }), ), ); const revoked = yield* serverAuth.revokePairingLink(payload.id); return HttpServerResponse.jsonUnsafe({ revoked }, { status: 200 }); }).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))), ); export const authClientsRouteLayer = HttpRouter.add( "GET", "/api/auth/clients", Effect.gen(function* () { const { serverAuth, session } = yield* authenticateOwnerSession; const clients = yield* serverAuth.listClientSessions(session.sessionId); return HttpServerResponse.jsonUnsafe(clients, { status: 200 }); }).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))), ); export const authClientsRevokeRouteLayer = HttpRouter.add( "POST", "/api/auth/clients/revoke", Effect.gen(function* () { const { serverAuth, session } = yield* authenticateOwnerSession; const payload = yield* HttpServerRequest.schemaBodyJson(AuthRevokeClientSessionInput).pipe( Effect.mapError( (cause) => new AuthError({ message: "Invalid revoke client payload.", status: 400, cause, }), ), ); const revoked = yield* serverAuth.revokeClientSession(session.sessionId, payload.sessionId); return HttpServerResponse.jsonUnsafe({ revoked }, { status: 200 }); }).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))), ); export const authClientsRevokeOthersRouteLayer = HttpRouter.add( "POST", "/api/auth/clients/revoke-others", Effect.gen(function* () { const { serverAuth, session } = yield* authenticateOwnerSession; const revokedCount = yield* serverAuth.revokeOtherClientSessions(session.sessionId); return HttpServerResponse.jsonUnsafe({ revokedCount }, { status: 200 }); }).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))), );