import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";import { Schema } from "effect";import { UserStoreError, WorkOSError } from "./errors";import { 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,}); // `state` is optional — some WorkOS-initiated redirects arrive at the// callback without the state we set on /auth/login. The CSRF check is// only enforced when state is present (see callback handler).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 },) {} export const AUTH_PATHS = { login: "/api/auth/login", logout: "/api/auth/logout", callback: "/api/auth/callback",} as const; const AuthErrors = [UserStoreError, WorkOSError] 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, }), ) .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], }), ) .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) {}
import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
import { Schema } from "effect";
import { UserStoreError, WorkOSError } from "./errors";
import { 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,
});
// `state` is optional — some WorkOS-initiated redirects arrive at the
// callback without the state we set on /auth/login. The CSRF check is
// only enforced when state is present (see callback handler).
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 },
) {}
export const AUTH_PATHS = {
login: "/api/auth/login",
logout: "/api/auth/logout",
callback: "/api/auth/callback",
} as const;
const AuthErrors = [UserStoreError, WorkOSError] 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,
}),
)
.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],
}),
)
.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) {}