96 lines · 3.3 KB · TypeScript
RawDownload
import { Context, Effect, Layer } from "effect";import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import {  AuthContext,  requestScopedMiddleware,  type MemberDirectory,} from "@executor-js/api/server"; import { UserStoreService } from "../auth/context";import { sessionFromSealed } from "../auth/middleware";import { WorkOsMirror } from "../auth/workos-mirror";import { ORG_SELECTOR_HEADER, authorizeOrganizationSelector } from "../auth/organization";import { WorkOSClient } from "../auth/workos";import { DbService } from "../db/db"; const unauthorized = () =>  HttpServerResponse.jsonUnsafe(    {      error: "Invalid or expired session",      code: "invalid_session",    },    { status: 401 },  ); const noOrganization = () =>  HttpServerResponse.jsonUnsafe(    {      error: "No organization in session",      code: "no_organization",    },    { status: 403 },  ); /** * The caller's role in the session org, as `authorizeOrganizationSelector` * read it for THIS request from the membership mirror (`auth/organization.ts`). * Provided beside `AuthContext` — the shared seam, which carries no role — so * the domain handlers' admin gate is this one value, never a second read of * the mirror. */export class OrgMemberRole extends Context.Service<  OrgMemberRole,  { readonly memberRole: "admin" | "member" }>()("@executor-js/cloud/OrgMemberRole") {} const OrgAuthMiddleware = HttpRouter.middleware<{  provides: AuthContext | OrgMemberRole;}>()(  Effect.gen(function* () {    const captured = yield* Effect.context<WorkOSClient>();    const workos = yield* WorkOSClient;    return (httpEffect) =>      Effect.gen(function* () {        const request = yield* HttpServerRequest.HttpServerRequest;        const cookieValue = request.cookies["wos-session"] ?? "";        const result = yield* workos          .authenticateSealedSession(cookieValue)          .pipe(Effect.orElseSucceed(() => null));        if (!result) return unauthorized();         // FAIL CLOSED: no header, no org — the sealed cookie's org is a        // browser-global pinned to whichever org WorkOS last touched, so        // falling back to it scopes a multi-org user's request to the WRONG        // org (see workos-auth-provider.resolveSessionPrincipal).        const selector = request.headers[ORG_SELECTOR_HEADER];        if (!selector) return noOrganization();         const org = yield* authorizeOrganizationSelector(result.userId, selector).pipe(          Effect.orElseSucceed(() => null),        );        if (!org) return noOrganization();         const session = sessionFromSealed(result, cookieValue);        const auth = AuthContext.of({          accountId: session.accountId,          organizationId: org.id,          email: session.email,          name: session.name,          avatarUrl: session.avatarUrl,          roles: [],        });         return yield* Effect.provideContext(          httpEffect,          Context.make(AuthContext, auth).pipe(            Context.add(OrgMemberRole, { memberRole: org.memberRole }),          ),        );      }).pipe(Effect.provideContext(captured));  }),); export const orgAuthMiddleware = (  rsLive: Layer.Layer<DbService | UserStoreService | MemberDirectory | WorkOsMirror>,) => OrgAuthMiddleware.combine(requestScopedMiddleware(rsLive)).layer;