Documentation
¶
Overview ¶
Package db owns the postgres connection pool, migrations, and per-table repositories.
Index ¶
- Variables
- func AcceptInvite(ctx context.Context, p *Pool, inviteID, acceptingUserID uuid.UUID) error
- func AddMember(ctx context.Context, p *Pool, accountID, userID uuid.UUID) error
- func ApproveDeviceCode(ctx context.Context, p *Pool, userCode string, userID uuid.UUID) error
- func ConsumeDeviceCode(ctx context.Context, p *Pool, deviceCode string) (uuid.UUID, error)
- func DeclineInvite(ctx context.Context, p *Pool, inviteID, decliningUserID uuid.UUID) error
- func DeletePipe(ctx context.Context, p *Pool, sourceID uuid.UUID, name string) error
- func DeleteSource(ctx context.Context, p *Pool, accountID uuid.UUID, handle string) error
- func GeneratePlaintextKey() (string, error)
- func HashAPIKey(plaintext string) (string, error)
- func IsMemberOrOwner(ctx context.Context, p *Pool, accountID, userID uuid.UUID) bool
- func KeyPrefix(plaintext string) string
- func Migrate(ctx context.Context, p *Pool) error
- func RemoveMember(ctx context.Context, p *Pool, accountID, userID uuid.UUID) error
- func RevokeAPIKey(ctx context.Context, p *Pool, id uuid.UUID) error
- func RevokeInvite(ctx context.Context, p *Pool, inviteID uuid.UUID) error
- func SetNATSAccount(ctx context.Context, p *Pool, accountID uuid.UUID, ...) error
- func UpdateLastBroadcast(ctx context.Context, p *Pool, accountID uuid.UUID, handle string, at time.Time, ...) error
- func UsernamesByIDs(ctx context.Context, p *Pool, ids []uuid.UUID) (map[uuid.UUID]string, error)
- func VerifyAPIKey(plaintext, stored string) bool
- type APIKey
- func InsertAPIKey(ctx context.Context, p *Pool, accountID, createdBy uuid.UUID, label string) (key APIKey, plaintext string, err error)
- func ListAPIKeysForOrg(ctx context.Context, p *Pool, accountID uuid.UUID) ([]APIKey, error)
- func LookupAPIKey(ctx context.Context, p *Pool, plaintext string) (APIKey, error)
- type Account
- func FirstOwnedAccountFor(ctx context.Context, p *Pool, userID uuid.UUID) (Account, error)
- func GetAccount(ctx context.Context, p *Pool, id uuid.UUID) (Account, error)
- func GetAccountByName(ctx context.Context, p *Pool, name string) (Account, error)
- func InsertAccount(ctx context.Context, p *Pool, name string, ownerUserID uuid.UUID) (Account, error)
- func ListAccounts(ctx context.Context, p *Pool) ([]Account, error)
- func ListAccountsForUser(ctx context.Context, p *Pool, userID uuid.UUID) ([]Account, error)
- type DeviceCode
- type Invite
- type InviteStatus
- type InviteWithAccount
- type Member
- type OAuthToken
- type Pipe
- type Pool
- type Source
- func GetSourceByHandle(ctx context.Context, p *Pool, accountID uuid.UUID, handle string) (Source, error)
- func InsertSource(ctx context.Context, p *Pool, accountID, createdBy uuid.UUID, handle string, ...) (Source, error)
- func ListSourcesForOrg(ctx context.Context, p *Pool, accountID uuid.UUID) ([]Source, error)
- type SourceKind
- type User
- func GetUser(ctx context.Context, p *Pool, id uuid.UUID) (User, error)
- func GetUserByUsername(ctx context.Context, p *Pool, username string) (User, error)
- func InsertUser(ctx context.Context, p *Pool, username, email string, mode UserMode) (User, error)
- func ListMembers(ctx context.Context, p *Pool, accountID uuid.UUID) ([]User, error)
- func UpsertUserByGitHubID(ctx context.Context, p *Pool, githubID int64, ...) (User, bool, error)
- type UserMode
Constants ¶
This section is empty.
Variables ¶
var ( ErrDeviceCodePending = errors.New("device_code not yet verified") ErrDeviceCodeExpired = errors.New("device_code expired") )
var ErrAlreadyMember = errors.New("user is already a member of this account")
ErrAlreadyMember is returned by CreateInvite when the invitee is already a member or the owner of the org. Same intent as the duplicate-pending case but a different cause — surface separately.
var ErrCannotRemoveOwner = errors.New("cannot remove the account's owner")
ErrCannotRemoveOwner is returned by RemoveMember when the caller tries to remove the org's owner. v1 has no transfer-ownership path; the owner has to stay on the org until v2 lands transfer.
var ErrDuplicatePendingInvite = errors.New("a pending invite for this user already exists")
ErrDuplicatePendingInvite is returned by CreateInvite when an active pending invite for the same (org, username) already exists. The unique partial index enforces this server-side; we surface a typed sentinel so handlers can return a clean 409.
var ErrHandleTaken = errors.New("handle taken")
ErrHandleTaken is returned when a (org, handle) row already exists.
var ErrInvalidUserMode = errors.New("user mode must be 'github' or 'internal'")
ErrInvalidUserMode is returned when a caller passes a Mode value outside the {github, internal} CHECK constraint.
var ErrInviteNotPending = errors.New("invite is not pending")
ErrInviteNotPending is returned when a transition (accept/decline/ revoke) targets an invite that is no longer pending. Caller surfaces it as 409.
var ErrNotFound = errors.New("not found")
ErrNotFound is returned by lookups when the row does not exist.
var ErrPipeNameTaken = errors.New("pipe name taken")
ErrPipeNameTaken — (source_id, name) collision on insert.
Functions ¶
func AcceptInvite ¶ added in v0.22.0
AcceptInvite transitions a pending invite to accepted and adds the accepting user as a non-owner member of the org, atomically. The caller passes the accepting user's id; the function checks that their username matches the invite's invitee_username (so a logged-in user can't accept someone else's invite even if they know the id).
Returns:
- ErrNotFound if the invite id doesn't exist
- ErrInviteNotPending if the invite is no longer pending
- errors.New("forbidden") if userID's username != invitee_username
func AddMember ¶
AddMember records a user as a non-owner member of an org. Idempotent — re-adding an existing member is a no-op (UPSERT-style).
func ApproveDeviceCode ¶
ApproveDeviceCode marks the user_code as verified by userID. Idempotent. Returns ErrNotFound if user_code doesn't exist or is already expired.
func ConsumeDeviceCode ¶
ConsumeDeviceCode is the CLI poll path. Returns the user_id and marks the code consumed (single-use). State machine errors:
- ErrDeviceCodePending if not yet verified
- ErrDeviceCodeExpired if past TTL
- ErrNotFound if unknown / already consumed
func DeclineInvite ¶ added in v0.22.0
DeclineInvite transitions a pending invite to declined. Same permission check as Accept: caller's username must match the invite's invitee_username.
func DeletePipe ¶
DeletePipe removes the row. Returns ErrNotFound when (source, name) doesn't exist. Stream cleanup is the caller's responsibility (server-side).
func DeleteSource ¶ added in v0.21.0
DeleteSource removes a source row. The pipes FK is ON DELETE CASCADE so pipe rows are removed automatically. JetStream stream cleanup is the caller's responsibility. Returns ErrNotFound when (org, handle) doesn't exist.
func GeneratePlaintextKey ¶
GeneratePlaintextKey returns a fresh plaintext API key. Format: "ppz_<26 hex chars>" (a UUIDv7 hex without dashes, prefixed). 30 chars total makes the 8-char display prefix human-meaningful while leaving plenty of entropy.
func HashAPIKey ¶
HashAPIKey produces a self-describing argon2id hash:
$argon2id$v=19$m=65536,t=1,p=4$<base64-salt>$<base64-tag>
func IsMemberOrOwner ¶
IsMemberOrOwner returns true if userID owns accountID or is a member. Used by /auth/exchange (Phase 3.5) to validate that a multi-account user is actually entitled to the account they're requesting a JWT for.
func KeyPrefix ¶
KeyPrefix is the first 8 characters AFTER the "ppz_" sentinel. Used for display in the GUI and the `ppz status` line. Never use the prefix for auth.
func Migrate ¶
Migrate runs every migration file in order, lexicographically by name. Each file uses IF NOT EXISTS / IF NOT EXISTS COLUMN clauses so re-running is idempotent.
func RemoveMember ¶
RemoveMember drops a non-owner from the org. Returns ErrCannotRemoveOwner when targetUserID matches the org's owner — caller surfaces it as 409. ErrNotFound when the user wasn't a member.
func RevokeAPIKey ¶
RevokeAPIKey marks the key revoked. Idempotent: revoking an already-revoked key is a no-op (returns nil). Returns ErrNotFound if no row matches the id.
func RevokeInvite ¶ added in v0.22.0
RevokeInvite transitions a pending invite to revoked. Owner-only: caller is expected to be the org's owner; this function does not re-check the role (handlers gate via owner-only middleware), but it does require the invite be in the pending state.
func SetNATSAccount ¶
func SetNATSAccount(ctx context.Context, p *Pool, accountID uuid.UUID, accountPub, accountJWT, signingSeed string) error
SetNATSAccount persists the Operator-signed Account JWT + the account's signing seed for an account row. Called once (lazily) on first /auth/exchange after Phase 3.5 — subsequent calls find the row already populated and skip.
func UpdateLastBroadcast ¶
func UpdateLastBroadcast(ctx context.Context, p *Pool, accountID uuid.UUID, handle string, at time.Time, payload string) error
UpdateLastBroadcast records the most recent broadcast for this source. Called by the server-side subscriber on every message. Idempotent on identical inputs.
func UsernamesByIDs ¶ added in v0.27.0
UsernamesByIDs resolves a set of user IDs to {id → username}. Used by the server's list endpoints that need to attribute every source/pipe row to a user (HUMAN column). Single round-trip via ANY($1::uuid[]). Missing IDs are simply absent from the returned map — callers should treat that as "" so a stale ID can't break rendering.
func VerifyAPIKey ¶
VerifyAPIKey checks plaintext against a stored hash in constant time.
Types ¶
type APIKey ¶
type APIKey struct {
ID uuid.UUID
AccountID uuid.UUID
CreatedByUserID uuid.UUID // user that minted the key (NOT NULL)
KeyHash string
KeyPrefix string
Label string
CreatedAt time.Time
// RevokedAt is nil for active keys, set to the revoke time once
// `POST /api/v1/keys/<id>/revoke` flips it. LookupAPIKey filters
// out revoked rows; the GUI shows them with strikethrough so the
// audit trail stays visible.
RevokedAt *time.Time
}
func InsertAPIKey ¶
func InsertAPIKey(ctx context.Context, p *Pool, accountID, createdBy uuid.UUID, label string) (key APIKey, plaintext string, err error)
InsertAPIKey mints a fresh plaintext key, hashes it, and writes the row. `createdBy` is the user who minted the key — required (NOT NULL on the table) so every key is attributable for `ppz ls` HUMAN.
func ListAPIKeysForOrg ¶
ListAPIKeysForOrg returns every key for the org, including revoked ones — the GUI shows revoked keys (with strikethrough) so the audit trail stays visible. Sorted active-first by creation time, then revoked rows.
func LookupAPIKey ¶
LookupAPIKey resolves a plaintext key to its row by scanning all keys with the matching 8-char prefix and verifying the hash. ErrNotFound when no match (including when the matching key has been revoked).
type Account ¶ added in v0.30.2
type Account struct {
ID uuid.UUID
Name string
OwnerUserID uuid.UUID
CreatedAt time.Time
// Auth V2 §Phase 3.5 — per-account NATS credential. NULL until the
// account's NATS credential is provisioned (lazy on first /auth/exchange).
NATSAccountPub string
NATSAccountJWT string
NATSAccountSigningSeed string
}
Account is the ppz-side tenancy boundary. Maps 1:1 to a NATS account (see SetNATSAccount below for the credential plumbing). Pre-launch this was called "Organisation"; the rename is part of the Phase 1 surface strip (see strategy doc OSS-PIPESCLOUD-ARCHITECTURE-SPLIT, private, locked decisions #11 and #18).
func FirstOwnedAccountFor ¶ added in v0.30.2
FirstOwnedAccountFor returns the account owned by userID. If they own multiple, returns the oldest. If they own none, returns ErrNotFound. Used by the OAuth path of requireAPIKey to pick a default account for callers who haven't yet specified one (Auth V2 Phase 2 interim; proper account-selection UX is V3).
func GetAccount ¶ added in v0.30.2
func GetAccountByName ¶ added in v0.30.2
GetAccountByName looks up an account by its unique name (used as a slug alias in the GUI: /accounts/alpha resolves the same as /accounts/<uuid>).
func InsertAccount ¶ added in v0.30.2
func InsertAccount(ctx context.Context, p *Pool, name string, ownerUserID uuid.UUID) (Account, error)
InsertAccount creates a new account owned by ownerUserID. If ownerUserID is uuid.Nil, the account defaults to the seeded unauthenticated user — preserves back-compat for tests + GUI callers that don't supply an owner yet.
func ListAccounts ¶ added in v0.30.2
func ListAccountsForUser ¶ added in v0.30.2
ListAccountsForUser returns the accounts userID owns or is a member of, ordered by name. Used by the GUI dashboard so each user sees only their own tenants instead of every account in the system.
type DeviceCode ¶
type DeviceCode struct {
DeviceCode string
UserCode string
ClientName string
UserID *uuid.UUID
ExpiresAt time.Time
VerifiedAt *time.Time
ConsumedAt *time.Time
CreatedAt time.Time
}
DeviceCode is the row stored in oauth_device_codes.
func CreateDeviceCode ¶
func CreateDeviceCode(ctx context.Context, p *Pool, ttl time.Duration, clientName string) (DeviceCode, error)
CreateDeviceCode mints a fresh pair, inserts the row, returns it. clientName is a free-form label the CLI sends so the verify page can name the calling app (e.g. "ppz CLI 0.15.0"); empty string is fine — the page falls back to generic copy.
func LookupDeviceCode ¶
type Invite ¶ added in v0.22.0
type Invite struct {
ID uuid.UUID
AccountID uuid.UUID
InviteeUsername string
InviterUserID uuid.UUID
Status InviteStatus
CreatedAt time.Time
DecidedAt *time.Time
}
func CreateInvite ¶ added in v0.22.0
func CreateInvite(ctx context.Context, p *Pool, accountID uuid.UUID, inviteeUsername string, inviterUserID uuid.UUID) (Invite, error)
CreateInvite inserts a pending invite for inviteeUsername into accountID, recording inviterUserID as the sender. Pre-flights two error conditions before the insert:
- inviteeUsername is already a member or the owner of accountID → ErrAlreadyMember
- a pending invite for the same (org, username) already exists → ErrDuplicatePendingInvite
Pre-flighting the duplicate case in addition to relying on the partial unique index gives us a typed error without inspecting pg error codes.
type InviteStatus ¶ added in v0.22.0
type InviteStatus string
InviteStatus values mirror the CHECK constraint on invites.status.
const ( InviteStatusPending InviteStatus = "pending" InviteStatusAccepted InviteStatus = "accepted" InviteStatusDeclined InviteStatus = "declined" InviteStatusRevoked InviteStatus = "revoked" )
type InviteWithAccount ¶ added in v0.30.2
InviteWithAccount is the dashboard projection — invite plus org name so the user can see where the invite came from without a second query.
func ListPendingInvitesForUsername ¶ added in v0.22.0
func ListPendingInvitesForUsername(ctx context.Context, p *Pool, username string) ([]InviteWithAccount, error)
ListPendingInvitesForUsername returns all pending invites whose invitee_username matches. Joined with accounts so callers can show the org name on the dashboard.
type OAuthToken ¶
type OAuthToken struct {
ID uuid.UUID
UserID uuid.UUID
TokenHash string
Prefix string
ExpiresAt time.Time
RevokedAt *time.Time
CreatedAt time.Time
LastUsedAt *time.Time
}
func IssueBearerToken ¶
func LookupBearerToken ¶
type Pipe ¶
type Pipe struct {
ID uuid.UUID
SourceID uuid.UUID
CreatedByUserID uuid.UUID // user that created the pipe (NOT NULL)
Name string
TTLSeconds *int // nil = use server default (86400 s)
MaxMsgs *int // nil = use server default (1000)
MaxBytes *int64 // nil = use server default (64 MiB)
CreatedAt time.Time
}
Pipe is one user-creatable sub-bucket on a source. Auto-provisioned pipes (broadcast, stdin, stdout) are NOT stored here — they're derived from the source's kind and joined in at API response time.
func GetPipeByName ¶
GetPipeByName returns one pipe row or ErrNotFound.
func InsertPipe ¶
func InsertPipe(ctx context.Context, p *Pool, sourceID, createdBy uuid.UUID, name string, ttl *int, maxMsgs *int, maxBytes *int64) (Pipe, error)
InsertPipe inserts a row. Retention overrides are NULL when the pointer arg is nil — the server provisions the JetStream stream with default values for any nil fields. `createdBy` is the user that created the pipe (NOT NULL on the table); rendered as HUMAN by `ppz ls`.
type Pool ¶
Pool is the public handle other packages use. It wraps pgxpool.Pool so the rest of the codebase doesn't import pgx directly.
type Source ¶
type Source struct {
ID uuid.UUID
AccountID uuid.UUID
CreatedByUserID uuid.UUID // user that created the source (NOT NULL)
Handle string
Kind SourceKind
CreatedAt time.Time
LastBroadcastAt *time.Time
LastBroadcastPayload *string
}
func GetSourceByHandle ¶
func InsertSource ¶
func InsertSource(ctx context.Context, p *Pool, accountID, createdBy uuid.UUID, handle string, kind SourceKind) (Source, error)
InsertSource creates a row attributed to `createdBy` (NOT NULL on the table). Server callers stamp this from the API-key's CreatedByUserID (API path) or caller.UserID (OAuth path).
func ListSourcesForOrg ¶
func (Source) IsAutoPipe ¶ added in v0.22.1
IsAutoPipe reports whether name is an auto-provisioned pipe for this source kind (i.e. JetStream-only, not stored in the pipes table).
func (Source) Pipes ¶
Pipes returns the pipe set for a source based on its kind. All sources have:
- inbox: direct messages intended for this source/agent
pty sources also have:
- stdin: input fed to the wrapped child via `ppz send`
- stdout: byte-faithful capture of the PTY master's output (ANSI escapes intact); both `ppz read` and `ppz terminal view` consume this pipe.
- stdctrl: control plane (resize events, etc.).
`broadcast` was an auto-provisioned pipe pre-launch; it was removed in Phase 1 (locked decision #16) — teams now use explicit room pipes (`ppz pipe create team1.room` with --writers=anyone) for shared channels.
type SourceKind ¶
type SourceKind string
SourceKind enumerates the supported source shapes.
"message" — default; two pipes: broadcast, inbox. "pty" — terminal source; broadcast, inbox, and terminal IO pipes.
const ( SourceKindMessage SourceKind = "message" SourceKindPTY SourceKind = "pty" )
type User ¶
type User struct {
ID uuid.UUID
Username string
Email string
Mode UserMode
GitHubID *int64 // nil for mode=internal users
AvatarURL string
CreatedAt time.Time
}
func GetUserByUsername ¶
func InsertUser ¶
func ListMembers ¶
ListMembers returns the non-owner members of an org, ordered by when they were added.
func UpsertUserByGitHubID ¶
func UpsertUserByGitHubID(ctx context.Context, p *Pool, githubID int64, username, email, avatarURL string) (User, bool, error)
UpsertUserByGitHubID inserts a brand new mode=github user, or updates the existing row matching the GitHub numeric id. Returns the resolved User row plus a bool indicating whether the row was freshly created (true) vs already existed (false). Callers use the bool to decide whether to auto-create the user's first org.