92 lines · 3.1 KB · TypeScript
RawDownload
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";import { Schema } from "effect"; // ---------------------------------------------------------------------------// Self-host admin API — the invite-code surface (app-local, self-host only).//// Member/role management is the shared, provider-neutral /account/* surface// (served by the Better Auth AccountProvider, rendered by the shared org page).// Invite CODES are self-host's join mechanism and have no neutral equivalent —// cloud joins via WorkOS — so they live in this app-local group, served// alongside the core API under /api and consumed by a self-host atom client.//// Browser-safe: schemas + the HttpApi value only (no server imports), so the// web client can build a typed AtomHttpApi from it.// --------------------------------------------------------------------------- export class AdminError extends Schema.TaggedErrorClass<AdminError>()(  "AdminError",  { message: Schema.String },  { httpApiStatus: 500 },) {} export class AdminUnauthorized extends Schema.TaggedErrorClass<AdminUnauthorized>()(  "AdminUnauthorized",  {},  { httpApiStatus: 401 },) {} export class AdminForbidden extends Schema.TaggedErrorClass<AdminForbidden>()(  "AdminForbidden",  {},  { httpApiStatus: 403 },) {} export const InviteCode = Schema.Struct({  id: Schema.String,  code: Schema.String,  role: Schema.String,  label: Schema.NullOr(Schema.String),  createdAt: Schema.String,  expiresAt: Schema.NullOr(Schema.String),  usedByEmail: Schema.NullOr(Schema.String),  usedAt: Schema.NullOr(Schema.String),}); export const InvitesResponse = Schema.Struct({  invites: Schema.Array(InviteCode),}); export const CreateInviteBody = Schema.Struct({  role: Schema.optional(Schema.String),  label: Schema.optional(Schema.String),  expiresInDays: Schema.optional(Schema.NullOr(Schema.Number)),}); export const SuccessResponse = Schema.Struct({  success: Schema.Boolean,}); const InviteParams = { inviteId: Schema.String }; // Paths are `/admin/*` (no `/api`): the server mounts this on the same// `/api`-prefixed router as the core API, and the client prepends the `/api`// base — symmetric with the account API.export const AdminApi = HttpApiGroup.make("admin")  .add(    HttpApiEndpoint.get("listInvites", "/admin/invites", {      success: InvitesResponse,      error: [AdminError, AdminUnauthorized, AdminForbidden],    }),  )  .add(    HttpApiEndpoint.post("createInvite", "/admin/invites", {      payload: CreateInviteBody,      success: InviteCode,      error: [AdminError, AdminUnauthorized, AdminForbidden],    }),  )  .add(    HttpApiEndpoint.delete("revokeInvite", "/admin/invites/:inviteId", {      params: InviteParams,      success: SuccessResponse,      error: [AdminError, AdminUnauthorized, AdminForbidden],    }),  ); /** * Standalone HttpApi wrapping the admin group — used to build the self-host * `AdminApiClient` atoms in the web app, and mounted server-side as an * extension route layer. */export const AdminHttpApi = HttpApi.make("executor-self-host-admin").add(AdminApi);