Documentation
¶
Overview ¶
Package teams is a thin client for the Truestamp Teams + Memberships JSON:API surfaces (GET /api/json/teams, /api/json/memberships). It exposes the small subset of operations the CLI needs to discover a user's team memberships and validate that a configured team is accessible.
The membership read policy on the server side filters to the actor's own memberships, so `ListMyMemberships` returns "the teams I'm a member of" with no extra filtering. See truestamp-v2/lib/truestamp/teams/membership.ex policy block.
Index ¶
- Constants
- Variables
- func FormatRole(role string) string
- func GetMyRoleOnTeam(ctx context.Context, cfg Config, teamID string) (string, error)
- func OwnershipDescription(model string) string
- func OwnershipLabel(model string) string
- func OwnershipModels() []string
- func PrivilegeRank(role string) int
- type APIError
- type Config
- type Membership
- type Team
Constants ¶
const ( OwnershipCreatorRetains = "creator_retains" OwnershipTeamRetains = "team_retains" )
Ownership models accepted by CreateTeam. creator_retains is the free-tier-compatible default; team_retains requires a plan entitlement (the team, not the creator, retains ownership across membership changes).
Variables ¶
var ( ErrForbidden = errors.New("forbidden") ErrNotFound = errors.New("not found") ErrBadRequest = errors.New("bad request") ErrRateLimited = errors.New("rate limited") ErrServer = errors.New("server error") // ErrTeamLimitReached is returned by CreateTeam when the actor's plan // team quota is exhausted (a distinct, user-actionable case: upgrade the // plan). Identified by the server's stable machine error code, not prose. ErrTeamLimitReached = errors.New("team limit reached") // ErrOwnershipNotEntitled is returned by CreateTeam when the requested // ownership_model (e.g. team_retains) requires a plan entitlement the // actor lacks — distinct from the team-count limit. ErrOwnershipNotEntitled = errors.New("ownership model not entitled") )
Errors surfaced by the client. CLI layers may errors.Is these to pick an exit code and user-facing message.
ErrUnauthorized covers 401 (the API key itself is invalid or expired). ErrForbidden covers 403 — auth was accepted but the actor isn't allowed to read this resource (typically: the tenant header points to a team the actor isn't a member of). Distinguishing the two matters because the user-facing remediation is completely different: 401 → run `auth login`; 403 → check the team id, ask for membership.
Functions ¶
func FormatRole ¶
FormatRole returns a human-friendly title-cased role label suitable for table cells: "team_owner" -> "Owner", "team_admin" -> "Admin", "team_member" -> "Member", "team_viewer" -> "Viewer". Unknown inputs pass through unchanged so a future server-side role addition shows up verbatim instead of being silently dropped.
func GetMyRoleOnTeam ¶
GetMyRoleOnTeam returns the role string for the API key's user on the given team, or the empty string if the user has no membership (without distinguishing the "no membership" case from "lookup failed"). Callers that need the failure distinction should call ListMyMemberships and search the slice.
func OwnershipDescription ¶ added in v0.10.0
OwnershipDescription returns a one-line explanation of an ownership model for help text and form field descriptions.
func OwnershipLabel ¶ added in v0.10.0
OwnershipLabel returns a short, title-cased label for an ownership model, suitable for selects and cards. Unknown values pass through unchanged.
func OwnershipModels ¶ added in v0.10.0
func OwnershipModels() []string
OwnershipModels returns the ownership models a client may offer, in display order. Both surfaces (the `team create` subcommand and the console modal) build their pickers from this single source so a future model is added once.
func PrivilegeRank ¶
PrivilegeRank returns a sort key for the given role. Lower values rank higher (Owner = 0, Viewer = 3). Unknown roles sort last so a future server-side addition isn't silently grouped with viewers.
Types ¶
type APIError ¶
type APIError struct {
Status int
Pointer string // JSON:API errors[].source.pointer, when present
Detail string // preferred; falls back to Title
RetryAfter string // verbatim Retry-After header on 429
// contains filtered or unexported fields
}
APIError carries HTTP status + preserved `errors[].detail` from the JSON:API envelope for display to the user. Wraps one of the sentinel errors above so callers can errors.Is() the class while still showing the detail text.
type Config ¶
type Config struct {
APIURL string // e.g. https://www.truestamp.com/api/json
Team string // optional tenant id; sent verbatim as the `tenant` header
}
Config carries the subset of runtime configuration needed for a request. Kept small to avoid importing the top-level config package. The credential is supplied out-of-band by the process-wide auth.Authorizer (set in cmd/root); only the tenant scoping lives here.
type Membership ¶
type Membership struct {
ID string `json:"id"`
TeamID string `json:"team_id"`
Role string `json:"role"` // "team_owner" | "team_admin" | "team_member" | "team_viewer"
Team *Team `json:"team,omitempty"`
}
Membership pairs a user's role with the team it applies to. Team is populated from a parallel `GET /teams` call, joined client-side (see ListMyMemberships). JSON tags are snake_case to match the shape of every other --json surface in the CLI; without explicit tags the Go field names ("ID", "TeamID") would leak.
func ListMyMemberships ¶
func ListMyMemberships(ctx context.Context, cfg Config) ([]Membership, error)
ListMyMemberships returns one Membership row per team the API key's user has access to, with the user's role on that team.
The implementation anchors on `GET /teams` (source of truth for "teams I can read", per the server-side `relates_to_actor_via(:members)` READ policy) rather than `/memberships` because:
- `/memberships` can return rows whose team_id no longer exists (orphaned dev seed data, mid-cascade-delete races).
- Under admin bypass policies, `/memberships` may return memberships from other users that the actor doesn't actually hold. Anchoring on the team list ensures every returned row represents a team the actor can actually read.
- `?include=team` on the memberships endpoint is unreliable — the included array is sparse for reasons we haven't fully diagnosed (possibly Ash's per-response include limits or relationship-load authorization filtering).
Roles are joined from a parallel `/memberships` request and deduplicated by team_id (when multiple memberships exist for the same team — e.g. different users on the same team under admin bypass — we take the first match deterministically).
Both endpoints walk `links.next` so users with many memberships or teams aren't silently truncated by the server's default page size.
type Team ¶
type Team struct {
ID string `json:"id"`
Name string `json:"name"`
Personal bool `json:"personal"`
OwnershipModel string `json:"ownership_model"`
CreatedAt string `json:"inserted_at"`
}
Team is the subset of a JSON:API team resource the CLI consumes.
func CreateTeam ¶ added in v0.10.0
CreateTeam creates a new (non-personal) team owned by the authenticated actor. ownershipModel is OwnershipCreatorRetains or OwnershipTeamRetains (the latter requires a plan entitlement); an empty string lets the server apply its default. The server derives the creator from the actor and auto-creates the owner Membership, so neither is sent.
The tenant header is cleared because creating a team is an account-level action, not an operation within an existing team context. On a plan-limit rejection the error satisfies errors.Is(err, ErrTeamLimitReached); on an ownership-entitlement rejection, errors.Is(err, ErrOwnershipNotEntitled).