db

package
v0.25.0 Latest Latest
Warning

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

Go to latest
Published: May 8, 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 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.

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

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, orgID, 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, orgID 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, orgID, userID uuid.UUID) bool

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

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, orgID, 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, 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

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
	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 InsertAPIKey(ctx context.Context, p *Pool, orgID uuid.UUID, label string) (key APIKey, plaintext string, err error)

func ListAPIKeysForOrg

func ListAPIKeysForOrg(ctx context.Context, p *Pool, orgID 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 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
	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.

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, orgID uuid.UUID) ([]Invite, error)

ListInvitesForOrg returns every invite (any status) for orgID, 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 InviteWithOrg added in v0.22.0

type InviteWithOrg struct {
	Invite
	OrganisationName string
}

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 Member

type Member struct {
	OrganisationID 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 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

func FirstOwnedOrgFor(ctx context.Context, p *Pool, userID uuid.UUID) (Organisation, error)

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

func GetOrganisationByName

func GetOrganisationByName(ctx context.Context, p *Pool, name string) (Organisation, error)

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

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

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
	OrganisationID       uuid.UUID
	Handle               string
	Kind                 SourceKind
	CreatedAt            time.Time
	LastBroadcastAt      *time.Time
	LastBroadcastPayload *string
}

func GetSourceByHandle

func GetSourceByHandle(ctx context.Context, p *Pool, orgID uuid.UUID, handle string) (Source, error)

func InsertSource

func InsertSource(ctx context.Context, p *Pool, orgID uuid.UUID, handle string, kind SourceKind) (Source, error)

func ListSourcesForOrg

func ListSourcesForOrg(ctx context.Context, p *Pool, orgID 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:

  • 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 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, orgID 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