db

package
v0.30.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package db owns the postgres connection pool, migrations, and per-table repositories.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrDeviceCodePending = errors.New("device_code not yet verified")
	ErrDeviceCodeExpired = errors.New("device_code expired")
)
View Source
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.

View Source
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.

View Source
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.

View Source
var ErrHandleTaken = errors.New("handle taken")

ErrHandleTaken is returned when a (org, handle) row already exists.

View Source
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.

View Source
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.

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound is returned by lookups when the row does not exist.

View Source
var ErrPipeNameTaken = errors.New("pipe name taken")

ErrPipeNameTaken — (source_id, name) collision on insert.

Functions

func AcceptInvite added in v0.22.0

func AcceptInvite(ctx context.Context, p *Pool, inviteID, acceptingUserID uuid.UUID) error

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

func AddMember(ctx context.Context, p *Pool, accountID, userID uuid.UUID) error

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

func ApproveDeviceCode(ctx context.Context, p *Pool, userCode string, userID uuid.UUID) error

ApproveDeviceCode marks the user_code as verified by userID. Idempotent. Returns ErrNotFound if user_code doesn't exist or is already expired.

func ConsumeDeviceCode

func ConsumeDeviceCode(ctx context.Context, p *Pool, deviceCode string) (uuid.UUID, error)

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

func DeclineInvite(ctx context.Context, p *Pool, inviteID, decliningUserID uuid.UUID) error

DeclineInvite transitions a pending invite to declined. Same permission check as Accept: caller's username must match the invite's invitee_username.

func DeletePipe

func DeletePipe(ctx context.Context, p *Pool, sourceID uuid.UUID, name string) error

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

func DeleteSource(ctx context.Context, p *Pool, accountID uuid.UUID, handle string) error

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

func GeneratePlaintextKey() (string, error)

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

func HashAPIKey(plaintext string) (string, error)

HashAPIKey produces a self-describing argon2id hash:

$argon2id$v=19$m=65536,t=1,p=4$<base64-salt>$<base64-tag>

func IsMemberOrOwner

func IsMemberOrOwner(ctx context.Context, p *Pool, accountID, userID uuid.UUID) bool

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

func KeyPrefix(plaintext string) string

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

func Migrate(ctx context.Context, p *Pool) error

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

func RemoveMember(ctx context.Context, p *Pool, accountID, userID uuid.UUID) error

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

func RevokeAPIKey(ctx context.Context, p *Pool, id uuid.UUID) error

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

func RevokeInvite(ctx context.Context, p *Pool, inviteID uuid.UUID) error

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

func UsernamesByIDs(ctx context.Context, p *Pool, ids []uuid.UUID) (map[uuid.UUID]string, error)

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

func VerifyAPIKey(plaintext, stored string) bool

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

func ListAPIKeysForOrg(ctx context.Context, p *Pool, accountID uuid.UUID) ([]APIKey, error)

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

func LookupAPIKey(ctx context.Context, p *Pool, plaintext string) (APIKey, error)

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).

func (APIKey) Revoked

func (k APIKey) Revoked() bool

Revoked is a small accessor — useful from html/template, which can't dereference pointers cleanly.

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

func FirstOwnedAccountFor(ctx context.Context, p *Pool, userID uuid.UUID) (Account, error)

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 GetAccount(ctx context.Context, p *Pool, id uuid.UUID) (Account, error)

func GetAccountByName added in v0.30.2

func GetAccountByName(ctx context.Context, p *Pool, name string) (Account, error)

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 ListAccounts(ctx context.Context, p *Pool) ([]Account, error)

func ListAccountsForUser added in v0.30.2

func ListAccountsForUser(ctx context.Context, p *Pool, userID uuid.UUID) ([]Account, error)

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

func LookupDeviceCode(ctx context.Context, p *Pool, deviceCode string) (DeviceCode, error)

func LookupDeviceCodeByUserCode

func LookupDeviceCodeByUserCode(ctx context.Context, p *Pool, userCode string) (DeviceCode, error)

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.

func GetInvite added in v0.22.0

func GetInvite(ctx context.Context, p *Pool, id uuid.UUID) (Invite, error)

GetInvite fetches a single invite by id. Returns ErrNotFound if the row doesn't exist.

func ListInvitesForOrg added in v0.22.0

func ListInvitesForOrg(ctx context.Context, p *Pool, accountID uuid.UUID) ([]Invite, error)

ListInvitesForOrg returns every invite (any status) for accountID, newest first. Used by the org page to show the invite history.

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

type InviteWithAccount struct {
	Invite
	AccountName string
}

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 Member

type Member struct {
	AccountID uuid.UUID
	UserID    uuid.UUID
	AddedAt   time.Time
}

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 IssueBearerToken(ctx context.Context, p *Pool, userID uuid.UUID, ttl time.Duration) (string, OAuthToken, error)

func LookupBearerToken

func LookupBearerToken(ctx context.Context, p *Pool, plaintext string) (OAuthToken, error)

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

func GetPipeByName(ctx context.Context, p *Pool, sourceID uuid.UUID, name string) (Pipe, error)

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`.

func ListPipesForSource

func ListPipesForSource(ctx context.Context, p *Pool, sourceID uuid.UUID) ([]Pipe, error)

ListPipesForSource returns the user-creatable pipes for one source, sorted by name. Excludes auto-provisioned pipes (those aren't stored).

type Pool

type Pool struct {
	*pgxpool.Pool
}

Pool is the public handle other packages use. It wraps pgxpool.Pool so the rest of the codebase doesn't import pgx directly.

func Open

func Open(ctx context.Context, url string) (*Pool, error)

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 GetSourceByHandle(ctx context.Context, p *Pool, accountID uuid.UUID, handle string) (Source, error)

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 ListSourcesForOrg(ctx context.Context, p *Pool, accountID uuid.UUID) ([]Source, error)

func (Source) IsAutoPipe added in v0.22.1

func (s Source) IsAutoPipe(name string) bool

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

func (s Source) Pipes() []string

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 GetUser

func GetUser(ctx context.Context, p *Pool, id uuid.UUID) (User, error)

func GetUserByUsername

func GetUserByUsername(ctx context.Context, p *Pool, username string) (User, error)

func InsertUser

func InsertUser(ctx context.Context, p *Pool, username, email string, mode UserMode) (User, error)

func ListMembers

func ListMembers(ctx context.Context, p *Pool, accountID uuid.UUID) ([]User, error)

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.

type UserMode

type UserMode string

UserMode = "github" (real OAuth identity) or "internal" (placeholder / seeded test user / pre-OAuth-era account).

const (
	UserModeGithub   UserMode = "github"
	UserModeInternal UserMode = "internal"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL