database

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: AGPL-3.0 Imports: 20 Imported by: 0

Documentation

Overview

Package database — query tracer.

queryTracer implements the pgx v5 QueryTracer interface and records two metrics for every query the driver executes:

  • llmsafespaces_db_query_duration_seconds{operation}
  • llmsafespaces_db_errors_total{operation,error_type}

It is attached to both the *sql.DB pool (via stdlib.OpenDB + pgx.ConnConfig.Tracer) and the *pgxpool.Pool used by the secrets store (via pgxpool.Config.ConnConfig.Tracer). Every SQL statement issued by the API binary therefore flows through one tracer and one metric API, which is the long-term-correct alternative to wrapping every call site or registering a database/sql wrapper driver.

Index

Constants

This section is empty.

Variables

View Source
var ErrTokenAlreadyConsumed = fmt.Errorf("token already consumed")

ErrTokenAlreadyConsumed is returned when ConsumeEmailToken affects 0 rows (the token was consumed by a concurrent request between Get and Consume — TOCTOU race). The handler maps this to 410 Gone.

Functions

func NewQueryTracer

func NewQueryTracer() pgx.QueryTracer

NewQueryTracer returns the QueryTracer used by the API's *sql.DB pool. Exported so callers (e.g. the secrets pgxpool initialized in internal/app) can attach the same tracer to their own pool, ensuring every query — regardless of which pool issues it — flows through one metrics path. The returned value implements pgx.QueryTracer.

Types

type OrgStore

type OrgStore interface {
	CreateOrgWithAdmin(ctx context.Context, org *types.Organization, adminUserID string) (*types.Organization, error)
	GetOrg(ctx context.Context, orgID string) (*types.Organization, error)
	GetOrgBySlug(ctx context.Context, slug string) (*types.Organization, error)
	ListOrgsForUser(ctx context.Context, userID string) ([]*types.OrgResponse, error)
	UpdateOrg(ctx context.Context, orgID string, req types.UpdateOrgRequest) (*types.Organization, error)
	SoftDeleteOrg(ctx context.Context, orgID string) error

	AddOrgMember(ctx context.Context, orgID, userID string, role types.OrgRole) error
	GetOrgMember(ctx context.Context, orgID, userID string) (*types.OrgMember, error)
	ListOrgMembers(ctx context.Context, orgID string) ([]*types.OrgMember, error)
	// CountOrgAdmins returns the number of admin members in an active (non-
	// deleted) org. Used by the SSO login flow to avoid demoting the last admin
	// on an IdP-driven role change (org orphaning prevention, cf. D19).
	CountOrgAdmins(ctx context.Context, orgID string) (int, error)
	UpdateOrgMemberRole(ctx context.Context, orgID, userID string, role types.OrgRole) error
	RemoveOrgMember(ctx context.Context, orgID, userID string) error
	RemoveOrgAdminIfNotLast(ctx context.Context, orgID, targetUserID string) (bool, error)
	DemoteOrgAdminIfNotLast(ctx context.Context, orgID, targetUserID string) (bool, error)
	IsOrgMember(ctx context.Context, orgID, userID string) (bool, error)
	IsOrgAdmin(ctx context.Context, orgID, userID string) (bool, error)
	ListOrgWorkspaces(ctx context.Context, orgID string, limit, offset int) ([]*types.WorkspaceMetadata, *types.PaginationMetadata, error)
	// GetUserIDByEmail resolves an owner email to a user ID for admin-driven org
	// creation (design 0031 D1). Returns ("", nil) when no user matches. This is
	// a single targeted lookup, never a search/list endpoint, to prevent account
	// enumeration. Users are hard-deleted (no deleted_at column), so no soft-delete
	// filter is needed.
	GetUserIDByEmail(ctx context.Context, email string) (string, error)
	// GetUserEmail resolves a user ID to their email (inverse of GetUserIDByEmail).
	// Used by invitation acceptance to verify email binding.
	GetUserEmail(ctx context.Context, userID string) (string, error)
	// MarkUserEmailVerified sets users.email_verified=true for the given user,
	// bypassing the email-verification token flow. Used by the org-admin
	// "Verify" action when an admin has confirmed the member's identity
	// out-of-band. Idempotent.
	MarkUserEmailVerified(ctx context.Context, userID string) error
	// GetUserOrgID returns the user's single org ID (or "" if not in any org).
	// With single-org enforcement (D8), a user belongs to at most one org. Used
	// by invitation acceptance (S3 cross-org check) and workspace auto-attribution
	// (D4). Returns ("", nil) on no membership. S7 in 0034.
	GetUserOrgID(ctx context.Context, userID string) (string, error)

	// US-43.1: Stripe lifecycle. UpdateOrgStatus sets the operational status
	// (active/suspended) and/or subscription_status and/or plan_id. A nil/empty
	// argument leaves the column unchanged.
	UpdateOrgStatus(ctx context.Context, orgID string, status *types.OrgStatus, subStatus *types.OrgSubscriptionStatus, planID *types.OrgPlan) error
	// GetOrgIDByStripeCustomer resolves a Stripe customer ID to the owning org's
	// ID via billing_accounts. Returns ("", nil) when no row matches.
	GetOrgIDByStripeCustomer(ctx context.Context, stripeCustomerID string) (string, error)
	// GetStripeCustomerID resolves an org to its Stripe customer id via
	// billing_accounts. Returns ("", nil) when no billing account exists.
	GetStripeCustomerID(ctx context.Context, orgID string) (string, error)
	// --- US-43.2: Invitations ---
	CreateInvitation(ctx context.Context, inv *types.OrgInvitation) error
	ListPendingInvitations(ctx context.Context, orgID string) ([]*types.OrgInvitation, error)
	GetInvitationByTokenHash(ctx context.Context, tokenHash string) (*types.OrgInvitation, error)
	GetInvitationByID(ctx context.Context, invID string) (*types.OrgInvitation, error)
	// AcceptInvitation performs the accept flow atomically under FOR UPDATE:
	// locks the invitation row, re-checks it is still pending, inserts the
	// membership, and marks the invitation accepted. Returns
	// (membership, alreadyAccepted, error).
	AcceptInvitationTx(ctx context.Context, invID, userID string, role types.OrgRole) (*types.OrgMember, bool, error)
	DeclineInvitation(ctx context.Context, invID string) error
	DeleteInvitation(ctx context.Context, invID string) error
	CountInvitationsLastHour(ctx context.Context, orgID string) (int, error)

	// --- US-43.7: Policies ---
	GetOrgPolicies(ctx context.Context, orgID string) ([]*types.OrgPolicy, error)
	SetOrgPolicy(ctx context.Context, orgID string, key types.OrgPolicyKey, value json.RawMessage, updatedBy string) error
	DeleteOrgPolicy(ctx context.Context, orgID string, key types.OrgPolicyKey) error

	// --- US-43.13: Org-scoped audit log ---
	LogOrgEvent(ctx context.Context, orgID, actorID, action, targetID string, metadata map[string]any) error
	// LogAuditEvent is the general audit writer (US-43.19). domain must be one
	// of audit_log_domain_chk's allowed values; orgID is nil for platform-level
	// (non-org-scoped) events.
	LogAuditEvent(ctx context.Context, domain, actorID, action, targetID string, orgID *string, metadata map[string]any) error
	ListOrgAudit(ctx context.Context, orgID string, limit, offset int) ([]*types.AuditEntry, *types.PaginationMetadata, error)
	// US-43.20: cross-org audit. ListAllAudit returns audit_log rows across all
	// orgs, narrowed by the supplied filters (nil pointers ⇒ no filter). Limit
	// defaults to 100 and is clamped to [1, 500]; Offset defaults to 0.
	ListAllAudit(ctx context.Context, filters types.AuditFilters) ([]*types.AuditEntry, *types.PaginationMetadata, error)
	// US-43.19: last-admin deadlock prevention. Returns orgs where the given
	// user is the sole active admin — suspending them would orphan the org.
	OrgsWhereUserIsLastActiveAdmin(ctx context.Context, userID string) ([]types.LastAdminOrg, error)
	// US-43.18: platform-admin dashboard. ListAllOrgs returns every
	// non-deleted org with aggregated member + workspace counts, optionally
	// narrowed by status. statusFilter is applied only when non-nil/non-empty.
	// limit is clamped to [1, adminListMaxLimit]; offset defaults to 0.
	ListAllOrgs(ctx context.Context, limit, offset int, statusFilter *string) ([]types.OrgSummary, *types.PaginationMetadata, error)

	// --- US-43.10: OIDC SSO configuration ---
	// GetSSOConfig returns the org's SSO config or (nil, nil) when none exists.
	GetSSOConfig(ctx context.Context, orgID string) (*types.OrgSSOConfig, error)
	// UpsertSSOConfig inserts or replaces the org's SSO config. ClientSecret is
	// the already-encrypted blob (server KEK, D17-S4). VerifiedDomains is the
	// caller-computed subset of ClaimedDomains that remain verified after this
	// update (the service layer intersects existing verified with new claimed).
	// VerificationToken is generated on INSERT if empty; ON CONFLICT preserves
	// the existing token (rotation is via RotateVerificationToken).
	UpsertSSOConfig(ctx context.Context, config *types.OrgSSOConfig) error
	// DeleteSSOConfig removes the org's SSO config.
	DeleteSSOConfig(ctx context.Context, orgID string) error
	// FindSSOConfigByDomain resolves a claimed email domain (without leading
	// "@") to the owning org's SSO config. Returns (nil, nil) when no org has
	// claimed the domain.
	FindSSOConfigByDomain(ctx context.Context, domain string) (*types.OrgSSOConfig, error)
	// ListSSODomains returns every DNS-verified domain across all orgs, for
	// the login-page discovery endpoint. Unverified claimed domains are NOT
	// returned — they cannot auto-route until the org admin completes DNS
	// verification (D17 Q-S2).
	ListSSODomains(ctx context.Context) ([]types.SSODomain, error)
	// CountSSOConfigs returns the number of orgs with an SSO config. Used by
	// GET /auth/config to set the OIDCEnabled feature flag.
	CountSSOConfigs(ctx context.Context) (int, error)
	// SetDomainVerified atomically appends a domain to verified_domains. The
	// domain MUST already be in claimed_domains (enforced by the WHERE clause);
	// a domain not in claimed_domains is silently not added. Idempotent: adding
	// an already-verified domain is a no-op. Returns (true, nil) if the domain
	// was newly verified, (false, nil) if it was already verified or not claimed.
	SetDomainVerified(ctx context.Context, orgID, domain string) (bool, error)
	// RotateVerificationToken replaces the org's DNS verification token with a
	// fresh random value and returns it. Used both for initial token creation
	// (when verification_token is NULL) and for rotation. Old tokens stop
	// matching after rotation — admins must update their DNS TXT record.
	RotateVerificationToken(ctx context.Context, orgID string) (string, error)
}

OrgStore is the data-access interface for organizations and their memberships.

type PendingOrgCleanup

type PendingOrgCleanup struct {
	OrgID            string
	Slug             string
	CreatedAt        time.Time
	StripeCustomerID string
}

PendingOrgCleanup describes a pending_activation org eligible for the cleanup cron. StripeCustomerID lets the cron verify checkout state with Stripe before deleting.

type PgEmailTokenStore

type PgEmailTokenStore struct {
	// contains filtered or unexported fields
}

PgEmailTokenStore implements the email-token CRUD against PostgreSQL.

func NewPgEmailTokenStore

func NewPgEmailTokenStore(db *sql.DB) *PgEmailTokenStore

func (*PgEmailTokenStore) ConsumeEmailToken

func (s *PgEmailTokenStore) ConsumeEmailToken(ctx context.Context, id string) error

func (*PgEmailTokenStore) CreateEmailToken

func (s *PgEmailTokenStore) CreateEmailToken(ctx context.Context, t *types.EmailToken) error

func (*PgEmailTokenStore) GetEmailTokenByHash

func (s *PgEmailTokenStore) GetEmailTokenByHash(ctx context.Context, hash string) (*types.EmailToken, error)

type PgOrgStore

type PgOrgStore struct {
	// contains filtered or unexported fields
}

PgOrgStore implements OrgStore using database/sql.

func NewPgOrgStore

func NewPgOrgStore(db *sql.DB) *PgOrgStore

NewPgOrgStore creates a new PgOrgStore.

func (*PgOrgStore) AcceptInvitationTx

func (s *PgOrgStore) AcceptInvitationTx(ctx context.Context, invID, userID string, role types.OrgRole) (*types.OrgMember, bool, error)

func (*PgOrgStore) AddOrgMember

func (s *PgOrgStore) AddOrgMember(ctx context.Context, orgID, userID string, role types.OrgRole) error

AddOrgMember inserts an org_memberships row AND migrates the new member's personal (NULL-org_id, non-deleted) workspaces to the org — mirroring CreateOrgWithAdmin (M1/D4) and AcceptInvitationTx (D4). Both operations run in a single transaction so the membership and the workspace migration commit atomically.

Callers (audited 2026-06-25):

  • admin "Add member" UI → POST /orgs/:id/members → orgs.go handler
  • SSO JIT provisioning during first login (sso.go)

Pre-fix: this function only INSERTed the membership row. The new member's existing workspaces stayed with org_id IS NULL forever, silently skipping org-credential auto-binding via BindCredentialToAllOrgWorkspaces (which filters w.org_id = $orgID). That produced the same orphan state migration 000044 was created to backfill. See the worklog `add-org-member-migrates-workspaces` for the discovery trail.

func (*PgOrgStore) ClearWorkspaceAgentRole

func (s *PgOrgStore) ClearWorkspaceAgentRole(ctx context.Context, workspaceID, userID string) error

ClearWorkspaceAgentRole removes the role assignment from a workspace (sets agent_role_id = NULL). Used by the "use platform default" action.

func (*PgOrgStore) CountInvitationsLastHour

func (s *PgOrgStore) CountInvitationsLastHour(ctx context.Context, orgID string) (int, error)

func (*PgOrgStore) CountOrgAdmins

func (s *PgOrgStore) CountOrgAdmins(ctx context.Context, orgID string) (int, error)

func (*PgOrgStore) CountSSOConfigs

func (s *PgOrgStore) CountSSOConfigs(ctx context.Context) (int, error)

func (*PgOrgStore) CreateAgentRole

func (s *PgOrgStore) CreateAgentRole(ctx context.Context, role *types.AgentRole, configJSON []byte) (*types.AgentRole, error)

CreateAgentRole inserts a new agent role.

func (*PgOrgStore) CreateInvitation

func (s *PgOrgStore) CreateInvitation(ctx context.Context, inv *types.OrgInvitation) error

func (*PgOrgStore) CreateOrgWithAdmin

func (s *PgOrgStore) CreateOrgWithAdmin(ctx context.Context, org *types.Organization, adminUserID string) (*types.Organization, error)

func (*PgOrgStore) DeclineInvitation

func (s *PgOrgStore) DeclineInvitation(ctx context.Context, invID string) error

func (*PgOrgStore) DeleteAgentRole

func (s *PgOrgStore) DeleteAgentRole(ctx context.Context, roleID string) error

DeleteAgentRole deletes a role. Caller must check dependents first.

func (*PgOrgStore) DeleteInvitation

func (s *PgOrgStore) DeleteInvitation(ctx context.Context, invID string) error

func (*PgOrgStore) DeleteOrgPolicy

func (s *PgOrgStore) DeleteOrgPolicy(ctx context.Context, orgID string, key types.OrgPolicyKey) error

func (*PgOrgStore) DeleteSSOConfig

func (s *PgOrgStore) DeleteSSOConfig(ctx context.Context, orgID string) error

func (*PgOrgStore) DeleteStripeEvent

func (s *PgOrgStore) DeleteStripeEvent(ctx context.Context, eventID string) error

func (*PgOrgStore) DeleteWorkspacePrompt

func (s *PgOrgStore) DeleteWorkspacePrompt(ctx context.Context, workspaceID string) error

DeleteWorkspacePrompt removes the user-level prompt override.

func (*PgOrgStore) DemoteOrgAdminIfNotLast

func (s *PgOrgStore) DemoteOrgAdminIfNotLast(ctx context.Context, orgID, targetUserID string) (bool, error)

func (*PgOrgStore) FindSSOConfigByDomain

func (s *PgOrgStore) FindSSOConfigByDomain(ctx context.Context, domain string) (*types.OrgSSOConfig, error)

FindSSOConfigByDomain resolves a claimed email domain (without leading "@") to the owning org's SSO config. Returns (nil, nil) when no org has claimed the domain.

NOTE: this matches on claimed_domains (NOT verified_domains). It is intended for internal lookups where the org has been identified by other means and its full config is needed regardless of verification status. The login-page auto-routing path uses ListSSODomains (which filters on verified_domains). If you wire this into login routing, you MUST add a verified_domains check or you bypass the DNS verification gate this migration introduces.

func (*PgOrgStore) GetAgentRole

func (s *PgOrgStore) GetAgentRole(ctx context.Context, roleID string) (*types.AgentRole, error)

GetAgentRole retrieves a single agent role by ID.

func (*PgOrgStore) GetInvitationByID

func (s *PgOrgStore) GetInvitationByID(ctx context.Context, invID string) (*types.OrgInvitation, error)

func (*PgOrgStore) GetInvitationByTokenHash

func (s *PgOrgStore) GetInvitationByTokenHash(ctx context.Context, tokenHash string) (*types.OrgInvitation, error)

func (*PgOrgStore) GetOrg

func (s *PgOrgStore) GetOrg(ctx context.Context, orgID string) (*types.Organization, error)

func (*PgOrgStore) GetOrgBySlug

func (s *PgOrgStore) GetOrgBySlug(ctx context.Context, slug string) (*types.Organization, error)

func (*PgOrgStore) GetOrgIDByStripeCustomer

func (s *PgOrgStore) GetOrgIDByStripeCustomer(ctx context.Context, stripeCustomerID string) (string, error)

func (*PgOrgStore) GetOrgMember

func (s *PgOrgStore) GetOrgMember(ctx context.Context, orgID, userID string) (*types.OrgMember, error)

func (*PgOrgStore) GetOrgPolicies

func (s *PgOrgStore) GetOrgPolicies(ctx context.Context, orgID string) ([]*types.OrgPolicy, error)

func (*PgOrgStore) GetPlatformSetting

func (s *PgOrgStore) GetPlatformSetting(ctx context.Context, key types.PlatformSettingKey) (*types.PlatformSetting, error)

GetPlatformSetting retrieves a single platform-wide setting by key. Returns nil (not an error) when the key does not exist.

func (*PgOrgStore) GetRoleDependents

func (s *PgOrgStore) GetRoleDependents(ctx context.Context, roleID string) ([]*types.AgentRole, error)

GetRoleDependents returns roles that extend the given role.

func (*PgOrgStore) GetSSOConfig

func (s *PgOrgStore) GetSSOConfig(ctx context.Context, orgID string) (*types.OrgSSOConfig, error)

func (*PgOrgStore) GetStripeCustomerID

func (s *PgOrgStore) GetStripeCustomerID(ctx context.Context, orgID string) (string, error)

func (*PgOrgStore) GetUserEmail

func (s *PgOrgStore) GetUserEmail(ctx context.Context, userID string) (string, error)

func (*PgOrgStore) GetUserIDByEmail

func (s *PgOrgStore) GetUserIDByEmail(ctx context.Context, email string) (string, error)

func (*PgOrgStore) GetUserOrgID

func (s *PgOrgStore) GetUserOrgID(ctx context.Context, userID string) (string, error)

func (*PgOrgStore) GetWorkspaceAgentRole

func (s *PgOrgStore) GetWorkspaceAgentRole(ctx context.Context, workspaceID string) (*types.AgentRole, error)

GetWorkspaceAgentRole retrieves the role assigned to a workspace.

func (*PgOrgStore) GetWorkspaceOrgID

func (s *PgOrgStore) GetWorkspaceOrgID(ctx context.Context, workspaceID string) (string, error)

GetWorkspaceOrgID returns the org_id for a workspace, or "" if the workspace has no org (standalone user).

func (*PgOrgStore) GetWorkspacePrompt

func (s *PgOrgStore) GetWorkspacePrompt(ctx context.Context, workspaceID string) (*types.WorkspacePrompt, error)

GetWorkspacePrompt retrieves the user-level prompt override for a workspace. Returns nil (not an error) when no override exists.

func (*PgOrgStore) HardDeleteOrg

func (s *PgOrgStore) HardDeleteOrg(ctx context.Context, orgID string) error

func (*PgOrgStore) HasRoleWorkspaceUsage

func (s *PgOrgStore) HasRoleWorkspaceUsage(ctx context.Context, roleID string) (bool, error)

HasRoleWorkspaceUsage checks if any workspace references this role.

func (*PgOrgStore) IsOrgAdmin

func (s *PgOrgStore) IsOrgAdmin(ctx context.Context, orgID, userID string) (bool, error)

func (*PgOrgStore) IsOrgMember

func (s *PgOrgStore) IsOrgMember(ctx context.Context, orgID, userID string) (bool, error)

func (*PgOrgStore) ListAgentRoles

func (s *PgOrgStore) ListAgentRoles(ctx context.Context, scope string, orgID string) ([]*types.AgentRole, error)

ListAgentRoles lists roles by scope and optional org_id.

func (*PgOrgStore) ListAllAudit

func (s *PgOrgStore) ListAllAudit(ctx context.Context, filters types.AuditFilters) ([]*types.AuditEntry, *types.PaginationMetadata, error)

ListAllAudit returns audit_log rows across every org, narrowed by filters. The WHERE clause is built from conditional ANDs over a parameterised args slice — no user input is ever interpolated into the SQL text.

func (*PgOrgStore) ListAllOrgs

func (s *PgOrgStore) ListAllOrgs(ctx context.Context, limit, offset int, statusFilter *string) ([]types.OrgSummary, *types.PaginationMetadata, error)

ListAllOrgs returns every non-deleted organization with aggregated member and workspace counts for the platform-admin dashboard. The optional statusFilter narrows the result to a single OrgStatus (e.g. "suspended"); an empty/nil filter returns all statuses. Results are ordered by created_at DESC.

The two counts are correlated subqueries on the same row, so a single round trip returns the full summary without an N+1 fan-out. The COUNT(*) total is fetched first so an empty page short-circuits the SELECT.

func (*PgOrgStore) ListOrgAudit

func (s *PgOrgStore) ListOrgAudit(ctx context.Context, orgID string, limit, offset int) ([]*types.AuditEntry, *types.PaginationMetadata, error)

func (*PgOrgStore) ListOrgMembers

func (s *PgOrgStore) ListOrgMembers(ctx context.Context, orgID string) ([]*types.OrgMember, error)

func (*PgOrgStore) ListOrgWorkspaces

func (s *PgOrgStore) ListOrgWorkspaces(ctx context.Context, orgID string, limit, offset int) ([]*types.WorkspaceMetadata, *types.PaginationMetadata, error)

func (*PgOrgStore) ListOrgsForUser

func (s *PgOrgStore) ListOrgsForUser(ctx context.Context, userID string) ([]*types.OrgResponse, error)

func (*PgOrgStore) ListPendingInvitations

func (s *PgOrgStore) ListPendingInvitations(ctx context.Context, orgID string) ([]*types.OrgInvitation, error)

func (*PgOrgStore) ListPendingOrgsOlderThan

func (s *PgOrgStore) ListPendingOrgsOlderThan(ctx context.Context, maxAge time.Duration) ([]PendingOrgCleanup, error)

func (*PgOrgStore) ListSSODomains

func (s *PgOrgStore) ListSSODomains(ctx context.Context) ([]types.SSODomain, error)

func (*PgOrgStore) LogAuditEvent

func (s *PgOrgStore) LogAuditEvent(ctx context.Context, domain, actorID, action, targetID string, orgID *string, metadata map[string]any) error

LogAuditEvent inserts a row into audit_log with an explicit domain and an optional org scope. It is the general audit writer used by both org-scoped events (domain='org', orgID non-nil) and platform-admin events (domain='admin', orgID nil). The domain must be one of the values allowed by the audit_log_domain_chk CHECK constraint (billing/secrets/admin/org).

func (*PgOrgStore) LogOrgEvent

func (s *PgOrgStore) LogOrgEvent(ctx context.Context, orgID, actorID, action, targetID string, metadata map[string]any) error

func (*PgOrgStore) MarkUserEmailVerified

func (s *PgOrgStore) MarkUserEmailVerified(ctx context.Context, userID string) error

MarkUserEmailVerified sets users.email_verified=true for the given user, bypassing the email-verification token flow. Used by the org-admin "Verify" action (POST /orgs/:id/members/:userID/verify) when an admin has confirmed the member's identity out-of-band. The membership is verified by the caller (OrgAdminGuard + GetOrgMember) before this is invoked, so a bare userID is safe here. Idempotent: re-verifying an already-verified user is a no-op.

func (*PgOrgStore) OrgsWhereUserIsLastActiveAdmin

func (s *PgOrgStore) OrgsWhereUserIsLastActiveAdmin(ctx context.Context, userID string) ([]types.LastAdminOrg, error)

OrgsWhereUserIsLastActiveAdmin returns every organization where the given user is an admin AND no OTHER active admin exists. Suspending such a user (D19) would orphan the org — no remaining admin could manage it (promote members, change policies, manage billing). The user-suspend path refuses with 409 when this returns a non-empty slice (unless force=true).

func (*PgOrgStore) RecordStripeEvent

func (s *PgOrgStore) RecordStripeEvent(ctx context.Context, eventID, eventType string) (bool, error)

func (*PgOrgStore) RemoveOrgAdminIfNotLast

func (s *PgOrgStore) RemoveOrgAdminIfNotLast(ctx context.Context, orgID, targetUserID string) (bool, error)

func (*PgOrgStore) RemoveOrgMember

func (s *PgOrgStore) RemoveOrgMember(ctx context.Context, orgID, userID string) error

func (*PgOrgStore) RotateVerificationToken

func (s *PgOrgStore) RotateVerificationToken(ctx context.Context, orgID string) (string, error)

RotateVerificationToken replaces the org's verification token with a fresh random 32-hex value and returns it. Used for both initial creation (when verification_token is NULL) and rotation. Returns the new token.

func (*PgOrgStore) SetBillingAccountSubscription

func (s *PgOrgStore) SetBillingAccountSubscription(ctx context.Context, ownerID, ownerType, provider, subscriptionID string) error

func (*PgOrgStore) SetDomainVerified

func (s *PgOrgStore) SetDomainVerified(ctx context.Context, orgID, domain string) (bool, error)

SetDomainVerified atomically appends a domain to verified_domains. The domain must already be in claimed_domains (the WHERE clause enforces this); a domain not claimed is a no-op. Idempotent: re-verifying an already- verified domain returns (false, nil) without error. Returns (true, nil) only when the domain was newly promoted.

func (*PgOrgStore) SetOrgDefaultRole

func (s *PgOrgStore) SetOrgDefaultRole(ctx context.Context, orgID, roleID string) error

SetOrgDefaultRole atomically sets one role as default and clears all others.

func (*PgOrgStore) SetOrgPolicy

func (s *PgOrgStore) SetOrgPolicy(ctx context.Context, orgID string, key types.OrgPolicyKey, value json.RawMessage, updatedBy string) error

func (*PgOrgStore) SetPlatformSetting

func (s *PgOrgStore) SetPlatformSetting(ctx context.Context, key types.PlatformSettingKey, value json.RawMessage, updatedBy string) error

SetPlatformSetting upserts a platform-wide setting.

func (*PgOrgStore) SetWorkspaceAgentRole

func (s *PgOrgStore) SetWorkspaceAgentRole(ctx context.Context, workspaceID, roleID, userID string) error

SetWorkspaceAgentRole sets the role for a workspace.

func (*PgOrgStore) SetWorkspacePrompt

func (s *PgOrgStore) SetWorkspacePrompt(ctx context.Context, workspaceID string, prompt string, updatedBy string) error

SetWorkspacePrompt upserts the user-level prompt override for a workspace.

func (*PgOrgStore) SoftDeleteOrg

func (s *PgOrgStore) SoftDeleteOrg(ctx context.Context, orgID string) error

func (*PgOrgStore) SuspendUserGuardedByLastAdmin

func (s *PgOrgStore) SuspendUserGuardedByLastAdmin(ctx context.Context, userID string, force bool) (*types.LastAdminOrg, error)

SuspendUserGuardedByLastAdmin atomically refuses to suspend the user when they are the sole active admin of any org (unless force), and otherwise sets the user's status to suspended. The SELECT … FOR UPDATE on the admin membership rows of every org the user administers plus the UPDATE on users run in a single transaction, closing the TOCTOU window of the prior read-then-write sequence (F7, US-43.19): two concurrent admin suspensions or a suspend racing a demote can no longer both pass the last-admin check and leave the org adminless. `active` is mirrored to `false` so the legacy column cannot drift from `status` (F6).

Returns a non-nil *LastAdminOrg when the suspend was refused (last admin); the caller surfaces this as 409. Returns (nil, nil) on a successful suspension.

func (*PgOrgStore) UpdateAgentRole

func (s *PgOrgStore) UpdateAgentRole(ctx context.Context, roleID string, role *types.AgentRole, configJSON []byte) (*types.AgentRole, error)

UpdateAgentRole updates an agent role.

func (*PgOrgStore) UpdateOrg

func (s *PgOrgStore) UpdateOrg(ctx context.Context, orgID string, req types.UpdateOrgRequest) (*types.Organization, error)

func (*PgOrgStore) UpdateOrgMemberRole

func (s *PgOrgStore) UpdateOrgMemberRole(ctx context.Context, orgID, userID string, role types.OrgRole) error

func (*PgOrgStore) UpdateOrgStatus

func (s *PgOrgStore) UpdateOrgStatus(ctx context.Context, orgID string, status *types.OrgStatus, subStatus *types.OrgSubscriptionStatus, planID *types.OrgPlan) error

func (*PgOrgStore) UpsertSSOConfig

func (s *PgOrgStore) UpsertSSOConfig(ctx context.Context, config *types.OrgSSOConfig) error

type Service

type Service struct {
	Logger *logger.Logger
	Config *config.Config
	DB     *sql.DB
}

Service handles database operations

func New

func New(cfg *config.Config, log *logger.Logger) (*Service, error)

func (*Service) BeginTx

func (s *Service) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)

BeginTx starts a new database transaction. Used by handlers that need multi-statement atomicity (e.g., AgentReloadHandler's SELECT FOR UPDATE + UPSERT).

func (*Service) CheckPermission

func (s *Service) CheckPermission(ctx context.Context, userID, resourceType, resourceID, action string) (bool, error)

CheckPermission checks if a user has permission to perform an action on a resource

func (*Service) CheckResourceOwnership

func (s *Service) CheckResourceOwnership(ctx context.Context, userID, resourceType, resourceID string) (bool, error)

CheckResourceOwnership checks if a user owns a resource

func (*Service) CountActiveWorkspacesByUserAndOrg

func (s *Service) CountActiveWorkspacesByUserAndOrg(ctx context.Context, userID, orgID string) (int, error)

func (*Service) CountUsers

func (s *Service) CountUsers(ctx context.Context) (int, error)

CountUsers returns the total number of users in the system. Used by the auth Register flow to detect a fresh installation (count == 0) and auto-promote the first user to admin so a brand-new install has at least one administrator.

func (*Service) CountWorkspacesByUserAndOrg

func (s *Service) CountWorkspacesByUserAndOrg(ctx context.Context, userID, orgID string) (int, error)

func (*Service) CreateAPIKey

func (s *Service) CreateAPIKey(ctx context.Context, apiKey *types.APIKey) error

func (*Service) CreateUser

func (s *Service) CreateUser(ctx context.Context, user *types.User) error

CreateUser creates a new user

func (*Service) CreateWorkspace

func (s *Service) CreateWorkspace(ctx context.Context, workspace *types.WorkspaceMetadata) error

CreateWorkspace inserts a new workspace record.

func (*Service) DeleteAPIKey

func (s *Service) DeleteAPIKey(ctx context.Context, userID, keyID string) error

func (*Service) DeleteSessionIndex

func (s *Service) DeleteSessionIndex(ctx context.Context, workspaceID string) error

func (*Service) DeleteSessionTree

func (s *Service) DeleteSessionTree(ctx context.Context, workspaceID, sessionID string) error

func (*Service) DeleteUser

func (s *Service) DeleteUser(ctx context.Context, userID string) error

DeleteUser deletes a user

func (*Service) DeleteWorkspace

func (s *Service) DeleteWorkspace(ctx context.Context, workspaceID string) error

DeleteWorkspace removes a workspace record.

func (*Service) GetAPIKey

func (s *Service) GetAPIKey(ctx context.Context, userID, keyID string) (*types.APIKey, error)

func (*Service) GetAPIKeyRecordByHash

func (s *Service) GetAPIKeyRecordByHash(ctx context.Context, keyHash string) (*types.APIKey, error)

func (*Service) GetAllInstanceSettings

func (s *Service) GetAllInstanceSettings(ctx context.Context) (map[string]json.RawMessage, error)

GetAllInstanceSettings returns all rows from instance_settings.

func (*Service) GetAllUserSettings

func (s *Service) GetAllUserSettings(ctx context.Context, userID string) (map[string]json.RawMessage, error)

GetAllUserSettings returns all settings for a specific user.

func (*Service) GetDefaultModel

func (s *Service) GetDefaultModel(ctx context.Context, workspaceID string) (string, error)

GetDefaultModel returns the workspace's configured default model, or "" if unset.

func (*Service) GetLastCredentialChangedAt

func (s *Service) GetLastCredentialChangedAt(ctx context.Context, workspaceID string) (time.Time, error)

GetLastCredentialChangedAt returns the most recent credential-changed timestamp for the workspace, or the zero time if no row exists.

func (*Service) GetUser

func (s *Service) GetUser(ctx context.Context, userID string) (*types.User, error)

GetUser gets a user by ID

func (*Service) GetUserByAPIKey

func (s *Service) GetUserByAPIKey(ctx context.Context, apiKey string) (*types.User, error)

GetUserByAPIKey gets the user associated with an API key

func (*Service) GetUserByEmail

func (s *Service) GetUserByEmail(ctx context.Context, email string) (*types.User, error)

GetUserByEmail gets a user by email address

func (*Service) GetWorkspace

func (s *Service) GetWorkspace(ctx context.Context, workspaceID string) (*types.WorkspaceMetadata, error)

GetWorkspace gets a workspace by ID.

func (*Service) InsertInstanceSettingIfMissing

func (s *Service) InsertInstanceSettingIfMissing(ctx context.Context, key string, value json.RawMessage) (bool, error)

InsertInstanceSettingIfMissing inserts a setting only if the key doesn't exist. Returns true if inserted, false if already existed.

func (*Service) ListAPIKeys

func (s *Service) ListAPIKeys(ctx context.Context, userID string) ([]*types.APIKey, error)

func (*Service) ListAPIKeysWithDecrypt

func (s *Service) ListAPIKeysWithDecrypt(ctx context.Context, userID string) ([]*types.APIKey, error)

func (*Service) ListAllUsers

func (s *Service) ListAllUsers(ctx context.Context, limit, offset int, statusFilter *string) ([]types.UserListEntry, *types.PaginationMetadata, error)

ListAllUsers returns every user for the platform-admin dashboard (US-43.18). The optional statusFilter narrows to a single UserStatus; an empty/nil filter returns all users. Each entry carries the user's single org membership (org_id/org_name) resolved via a LEFT JOIN — under single-org enforcement (D8) a user belongs to at most one org, so this adds no row fan-out.

Password hashes and other sensitive columns are never selected. limit is clamped to [1, adminListMaxLimit]; offset defaults to 0.

func (*Service) ListAllWorkspaceOwners

func (s *Service) ListAllWorkspaceOwners(ctx context.Context) (map[string]string, error)

func (*Service) ListAllWorkspacesForBilling

func (s *Service) ListAllWorkspacesForBilling(ctx context.Context) ([]WorkspaceBillingRecord, error)

func (*Service) ListPendingReloadWorkspaces

func (s *Service) ListPendingReloadWorkspaces(ctx context.Context, userID string) ([]*types.WorkspaceMetadata, error)

ListPendingReloadWorkspaces returns workspaces with pending_refresh=TRUE for the given user.

func (*Service) ListSessionIndex

func (s *Service) ListSessionIndex(ctx context.Context, workspaceID string) ([]types.SessionListItem, error)

func (*Service) ListWorkspaces

func (s *Service) ListWorkspaces(ctx context.Context, userID string, limit, offset int) ([]*types.WorkspaceMetadata, *types.PaginationMetadata, error)

ListWorkspaces lists workspaces owned by the user with pagination.

Per Epic 43 decision D6, this returns only workspaces the user created (w.user_id = $1). The prior LEFT JOIN org_memberships + OR clause that let any org member see every other member's org workspace has been removed: members now see only their own workspaces. Org admins who need to see all org workspaces use the dedicated GET /orgs/:id/workspaces endpoint (OrgStore.ListOrgWorkspaces).

func (*Service) MarkAgentReloaded

func (s *Service) MarkAgentReloaded(ctx context.Context, tx *sql.Tx, workspaceID string, priorChangedAt time.Time) (time.Time, error)

MarkAgentReloaded clears pending_refresh after a successful dispose. Uses SELECT FOR UPDATE to serialize against concurrent MarkCredentialChanged. priorChangedAt is captured BEFORE dispose; if a new credential was staged during the dispose window, pending_refresh stays true. Returns the DB-clock timestamp written to last_agent_disposed_at.

func (*Service) MarkCredentialChanged

func (s *Service) MarkCredentialChanged(ctx context.Context, workspaceID string) error

MarkCredentialChanged flips a workspace into "credentials staged, reload needed" state. Uses a single auto-commit UPSERT (no external transaction parameter) because the binding write (PgSecretStore, pgxpool) and this write (*sql.DB) use incompatible connection pools — cross-pool transactions are impossible.

func (*Service) MarkWorkspaceDeleted

func (s *Service) MarkWorkspaceDeleted(ctx context.Context, workspaceID string)

MarkWorkspaceDeleted soft-deletes a workspace by setting deleted_at and purges any user_secret_bindings rows pointing at it within a single transaction. The bindings table has no FK to workspaces.id (the column types differ historically) so a soft delete leaves orphan binding rows behind unless we clean up here explicitly. See Bug 11 in worklog 0085.

The two writes are wrapped in a single transaction so an API-process crash between them cannot leave a soft-deleted workspace with orphan bindings (validator finding on Bug 11 follow-up).

func (*Service) Ping

func (s *Service) Ping(ctx context.Context) error

Ping checks the database connection

func (*Service) PurgeUserSecrets

func (s *Service) PurgeUserSecrets(ctx context.Context, userID string) error

PurgeUserSecrets deletes every user-owned secret row for a user: provider_credentials (LLM provider keys) and user_secrets. It is called from the email password-reset flow to make the "your saved keys will be deleted" guarantee literal.

The DEK reinitialisation that precedes this call already makes the old ciphertext cryptographically undecryptable; deleting the rows removes them outright and guarantees no future materialization can resurrect them. Both tables' dependents (workspace_credential_bindings, user_secret_bindings) reference the parent with ON DELETE CASCADE, so no orphaned binding rows remain. Rows deleted before this call by the DEK reinit's UPSERT (user_keys) are unaffected.

Best-effort at the caller: a failure here does not undo the reset because the cryptographic erasure has already happened.

func (*Service) SetInstanceSetting

func (s *Service) SetInstanceSetting(ctx context.Context, key string, value json.RawMessage) error

SetInstanceSetting upserts a single instance setting.

func (*Service) SetUserSetting

func (s *Service) SetUserSetting(ctx context.Context, userID, key string, value json.RawMessage) error

SetUserSetting upserts a single user setting.

func (*Service) SetUserStatus

func (s *Service) SetUserStatus(ctx context.Context, userID string, status types.UserStatus) error

SetUserStatus sets the authoritative operational status of a user account (D19). status='suspended' blocks the user across all contexts via the auth middleware; 'active' restores access.

F6 (US-43.19): the legacy `active` boolean is mirrored from `status` (active = (status='active')) so the two columns cannot drift apart. The auth middleware authorizes on `status`; Login historically checks `active`. Before this fix, any path that wrote `active` independently of `status` would leave the user blocked at Login but not at the middleware (or vice-versa). Keeping them in lockstep removes that divergence vector.

func (*Service) Start

func (s *Service) Start() error

Start starts the database service

func (*Service) Stop

func (s *Service) Stop() error

Stop stops the database service

func (*Service) SyncWorkspaceVersionInfo

func (s *Service) SyncWorkspaceVersionInfo(ctx context.Context, workspaceID, imageTag, agentVersion string)

SyncWorkspaceVersionInfo updates image_tag and/or agent_version in the DB. Only non-empty values are written; passing an empty string for either field leaves the existing DB value untouched. This allows the CRD watcher to sync imageTag without clobbering agentVersion, which is sourced separately from agentd health checks.

func (*Service) UpdateAPIKeyDEK

func (s *Service) UpdateAPIKeyDEK(ctx context.Context, keyID string, wrappedDEK, kekSalt []byte, synced bool) error

func (*Service) UpdateSessionLastSeen

func (s *Service) UpdateSessionLastSeen(ctx context.Context, workspaceID, sessionID string) error

func (*Service) UpdateUser

func (s *Service) UpdateUser(ctx context.Context, userID string, updates types.UserUpdates) error

UpdateUser updates specific fields on a user record. Only non-nil fields are applied.

func (*Service) UpdateWorkspace

func (s *Service) UpdateWorkspace(ctx context.Context, workspaceID string, updates types.WorkspaceUpdates) error

UpdateWorkspace updates specific fields on a workspace record.

func (*Service) UpsertSessionContextUsed

func (s *Service) UpsertSessionContextUsed(ctx context.Context, workspaceID, sessionID string, contextUsed int64) error

UpsertSessionContextUsed persists the prompt token count for the most recent LLM step in this session. Called by the API proxy on every session.next.step.ended SSE event. Idempotent: concurrent writes of the same value are safe (both replicas receive the same event and write the same data).

func (*Service) UpsertSessionMessage

func (s *Service) UpsertSessionMessage(ctx context.Context, workspaceID, sessionID string, at time.Time) error

func (*Service) UpsertSessionParent

func (s *Service) UpsertSessionParent(ctx context.Context, workspaceID, sessionID, parentID string) error

UpsertSessionParent records (or refreshes) the parent_session_id for a session. Used to mirror opencode subagent (subtask) parent links into the sidebar's session_index so the UI can render the hierarchy without round-tripping the agent.

Idempotent: passing the same parentID is a no-op. We deliberately do not guard against parentID changes — opencode never re-parents a session in practice, and an UPDATE-on-conflict path costs less than a SELECT-then- UPDATE round trip.

func (*Service) UpsertSessionTitle

func (s *Service) UpsertSessionTitle(ctx context.Context, workspaceID, sessionID, title string) error

type WorkspaceBillingRecord

type WorkspaceBillingRecord struct {
	ID          string
	UserID      string
	StorageSize string
}

Jump to

Keyboard shortcuts

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