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(); 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, ) => OrgAuthMiddleware.combine(requestScopedMiddleware(rsLive)).layer;