46 lines · 2.3 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Who may see the admin sections.//// There are two independent gates and both are load-bearing:////   1. NAV VISIBILITY (this file). The console has no role on the session —//      `AccountMeResponse` carries a user and an organization, not a role — so//      the only client-side role signal available today is the member list://      find the row flagged `isCurrentUser` and read its role. Cloud's org page//      already derives its danger zone this way; this generalizes it.//   2. ACCESS ITSELF (the page). The server decides. Every admin endpoint is//      gated per host (cloud: an org API key or an admin-role session;//      self-host: a Better Auth owner/admin), and the page renders a denied//      state on 401/403 regardless of what this file concluded.//// The nav gate is a convenience, not security: it exists so a plain member// isn't shown a section that would only refuse them. A member who guesses the// URL hits gate 2, which is the real one.// --------------------------------------------------------------------------- /** Roles that may read the tenant-wide admin plane. Cloud (WorkOS) issues *  `admin` / `member`; self-host (Better Auth) issues `owner` / `admin` / *  `member`. Both hosts' admin middleware accept exactly this set. */const TENANT_ADMIN_ROLES: ReadonlySet<string> = new Set(["admin", "owner"]); /** A member row as `/account/members` returns it, typed structurally. */export interface TenantMemberRow {  readonly role: string;  readonly status: string;  readonly isCurrentUser: boolean;} /** * Whether the current member may read the admin plane, per the member list. * * Only an ACTIVE membership counts: a pending invite carries a role but no * access yet, and treating it as admin would show a section the server refuses. * A list with no `isCurrentUser` row (self-host before the org is populated, a * host that doesn't serve members) is not an admin — the safe default, since * the page's own denied state is the honest surface for that case. */export const isTenantAdminMember = (members: readonly TenantMemberRow[]): boolean =>  members.some(    (member) =>      member.isCurrentUser && member.status === "active" && TENANT_ADMIN_ROLES.has(member.role),  );