265 lines · 9.2 KB · TypeScript
RawDownload
import {  type AuthBearerBootstrapResult,  AuthBootstrapInput,  AuthCreatePairingCredentialInput,  AuthRevokeClientSessionInput,  AuthRevokePairingLinkInput,  type AuthWebSocketTokenResult,} from "@t3tools/contracts";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Schema from "effect/Schema";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";import { browserApiCorsHeaders } from "../httpCors.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, headers: browserApiCorsHeaders },    );  }); 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,      headers: browserApiCorsHeaders,    });  }),); 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,      headers: browserApiCorsHeaders,    }).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,      headers: browserApiCorsHeaders,    });  }).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,      headers: browserApiCorsHeaders,    });  }).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))),);