112 lines · 4.3 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// HTTP API middleware — live implementations (server-only).// Imports the WorkOS SDK so it must NOT be pulled into the client bundle.// --------------------------------------------------------------------------- import { Effect, Layer, Redacted } from "effect";import { HttpServerResponse } from "effect/unstable/http"; import { AuthContext, NoOrganization, Unauthorized } from "@executor-js/api/server"; import {  OrgAuth,  SessionAuth,  SessionContext,  SessionCookies,  sessionFromSealed,  type SessionCookieOptions,  type SessionCookieSetter,} from "./middleware";import { WorkOSClient } from "./workos"; export const SessionAuthLive = Layer.effect(  SessionAuth,  Effect.gen(function* () {    const workos = yield* WorkOSClient;    return {      cookie: (httpEffect, { credential }) =>        Effect.gen(function* () {          const result = yield* workos            .authenticateSealedSession(Redacted.value(credential))            .pipe(Effect.orElseSucceed(() => null));           if (!result) {            return yield* Effect.fail(new Unauthorized());          }           // Per-request cookie queue. Typed `.handle()` session handlers (the          // WorkOS session-refresh on switchOrganization / createOrganization /          // acceptInvitation) return DATA, so they can't attach a Set-Cookie          // themselves — they `yield* SessionCookies` and queue writes, which we          // drain onto the response below. This is what lets `handlers.ts` drop          // the `@tanstack/react-start/server` `setCookie` import (the sole thing          // that pulled TanStack Start into the backend / Durable-Object graph).          const pending: Array<{            readonly name: string;            readonly value: string;            readonly options: SessionCookieOptions;          }> = [];          const cookieSetter: SessionCookieSetter = {            set: (name, value, options) => {              pending.push({ name, value, options });            },          };           const session = sessionFromSealed(result, Redacted.value(credential));          const response = yield* httpEffect.pipe(            Effect.provideService(SessionContext, session),            Effect.provideService(SessionCookies, cookieSetter),          );          return pending.reduce(            (res, c) => HttpServerResponse.setCookieUnsafe(res, c.name, c.value, c.options),            response,          );        }),    };  }),); export const OrgAuthLive = Layer.effect(  OrgAuth,  Effect.gen(function* () {    const workos = yield* WorkOSClient;    return {      cookie: (httpEffect, { credential }) =>        Effect.gen(function* () {          const result = yield* workos            .authenticateSealedSession(Redacted.value(credential))            .pipe(Effect.orElseSucceed(() => null));           if (!result) {            return yield* Effect.fail(new Unauthorized());          }           if (!result.organizationId) {            return yield* Effect.fail(new NoOrganization());          }           // NOTE: the domains plane stays scoped to the SESSION org, not the URL          // org. Unlike the data + account planes (which resolve the selector          // header), this `HttpApiMiddleware` security handler may carry no          // residual requirement, so it can't reach the per-request          // `UserStoreService` a slug→id resolution needs. The domains surface          // (org-settings → domain verification) is niche; URL-scoping it would          // mean converting this to an HttpRouter middleware — deferred.          const session = sessionFromSealed(result, Redacted.value(credential));          const auth = {            accountId: session.accountId,            organizationId: result.organizationId,            email: session.email,            name: session.name,            avatarUrl: session.avatarUrl,            // The unified `AuthContext` carries roles; cloud's WorkOS control            // plane does not resolve them here, so pass an empty list (no cloud            // handler reads roles today).            roles: [],          };           return yield* Effect.provideService(httpEffect, AuthContext, auth);        }),    };  }),);