import type { PlanCatalog } from "./plan-model.js"; import type { BillingAdapter, OrgMember } from "./types.js"; import type { Invitation, InvitationService } from "./invitations.js"; /** What a refusal was, for a caller that wants to branch rather than read a sentence. */ export type MemberRefusal = /** The plan's `limits.members` is already met by active members + pending invitations. */ "limit_reached" /** They are the only admin left: demoting or removing them locks every human out. */ | "last_admin" /** Not in this workspace (or already gone). */ | "not_a_member" /** The adapter cannot answer the question this rule needs. */ | "unsupported" /** A `seatType` this workspace's own plan does not sell. */ | "unknown_seat" /** The seat exists and there is no room: nobody bought it, or the plan caps it. */ | "seat_unavailable"; export interface MemberSeats { /** Active memberships. */ active: number; /** Invitations sent and not yet accepted — they are seats already promised. */ pending: number; /** The ceiling that actually binds, or null for unlimited. */ limit: number | null; /** How many more people may be invited. Null when unlimited. */ remaining: number | null; /** * WHICH ceiling that is, because the two mean opposite things to whoever is refused. * * `"purchased"` — every seat the workspace BOUGHT is taken. Money fixes it: buy another. * `"plan"` — the plan's own `limits.members`. Money does not fix it at this tier; a * different plan does. * * A screen that cannot tell them apart offers "upgrade" to somebody who needs one more * seat, or "buy a seat" to somebody whose plan forbids a fourth person. */ limitSource: "purchased" | "plan" | null; } /** * How many seats are taken and how many are left. * * PENDING INVITATIONS COUNT. A limit checked against active members alone is not a limit: a * ten-seat workspace can send a hundred invitations and let every one of them in, and the * refusal arrives — if at all — when the eleventh person accepts, to the person accepting, * who cannot do anything about it. Counting the promise at the moment it is made is the * only version that refuses the right person. */ export declare function memberSeats(adapter: BillingAdapter, orgId: string, opts: { plans?: PlanCatalog; plan?: string | null; invitations?: InvitationService; }): Promise; /** Everyone in the workspace, with their role. Empty when the adapter cannot enumerate. */ export declare function listMembers(adapter: BillingAdapter, orgId: string): Promise; /** * Is this the only admin left? * * `null` means the question could not be answered — no `listMembers`, or a list with no role * on it. Callers must treat that as REFUSE, which is the opposite of the "unknown allows" * rule the rest of this library follows, and deliberately: the failure being prevented is * every human in a workspace getting 403 from every admin-gated tool, recoverable only with * an org API key. Refusing one demotion is the cheaper mistake. */ export declare function isLastAdmin(adapter: BillingAdapter, orgId: string, userId: string): Promise; /** * The sole active admin's id, or null when there are several — or none can be read. * * The list-shaped read of `isLastAdmin`, which answers one candidate at a time: a * members TABLE drawing a lock icon per row would cost N `listMembers` calls asking N * times about one list. Null disables nothing in a UI, and that is safe in both * directions — with several admins there is no lock to draw, and with unreadable * roles the WRITE path still refuses via `isLastAdmin`'s fail-closed null. */ export declare function lastAdminId(adapter: BillingAdapter, orgId: string): Promise; /** * Invite somebody, refusing when the plan has no seat for them. * * The invitation record and the email are the service's (`createWorkOSInvitations`); what is * here is the one thing it cannot know — whether this plan may have another member. */ export declare function inviteMember(adapter: BillingAdapter, orgId: string, input: { email: string; roleSlug?: string; inviterUserId?: string; invitations: InvitationService; plans?: PlanCatalog; plan?: string | null; /** * Seat to put them on, checked BEFORE the invitation goes out. * * A seat is a PRICE, and assigning one touches no subscription — so offering the choice * on an invite form without this check is the same giveaway `seatAssignable` exists to * stop, arriving through a different door. Omit it and the invitee draws the plan's * default seat, which is what every invitation did before. */ seatType?: string | null; }): Promise<{ ok: true; invitation: Invitation; seats: MemberSeats; seatType?: string | null; } | { ok: false; reason: MemberRefusal; seats: MemberSeats; }>; /** Move somebody between roles, refusing the demotion that locks everyone out. */ export declare function changeMemberRole(adapter: BillingAdapter, orgId: string, userId: string, roleSlug: string): Promise<{ ok: true; roleSlug: string; } | { ok: false; reason: MemberRefusal; }>; /** * Remove somebody — their records first, then the membership. * * That order is the `closeWorkspace` rule applied to one person: both per-member stores are * keyed by org, so a membership deleted first leaves this workspace's seat and grants sitting * in an ex-member's own metadata for ever, spending a character budget their remaining * workspaces still need. Once the membership is gone there is nothing left to enumerate them * from, so the cleanup cannot be done afterwards. */ export declare function removeMember(adapter: BillingAdapter, orgId: string, userId: string): Promise<{ ok: true; cleared: number; } | { ok: false; reason: MemberRefusal; }>; //# sourceMappingURL=members.d.ts.map