// ---------------------------------------------------------------------------
// Cloud admin users API — the shared, provider-neutral `AdminUsersHandlers`
// backed by a WorkOS-authorized platform view, mounted at `/api/admin/users*`.
//
// TWO credentials reach this plane, and they are the two an operator actually
// has:
// 1. an ORG-SCOPED api key -> `PlatformAuth`. The key IS the authority: WorkOS
// validated it and reported which org owns it, and there is no member
// behind it to check membership for. This is the machine credential
// (a customer's backend calling us).
// 2. an admin SESSION member -> the console. Requires a live `getUserOrgMembership`
// whose role slug is `admin` AND whose status is `active`, matching the
// strictest existing cloud guard (`auth/handlers.ts`'s org-delete check) —
// a pending admin invite is not an admin.
// A plain member session, or a USER-scoped api key, is refused: both name one
// acting member, and this plane deliberately serves the whole tenant.
//
// The executor is built by `makePlatformExecutor` — `{ tenant, subject:
// undefined, platformView: true }` — so the reads are tenant-wide and read-only
// by storage policy, and no `subject` row is minted for the caller.
//
// Cross-tenant isolation is structural, not a check in this file: the tenant is
// taken from the resolved credential (the key's own org, or the session's
// authorized org), never from client input, and `reach: "tenant"` still filters
// every query by that tenant.
// ---------------------------------------------------------------------------
import { HttpRouter } from "effect/unstable/http";
import { Effect, Layer } from "effect";
import {
AdminUsersProvider,
DbProvider,
HostConfig,
PluginsProvider,
listAdminUserConnections,
listAdminUsers,
listAdminUsersWithConnections,
makeAdminUsersApiLayer,
makePlatformExecutor,
platformViewOf,
requestScopedMiddleware,
type AdminUsersHeaders,
} from "@executor-js/api/server";
import { AdminUsersError, AdminUsersForbidden, AdminUsersUnauthorized } from "@executor-js/api";
import type { Executor } from "@executor-js/sdk";
import { ApiKeyService } from "../auth/api-keys";
import { UserStoreService } from "../auth/context";
import { isPlatformAuth, resolveBearerAuth } from "../auth/workos-auth-provider";
import { orgSelectorFromRequest, authorizeOrganizationSelector } from "../auth/organization";
import { WorkOSClient } from "../auth/workos";
import { DbService } from "../db/db";
import { CloudExecutionSeamsLayer } from "../engine/execution-stack";
/**
* Resolve the tenant this request may read, or fail with the neutral 401/403.
*
* Returns only the organization id: nothing downstream needs to know WHICH of
* the two credentials got the caller here, and keeping the acting member out of
* the return value means no admin read can accidentally become subject-scoped.
*/
const authorizeTenant = (
request: Request,
): Effect.Effect<
string,
AdminUsersUnauthorized | AdminUsersForbidden,
WorkOSClient | ApiKeyService | UserStoreService
> =>
Effect.gen(function* () {
// (1) The bearer path. `resolveBearerAuth` (not `resolveApiKeyPrincipal`,
// which rejects org keys for the product plane) is what distinguishes an
// org key from a user key.
const bearer = yield* resolveBearerAuth(request).pipe(
// Every rejected-credential and infra failure collapses to one refusal:
// this plane must not report whether a key exists, belongs to another
// org, or merely lacks privilege.
Effect.catchCause(() => Effect.succeed(null)),
);
if (bearer !== null) {
if (isPlatformAuth(bearer)) return bearer.organizationId;
// A user-scoped key authenticated fine but names one member; the platform
// plane has no honest way to serve it.
return yield* new AdminUsersForbidden();
}
// (2) The session path: a live admin membership in the selected org.
const workos = yield* WorkOSClient;
const session = yield* workos
.authenticateRequest(request)
.pipe(Effect.catchCause(() => Effect.succeed(null)));
if (!session) return yield* new AdminUsersUnauthorized();
const selector = orgSelectorFromRequest(request) ?? session.organizationId;
if (!selector) return yield* new AdminUsersForbidden();
// Re-checks live membership, so the org selector header can only ever name
// an org the caller already belongs to.
const org = yield* authorizeOrganizationSelector(session.userId, selector).pipe(
Effect.catchCause(() => Effect.succeed(null)),
);
if (!org) return yield* new AdminUsersForbidden();
const membership = yield* workos
.getUserOrgMembership(org.id, session.userId)
.pipe(Effect.catchCause(() => Effect.succeed(null)));
// A pending admin invite is not an active admin — require both.
if (!membership || membership.status !== "active" || membership.role?.slug !== "admin") {
return yield* new AdminUsersForbidden();
}
return org.id;
});
/**
* Authorize, then run `body` against the tenant's platform view.
*
* The executor is built per request and closed after, like every other cloud
* execution stack: it holds the per-request postgres socket, which Cloudflare's
* I/O isolation forbids sharing across requests.
*/
const withPlatformView = (
headers: AdminUsersHeaders,
body: (executor: Executor) => Effect.Effect,
): Effect.Effect<
A,
AdminUsersError | AdminUsersUnauthorized | AdminUsersForbidden,
WorkOSClient | ApiKeyService | UserStoreService | DbProvider | PluginsProvider | HostConfig
> =>
Effect.gen(function* () {
const organizationId = yield* authorizeTenant(
new Request("https://admin.invalid", { headers }),
);
const executor = yield* makePlatformExecutor(organizationId).pipe(
Effect.mapError(() => new AdminUsersError({ message: "Failed to open the platform view" })),
);
return yield* Effect.ensuring(body(executor), executor.close().pipe(Effect.ignore));
});
/**
* Cloud's `AdminUsersProvider`, built per request so the platform executor
* closes over the per-request postgres socket.
*/
export const workosAdminUsersProvider: Layer.Layer<
AdminUsersProvider,
never,
WorkOSClient | ApiKeyService | UserStoreService | DbProvider | PluginsProvider | HostConfig
> = Layer.effect(AdminUsersProvider)(
Effect.gen(function* () {
const context = yield* Effect.context<
WorkOSClient | ApiKeyService | UserStoreService | DbProvider | PluginsProvider | HostConfig
>();
return AdminUsersProvider.of({
listUsers: (headers, options) =>
withPlatformView(headers, (executor) =>
platformViewOf(executor).pipe(Effect.flatMap((admin) => listAdminUsers(admin, options))),
).pipe(Effect.provideContext(context)),
listUsersWithConnections: (headers, options) =>
withPlatformView(headers, (executor) =>
platformViewOf(executor).pipe(
Effect.flatMap((admin) => listAdminUsersWithConnections(admin, options)),
),
).pipe(Effect.provideContext(context)),
listUserConnections: (headers, externalId) =>
withPlatformView(headers, (executor) =>
platformViewOf(executor).pipe(
Effect.flatMap((admin) => listAdminUserConnections(admin, externalId)),
),
).pipe(Effect.provideContext(context)),
});
}),
);
// Builds the provider per request, providing it to the handlers. Long-lived
// `WorkOSClient | ApiKeyService` come from the surrounding boot context; the
// per-request `DbService`/`UserStoreService` (and the execution seams built
// over them) are supplied by the combined `requestScopedMiddleware`.
const AdminUsersProviderMiddleware = HttpRouter.middleware<{ provides: AdminUsersProvider }>()(
Effect.gen(function* () {
const longLived = yield* Effect.context();
return (httpEffect) =>
Effect.gen(function* () {
// Built inside the request body so the execution seams close over the
// per-request postgres socket.
const provider = yield* Effect.provide(
AdminUsersProvider.asEffect(),
workosAdminUsersProvider.pipe(Layer.provide(CloudExecutionSeamsLayer)),
);
return yield* Effect.provideService(httpEffect, AdminUsersProvider, provider);
}).pipe(Effect.provideContext(longLived));
}),
);
/**
* The cloud admin-users route layer, mounted as an app extension under the same
* `/api` prefix as the rest of the cloud router.
*/
export const makeCloudAdminUsersRoutes = (
rsLive: Layer.Layer,
options: Parameters[1] = {},
) =>
makeAdminUsersApiLayer(
AdminUsersProviderMiddleware.combine(requestScopedMiddleware(rsLive)).layer,
options,
);