Org membership & project grants
Org membership & project grants (r.orgs, r.org(id), r.grants — org-owned control plane; first-class orgs)
Section titled “Org membership & project grants (r.orgs, r.org(id), r.grants — org-owned control plane; first-class orgs)”A wallet authenticates (SIWX → a control-plane principal); an org owns projects, and what a principal may do is decided by its org membership role (owner > admin > developer > billing > viewer) or a per-project grant — never wallet_address == signer. The collection + identity lives on r.orgs; per-org operations on the scoped sub-client r.org(id) (the org analog of r.project(id) — the id is bound once). Memberships carry org_id + display_name.
r.orgs.create({ displayName? })→{ org_id, display_name, tier, lease_started_at, lease_expires_at }(POST/orgs/v1). Creates an empty org on the prototype tier; you become owner. Accepts onlydisplayName— no tier input. Step-up gated; may throwApiError code: "FREE_ORG_OWNER_LIMIT_EXCEEDED"(429).r.orgs.list()→ orgs you are an active member of (OrgMembership[], each{ org_id, display_name, role, status }).r.orgs.whoami()→{ principal, memberships[], authenticator_id }(GET/agent/v1/whoami). The REMOTE, gateway-resolved identity — distinct fromr.whoami()(local + network-free wallet/profile label, used byrun402 status).r.org(id).get()→{ org_id, display_name, tier, lease_started_at, lease_expires_at, role }. Any active member; a non-member (incl. a guessed id) gets the same non-revealing 403.r.org(id).rename(displayName | null)→{ org_id, display_name, tier, lease_started_at, lease_expires_at }. Owner-only; set or clear the label (null/""clears). Step-up gated.r.org(id).claimSlug(slug, { idempotencyKey? })→{ org_id, slug, previous_slug, created }(POST/orgs/v1/:org_id/slug, repo-first-onramp design D6). Owner-only. A genesis claim debits a small one-time claim fee; a rename is free but releases the OLD slug into a ~90-day cooldown (typedSLUG_RELEASEDrefusal thereafter, naming this org’s new slug as successor). A paid, side-effecting mutation — requiresIdempotency-Key; the SDK generates a fresh one per call whenidempotencyKeyis omitted, so a retried call after a dropped response cannot double-bill.OrgSummary/OrgDetailgain an additiveslug: string | nullfield.r.org(id).members.list()/.add({ wallet, role? })/.setRole(principalId, { role })/.revoke(principalId)— owner-gated; a new wallet is provisioned as ahumanprincipal,roledefaults todeveloper. Removing/demoting the org’s only active owner throwsApiError code: "LAST_OWNER"(409).r.org(id).invites.list()/.create({ email, role, inviteTtlHours? })/.revoke(principalId)— email invites, claimed automatically at the invitee’s first login.r.org(id).audit({ limit?, before? })→ control-plane audit trail (admin+), newest-first; page withbefore.r.grants.create(projectId, { wallet, capability, policy?, expiresAt? })/r.grants.revoke(projectId, grantId)— per-project capability grants for agent/CI principals; requires owner of the project’s org. Also project-scoped:r.project(id).grants.create({...})/.revoke(grantId).capabilityexamples:"deploy","functions:write".
Control-plane denials throw NotAuthorizedError (403 NOT_AUTHORIZED, carrying requiredRole / requiredCapability / reason). Bad input is ApiError code: "VALIDATION_ERROR" (400). Exported types: OrgRole, Principal, OrgMembership, OrgMember, WhoAmIResult, OrgSummary, OrgDetail, CreateOrgInput, ProjectGrant, plus input/result types.