285 lines · 8.9 KB · TypeScript
RawDownload
import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";import { Schema } from "effect";import { UserStoreError, WorkOSError, WorkOsMirrorError } from "./errors";import { MemberDirectoryError, NoOrganization } from "@executor-js/api/server";import { SessionAuth } from "./middleware"; const AuthUser = Schema.Struct({  id: Schema.String,  email: Schema.String,  name: Schema.NullOr(Schema.String),  avatarUrl: Schema.NullOr(Schema.String),}); const AuthOrganization = Schema.Struct({  id: Schema.String,  name: Schema.String,  /** URL slug for org-prefixed console paths (`/<slug>/policies`). */  slug: Schema.String,}); const AuthMeResponse = Schema.Struct({  user: AuthUser,  organization: Schema.NullOr(AuthOrganization),}); const AuthOrganizationSummary = Schema.Struct({  id: Schema.String,  name: Schema.String,  slug: Schema.String,}); const AuthOrganizationsResponse = Schema.Struct({  organizations: Schema.Array(AuthOrganizationSummary),  activeOrganizationId: Schema.NullOr(Schema.String),}); const CreateOrganizationBody = Schema.Struct({  name: Schema.String,}); const CreateOrganizationResponse = Schema.Struct({  id: Schema.String,  name: Schema.String,  slug: Schema.String,}); // Deleting an org requires re-typing its name (the label the UI shows) as a// deliberate, non-automatable confirmation. Verified server-side too.const DeleteOrganizationBody = Schema.Struct({  confirmName: Schema.String,}); const DeleteOrganizationResponse = Schema.Struct({  success: Schema.Boolean,}); // CLI device-login discovery (`executor login`). Tells the CLI where to run// the OAuth 2.0 Device Authorization Grant (RFC 8628) and which public client// to use. The CLI hits these provider endpoints directly, gets a WorkOS access// token (a JWT), and sends it as a Bearer to the `/api/*` plane.const CliLoginResponse = Schema.Struct({  provider: Schema.Literal("workos"),  deviceAuthorizationEndpoint: Schema.String,  tokenEndpoint: Schema.String,  clientId: Schema.String,}); // Decode missing state so the callback can reject it with the same explicit// login-state failure as a mismatched value. Every successful callback must// match the state cookie created by /auth/login.const AuthCallbackSearch = Schema.Struct({  code: Schema.String,  state: Schema.optional(Schema.String),}); // Where to send the user after the callback completes (the /login page passes// the path the SSR auth gate captured). Validated server-side to same-origin// relative paths before use.const AuthLoginSearch = Schema.Struct({  returnTo: Schema.optional(Schema.String),}); const PendingInvitationInviter = Schema.Struct({  email: Schema.String,  name: Schema.NullOr(Schema.String),}); const PendingInvitation = Schema.Struct({  id: Schema.String,  organizationId: Schema.String,  organizationName: Schema.String,  createdAt: Schema.String,  inviter: Schema.NullOr(PendingInvitationInviter),}); const PendingInvitationsResponse = Schema.Struct({  invitations: Schema.Array(PendingInvitation),}); const AcceptInvitationBody = Schema.Struct({  invitationId: Schema.String,}); const AcceptInvitationResponse = Schema.Struct({  id: Schema.String,  name: Schema.String,  slug: Schema.String,}); const McpSessionExecutionParams = {  mcpSessionId: Schema.String,  executionId: Schema.String,}; const ResumeMcpExecutionBody = Schema.Struct({  action: Schema.Literals(["accept", "decline", "cancel"]),  content: Schema.optional(Schema.Unknown),  persist: Schema.optional(Schema.String),}); const McpPausedExecutionResponse = Schema.Struct({  text: Schema.String,  structured: Schema.Unknown,}); const McpResumeCompletedResponse = Schema.Struct({  status: Schema.Literal("completed"),  text: Schema.String,  structured: Schema.Unknown,  isError: Schema.Boolean,}); const McpResumePausedResponse = Schema.Struct({  status: Schema.Literal("paused"),  text: Schema.String,  structured: Schema.Unknown,}); const McpResumeExecutionResponse = Schema.Union([  McpResumeCompletedResponse,  McpResumePausedResponse,]); export class McpExecutionNotFoundError extends Schema.TaggedErrorClass<McpExecutionNotFoundError>()(  "McpExecutionNotFoundError",  {    executionId: Schema.String,  },  { httpApiStatus: 404 },) {} export class McpSessionForbiddenError extends Schema.TaggedErrorClass<McpSessionForbiddenError>()(  "McpSessionForbiddenError",  {    mcpSessionId: Schema.String,  },  { httpApiStatus: 403 },) {} // Refused org deletion: the caller is not an admin of the org, or the typed// confirmation did not match. Deliberately one error for both so it never// reveals which check failed.export class OrganizationDeletionForbidden extends Schema.TaggedErrorClass<OrganizationDeletionForbidden>()(  "OrganizationDeletionForbidden",  {},  { httpApiStatus: 403 },) {} // An org deletion that started but did not finish: the org is marked deleted// (every session is refused) and a later step — `step` — failed before the// local purge ran. The admin's own membership row is still there, so the// same request sent again resumes at the failed step.export class OrganizationDeletionIncomplete extends Schema.TaggedErrorClass<OrganizationDeletionIncomplete>()(  "OrganizationDeletionIncomplete",  { step: Schema.Literals(["billing"]) },  { httpApiStatus: 500 },) {} export const AUTH_PATHS = {  login: "/api/auth/login",  logout: "/api/auth/logout",  callback: "/api/auth/callback",} as const; // The login callback and the org handlers feed the membership mirror, so a// mirror write failure is one of their wire errors (same 500 as a store// failure); the session handlers READ it (membership, the org list, the admin// gate), so a directory read failure is one too.const AuthErrors = [UserStoreError, WorkOSError, WorkOsMirrorError, MemberDirectoryError] as const;const McpApprovalErrors = [  NoOrganization,  McpExecutionNotFoundError,  McpSessionForbiddenError,] as const; /** Public auth endpoints — no authentication required */export class CloudAuthPublicApi extends HttpApiGroup.make("cloudAuthPublic")  .add(HttpApiEndpoint.get("login", "/auth/login", { query: AuthLoginSearch }))  // Sign-out is PUBLIC on purpose. The console posts it as a top-level form  // navigation (the WorkOS hop is cross-origin, so it can't be a fetch), which  // means an error response is rendered as the page — and a browser whose  // session has already ended is exactly the browser most likely to click it  // (a second tab, a re-submit from history, an expired or revoked session).  // Behind SessionAuth all of those got a raw `{"_tag":"Unauthorized"}` screen  // instead of being signed out. There is nothing to authorize here anyway:  // the request can only end the session whose cookie it presents.  .add(HttpApiEndpoint.post("logout", "/auth/logout"))  .add(    HttpApiEndpoint.get("callback", "/auth/callback", {      query: AuthCallbackSearch,      error: AuthErrors,    }),  )  .add(    HttpApiEndpoint.get("cliLogin", "/auth/cli-login", {      success: CliLoginResponse,    }),  ) {} /** Session auth endpoints — require a logged-in user, may not have an org */export class CloudAuthApi extends HttpApiGroup.make("cloudAuth")  .add(    HttpApiEndpoint.get("me", "/auth/me", {      success: AuthMeResponse,      error: AuthErrors,    }),  )  .add(    HttpApiEndpoint.get("organizations", "/auth/organizations", {      success: AuthOrganizationsResponse,      error: [WorkOSError, UserStoreError, MemberDirectoryError],    }),  )  .add(    HttpApiEndpoint.post("createOrganization", "/auth/create-organization", {      payload: CreateOrganizationBody,      success: CreateOrganizationResponse,      error: AuthErrors,    }),  )  .add(    HttpApiEndpoint.post("deleteOrganization", "/auth/delete-organization", {      payload: DeleteOrganizationBody,      success: DeleteOrganizationResponse,      error: [        ...AuthErrors,        NoOrganization,        OrganizationDeletionForbidden,        OrganizationDeletionIncomplete,      ],    }),  )  .add(    HttpApiEndpoint.get("pendingInvitations", "/auth/pending-invitations", {      success: PendingInvitationsResponse,      error: WorkOSError,    }),  )  .add(    HttpApiEndpoint.post("acceptInvitation", "/auth/accept-invitation", {      payload: AcceptInvitationBody,      success: AcceptInvitationResponse,      error: AuthErrors,    }),  )  .add(    HttpApiEndpoint.get("getMcpPaused", "/mcp-sessions/:mcpSessionId/executions/:executionId", {      params: McpSessionExecutionParams,      success: McpPausedExecutionResponse,      error: McpApprovalErrors,    }),  )  .add(    HttpApiEndpoint.post(      "resumeMcpExecution",      "/mcp-sessions/:mcpSessionId/executions/:executionId/resume",      {        params: McpSessionExecutionParams,        payload: ResumeMcpExecutionBody,        success: McpResumeExecutionResponse,        error: McpApprovalErrors,      },    ),  )  .middleware(SessionAuth) {}