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, orgID, 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, orgID uuid.UUID, handle string) error
- func GeneratePlaintextKey() (string, error)
- func HashAPIKey(plaintext string) (string, error)
- func IsMemberOrOwner(ctx context.Context, p *Pool, orgID, userID uuid.UUID) bool
- func KeyPrefix(plaintext string) string
- func Migrate(ctx context.Context, p *Pool) error
- func RemoveMember(ctx context.Context, p *Pool, orgID, 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, orgID uuid.UUID, ...) error
- func UpdateLastBroadcast(ctx context.Context, p *Pool, orgID uuid.UUID, handle string, at time.Time, ...) error
- func VerifyAPIKey(plaintext, stored string) bool
- type APIKey
- type DeviceCode
- type Invite
- type InviteStatus
- type InviteWithOrg
- type Member
- type OAuthToken
- type Organisation
- func FirstOwnedOrgFor(ctx context.Context, p *Pool, userID uuid.UUID) (Organisation, error)
- func GetOrganisation(ctx context.Context, p *Pool, id uuid.UUID) (Organisation, error)
- func GetOrganisationByName(ctx context.Context, p *Pool, name string) (Organisation, error)
- func InsertOrganisation(ctx context.Context, p *Pool, name string, ownerUserID uuid.UUID) (Organisation, error)
- func ListOrganisations(ctx context.Context, p *Pool) ([]Organisation, error)
- func ListOrganisationsForUser(ctx context.Context, p *Pool, userID uuid.UUID) ([]Organisation, error)
- type Pipe
- type Pool
- type Source
- func GetSourceByHandle(ctx context.Context, p *Pool, orgID uuid.UUID, handle string) (Source, error)
- func InsertSource(ctx context.Context, p *Pool, orgID uuid.UUID, handle string, kind SourceKind) (Source, error)
- func ListSourcesForOrg(ctx context.Context, p *Pool, orgID 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, orgID 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 organisation")
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 organisation'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 orgID or is a member. Used by /auth/exchange (Phase 3.5) to validate that a multi-org user is actually entitled to the org 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, orgID uuid.UUID, accountPub, accountJWT, signingSeed string) error
SetNATSAccount persists the Operator-signed Account JWT + the account's signing seed for an org. 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, orgID 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 VerifyAPIKey ¶
VerifyAPIKey checks plaintext against a stored hash in constant time.
Types ¶
type APIKey ¶
type APIKey struct {
ID uuid.UUID
OrganisationID uuid.UUID
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 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 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
OrganisationID 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, orgID uuid.UUID, inviteeUsername string, inviterUserID uuid.UUID) (Invite, error)
CreateInvite inserts a pending invite for inviteeUsername into orgID, recording inviterUserID as the sender. Pre-flights two error conditions before the insert:
- inviteeUsername is already a member or the owner of orgID → 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 InviteWithOrg ¶ added in v0.22.0
InviteWithOrg 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) ([]InviteWithOrg, error)
ListPendingInvitesForUsername returns all pending invites whose invitee_username matches. Joined with organisations 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 Organisation ¶
type Organisation struct {
ID uuid.UUID
Name string
OwnerUserID uuid.UUID
CreatedAt time.Time
// Auth V2 §Phase 3.5 — per-org NATS account. NULL until the org's
// account is provisioned (lazy on first /auth/exchange).
NATSAccountPub string
NATSAccountJWT string
NATSAccountSigningSeed string
}
func FirstOwnedOrgFor ¶
FirstOwnedOrgFor returns the org 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 org for callers who haven't yet specified one (Auth V2 Phase 2 interim; proper org-selection UX is V3).
func GetOrganisation ¶
func GetOrganisationByName ¶
GetOrganisationByName looks up an org by its unique name (used as a slug alias in the GUI: /orgs/alpha resolves the same as /orgs/<uuid>).
func InsertOrganisation ¶
func InsertOrganisation(ctx context.Context, p *Pool, name string, ownerUserID uuid.UUID) (Organisation, error)
InsertOrganisation creates a new org owned by ownerUserID. If ownerUserID is uuid.Nil, the org defaults to the seeded unauthenticated user — preserves back-compat for tests + GUI callers that don't supply an owner yet.
func ListOrganisations ¶
func ListOrganisations(ctx context.Context, p *Pool) ([]Organisation, error)
func ListOrganisationsForUser ¶
func ListOrganisationsForUser(ctx context.Context, p *Pool, userID uuid.UUID) ([]Organisation, error)
ListOrganisationsForUser returns the orgs 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 org in the system.
type Pipe ¶
type Pipe struct {
ID uuid.UUID
SourceID uuid.UUID
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 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.
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
OrganisationID uuid.UUID
Handle string
Kind SourceKind
CreatedAt time.Time
LastBroadcastAt *time.Time
LastBroadcastPayload *string
}
func GetSourceByHandle ¶
func InsertSource ¶
func ListSourcesForOrg ¶
func (Source) Pipes ¶
Pipes returns the pipe set for a source based on its kind. All sources have:
- broadcast: user-level messages (same as message-kind sources)
- 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.
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.