handlers

package
v0.2.2 Latest Latest
Warning

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

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

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrDuplicateCredential = errors.New("duplicate credential")
	ErrCredentialNotFound  = errors.New("credential not found")
	// ErrCredentialCheckViolation is returned when an INSERT/UPDATE
	// trips a CHECK constraint (PG SQLSTATE 23514). With Epic 55 the
	// `kind` and `slug` columns carry CHECK constraints; the handler
	// layer should ideally have caught the bad input via ValidateKind /
	// ValidateSlug at the boundary, so reaching this code is a sign
	// that boundary validation drifted from the DB constraint set
	// (the property tests in credential_identity_test.go guard against
	// that drift). Map to 400 anyway as defense-in-depth — the user
	// sent invalid data, not a server fault.
	ErrCredentialCheckViolation = errors.New("credential failed validation")
)

Functions

func ClassifyPostgresError

func ClassifyPostgresError(err error) error

func EnrichChatErrorBody

func EnrichChatErrorBody(
	body []byte,
	needsRefresh bool,
	since time.Time,
	workspaceID string,
) []byte

EnrichChatErrorBody adds agentNeedsRefresh hint to error responses when the workspace has staged credentials.

func ProbeModelsAnon

func ProbeModelsAnon(c *gin.Context)

ProbeModelsAnon handles POST /api/v1/probe-models. No credential ID needed — caller passes apiKey + baseURL directly. Auth is still required so arbitrary API keys can't be proxied by unauthenticated users. The baseURL is validated against SSRF rules before making any outbound request.

func WaitUntilIdle

func WaitUntilIdle(
	ctx context.Context,
	workspaceID string,
	tracker *sse.Tracker,
	opencodeClient *opencode.Client,
	timeout time.Duration,
) error

WaitUntilIdle blocks until all sessions in the workspace are idle, the context is canceled, or the deadline fires.

Types

type AdminProviderCredentialsHandler

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

AdminProviderCredentialsHandler handles CRUD for admin provider credentials.

func NewAdminProviderCredentialsHandler

func NewAdminProviderCredentialsHandler(store CredentialStore, provider secrets.RootKeyProvider) *AdminProviderCredentialsHandler

NewAdminProviderCredentialsHandler creates a new handler.

func (*AdminProviderCredentialsHandler) Create

Create handles POST /api/v1/admin/provider-credentials.

func (*AdminProviderCredentialsHandler) CreateAutoApply

func (h *AdminProviderCredentialsHandler) CreateAutoApply(c *gin.Context)

CreateAutoApply handles POST /api/v1/admin/provider-credentials/:id/auto-apply.

func (*AdminProviderCredentialsHandler) Delete

Delete handles DELETE /api/v1/admin/provider-credentials/:id.

func (*AdminProviderCredentialsHandler) DeleteAutoApply

func (h *AdminProviderCredentialsHandler) DeleteAutoApply(c *gin.Context)

DeleteAutoApply handles DELETE /api/v1/admin/provider-credentials/:id/auto-apply/:targetType/:targetId.

func (*AdminProviderCredentialsHandler) Get

Get handles GET /api/v1/admin/provider-credentials/:id.

func (*AdminProviderCredentialsHandler) List

List handles GET /api/v1/admin/provider-credentials.

func (*AdminProviderCredentialsHandler) ListAutoApply

func (h *AdminProviderCredentialsHandler) ListAutoApply(c *gin.Context)

ListAutoApply handles GET /api/v1/admin/provider-credentials/:id/auto-apply.

func (*AdminProviderCredentialsHandler) ProbeModels

func (h *AdminProviderCredentialsHandler) ProbeModels(c *gin.Context)

ProbeModels handles GET /api/v1/admin/provider-credentials/:id/models. Admin variant — uses the platform KEK to decrypt.

func (*AdminProviderCredentialsHandler) SetAutoApplyStore

func (h *AdminProviderCredentialsHandler) SetAutoApplyStore(s AutoApplyStore)

SetAutoApplyStore sets the auto-apply store (called after construction).

func (*AdminProviderCredentialsHandler) Update

Update handles PUT /api/v1/admin/provider-credentials/:id.

type AdminSessionHandler

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

AdminSessionHandler serves admin-only session recovery endpoints.

ForceAbortSession clears a workspace-scoped session that is stuck in the ProxyHandler's active-session set (wsstate.Store.activeSess) after the workspace pod has been deleted or become unreachable. It does NOT call the opencode proxy — the pod may be gone, by design. It mirrors the local cleanup half of ProxyHandler.DeleteSession (proxy_handlers.go:234-268) minus the proxy call, plus an audit-log row.

The session-index DB row is deliberately NOT cleaned: force-abort clears only the stuck active-session marker, not the session itself. The session may still be live in opencode (just not busy). DeleteWorkspace already does not clean session_index rows (pre-existing pattern).

Audit-log failure is non-fatal: the force-abort succeeds even if the DB INSERT fails, because incident recovery must not be blocked by a DB hiccup (differs from AdminDiscardDLQ which returns 500 on audit failure — that operation is not incident-recovery-critical).

func NewAdminSessionHandler

func NewAdminSessionHandler(proxy *ProxyHandler, db *sql.DB, logger pkginterfaces.LoggerInterface) *AdminSessionHandler

func (*AdminSessionHandler) ForceAbortSession

func (h *AdminSessionHandler) ForceAbortSession(c *gin.Context)

type AgentReloadHandler

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

AgentReloadHandler handles POST /api/v1/workspaces/:id/agent/reload.

func NewAgentReloadHandler

func NewAgentReloadHandler(
	wsSvc WorkspaceServicer,
	db AgentStateStore,
	podResolver PodIPResolver,
	httpClient *http.Client,
	logger pkginterfaces.LoggerInterface,
) *AgentReloadHandler

NewAgentReloadHandler constructs the handler with all dependencies.

func (*AgentReloadHandler) Reload

func (h *AgentReloadHandler) Reload(c *gin.Context)

Reload handles POST /api/v1/workspaces/:id/agent/reload.

func (*AgentReloadHandler) SetBrokerPublisher

func (h *AgentReloadHandler) SetBrokerPublisher(b BrokerPublisher)

SetBrokerPublisher injects the SSE broker for publishing dismissed events on dispose.

func (*AgentReloadHandler) SetMetrics

func (h *AgentReloadHandler) SetMetrics(m MetricsRecorder)

SetMetrics injects the metrics recorder.

func (*AgentReloadHandler) SetPasswordGetter

func (h *AgentReloadHandler) SetPasswordGetter(provider interfaces.WorkspacePasswordProvider)

SetPasswordGetter injects the password getter for drain mode (needs opencode client).

func (*AgentReloadHandler) SetQueueClearer

func (h *AgentReloadHandler) SetQueueClearer(q QueueClearer)

SetQueueClearer injects the queue service for clearing queued messages on dispose.

func (*AgentReloadHandler) SetSSETracker

func (h *AgentReloadHandler) SetSSETracker(t *sse.Tracker)

SetSSETracker injects the tracker for drain mode support.

type AgentRoleHandler

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

AgentRoleHandler handles platform and org agent role CRUD.

func NewAgentRoleHandler

func NewAgentRoleHandler(store roleStore, svc *role.Service, authSvc orgAuthService, logger policyLogger) *AgentRoleHandler

func (*AgentRoleHandler) ClearWorkspaceRole

func (h *AgentRoleHandler) ClearWorkspaceRole(c *gin.Context)

ClearWorkspaceRole removes the workspace's role assignment so it falls back to the platform default. Enforces the same allow_user_prompt gate as SetWorkspaceRole.

func (*AgentRoleHandler) CreateOrg

func (h *AgentRoleHandler) CreateOrg(c *gin.Context)

func (*AgentRoleHandler) CreatePlatform

func (h *AgentRoleHandler) CreatePlatform(c *gin.Context)

func (*AgentRoleHandler) DeleteOrg

func (h *AgentRoleHandler) DeleteOrg(c *gin.Context)

func (*AgentRoleHandler) DeletePlatform

func (h *AgentRoleHandler) DeletePlatform(c *gin.Context)

func (*AgentRoleHandler) GetEffectiveWorkspaceRole

func (h *AgentRoleHandler) GetEffectiveWorkspaceRole(c *gin.Context)

func (*AgentRoleHandler) GetOrg

func (h *AgentRoleHandler) GetOrg(c *gin.Context)

func (*AgentRoleHandler) GetPlatform

func (h *AgentRoleHandler) GetPlatform(c *gin.Context)

func (*AgentRoleHandler) GetWorkspaceRole

func (h *AgentRoleHandler) GetWorkspaceRole(c *gin.Context)

func (*AgentRoleHandler) ListOrg

func (h *AgentRoleHandler) ListOrg(c *gin.Context)

func (*AgentRoleHandler) ListPlatform

func (h *AgentRoleHandler) ListPlatform(c *gin.Context)

func (*AgentRoleHandler) SetWorkspaceRole

func (h *AgentRoleHandler) SetWorkspaceRole(c *gin.Context)

func (*AgentRoleHandler) UpdateOrg

func (h *AgentRoleHandler) UpdateOrg(c *gin.Context)

func (*AgentRoleHandler) UpdatePlatform

func (h *AgentRoleHandler) UpdatePlatform(c *gin.Context)

type AgentStateChecker

type AgentStateChecker interface {
	GetLastCredentialChangedAt(ctx context.Context, workspaceID string) (time.Time, error)
}

AgentStateChecker is the interface for checking workspace agent state.

type AgentStateStore

type AgentStateStore interface {
	GetLastCredentialChangedAt(ctx context.Context, workspaceID string) (time.Time, error)
	MarkAgentReloaded(ctx context.Context, tx *sql.Tx, workspaceID string, priorChangedAt time.Time) (time.Time, error)
	BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
}

AgentStateStore is the DB surface needed by the reload handler.

type AuditHandler

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

AuditHandler handles GET /api/v1/orgs/:id/audit (admin-only).

func NewAuditHandler

func NewAuditHandler(store auditStore) *AuditHandler

NewAuditHandler constructs the handler.

func (*AuditHandler) List

func (h *AuditHandler) List(c *gin.Context)

List handles GET /api/v1/orgs/:id/audit.

func (*AuditHandler) ListCrossOrg

func (h *AuditHandler) ListCrossOrg(c *gin.Context)

ListCrossOrg handles GET /api/v1/admin/audit (platform-admin only). It reads the org_id / actor_id / domain / limit / offset query params, constructs an AuditFilters, and returns all matching audit entries across every org.

type AutoApplyStore

type AutoApplyStore interface {
	CreateAutoApply(ctx context.Context, credentialID, targetType string, targetID *string, priority int) error
	DeleteAutoApply(ctx context.Context, credentialID, targetType string, targetID *string) error
	ListAutoApply(ctx context.Context, credentialID string) ([]secrets.AutoApplyRule, error)
}

AutoApplyStore abstracts auto-apply DB operations.

type BrokerPublisher

type BrokerPublisher interface {
	PublishToWorkspace(workspaceID string, event apitypes.WorkspaceSSEEvent)
}

BrokerPublisher is the minimal SSE broker interface needed by the reload handler.

type BulkReloadHandler

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

BulkReloadHandler handles POST /api/v1/users/me/agents/reload.

func NewBulkReloadHandler

func NewBulkReloadHandler(
	pendingLister PendingReloadLister,
	wsSvc WorkspaceServicer,
	db AgentStateStore,
	podResolver PodIPResolver,
	httpClient *http.Client,
	logger pkginterfaces.LoggerInterface,
) *BulkReloadHandler

NewBulkReloadHandler constructs the bulk reload handler.

func (*BulkReloadHandler) BulkReload

func (h *BulkReloadHandler) BulkReload(c *gin.Context)

BulkReload streams per-workspace reload results as NDJSON.

func (*BulkReloadHandler) SetBrokerPublisher

func (h *BulkReloadHandler) SetBrokerPublisher(b BrokerPublisher)

SetBrokerPublisher injects the SSE broker for publishing dismissed events on dispose.

func (*BulkReloadHandler) SetMetrics

func (h *BulkReloadHandler) SetMetrics(m MetricsRecorder)

SetMetrics injects the metrics recorder.

func (*BulkReloadHandler) SetPasswordGetter

func (h *BulkReloadHandler) SetPasswordGetter(provider interfaces.WorkspacePasswordProvider)

SetPasswordGetter injects the password getter for drain mode.

func (*BulkReloadHandler) SetQueueClearer

func (h *BulkReloadHandler) SetQueueClearer(q QueueClearer)

SetQueueClearer injects the queue service for clearing queued messages on dispose.

func (*BulkReloadHandler) SetSSETracker

func (h *BulkReloadHandler) SetSSETracker(t *sse.Tracker)

SetSSETracker injects the SSE tracker for drain mode.

type Catalog

type Catalog struct {
	Connected []string          `json:"connected"`
	Providers []CatalogProvider `json:"all"`
}

Catalog is the normalized, agent-agnostic shape of a model listing. Connected holds provider IDs the agent reports as active; Providers holds the full provider→model tree. A ModelCatalogParser converts an agent-specific wire format into this shape.

type CatalogModel

type CatalogModel struct {
	ID   string            `json:"id"`
	Name string            `json:"name"`
	Cost ProviderModelCost `json:"cost"`
}

CatalogModel is a single model entry in a parsed agent catalog.

type CatalogProvider

type CatalogProvider struct {
	ID     string                  `json:"id"`
	Models map[string]CatalogModel `json:"models"`
}

CatalogProvider groups models under a provider. Models is keyed by the provider-local model key (which may differ from the model's own ID).

type CredentialBindingStore

type CredentialBindingStore interface {
	BindCredentialToWorkspace(ctx context.Context, credentialID, workspaceID string) error
	// UnbindCredentialFromWorkspace removes an EXPLICIT binding.
	// Returns secrets.ErrAutoBindingProtected for auto-managed bindings.
	UnbindCredentialFromWorkspace(ctx context.Context, credentialID, workspaceID string) error
	// GetCredentialBindingsWithSource returns bindings with source type (explicit vs auto).
	GetCredentialBindingsWithSource(ctx context.Context, credentialID, userID string) ([]secrets.CredentialBindingInfo, error)
	// GetCredentialBindings returns workspace IDs bound to the credential.
	GetCredentialBindings(ctx context.Context, credentialID, userID string) ([]string, error)
	// BindCredentialToAllUserWorkspaces binds a credential to every workspace owned by userID.
	BindCredentialToAllUserWorkspaces(ctx context.Context, credentialID, userID string) error
}

CredentialBindingStore abstracts user-only credential↔workspace binding operations (the cross-entity concern that only the user handler needs).

type CredentialResponse

type CredentialResponse struct {
	ID                 string         `json:"id"`
	OrgID              string         `json:"orgId,omitempty"`
	Name               string         `json:"name"`
	Kind               string         `json:"kind"`
	Slug               string         `json:"slug"`
	BaseURL            string         `json:"baseURL,omitempty"`
	ModelAllowlist     []string       `json:"modelAllowlist"`
	ModelContextLimits map[string]int `json:"modelContextLimits"`
	ModelOutputLimits  map[string]int `json:"modelOutputLimits"`
	CreatedAt          string         `json:"createdAt"`
	UpdatedAt          string         `json:"updatedAt"`
	BindWarning        string         `json:"bindWarning,omitempty"`
}

CredentialResponse is the API response for any provider credential. Never exposes apiKey. BaseURL is extracted from the encrypted ciphertext. OrgID is populated only for org-scoped credentials (omitted otherwise). BindWarning is set only by org Create when auto-bind fails (non-fatal).

Epic 55:

  • Kind: SDK-class enum (openai, anthropic, openai_compatible, ...).
  • Slug: per-owner unique identity; reaches opencode as providerID in agent-config.json.
  • Name: free-form display label.

type CredentialStateWriter

type CredentialStateWriter interface {
	MarkCredentialChanged(ctx context.Context, workspaceID string) error
}

CredentialStateWriter records that workspace credentials have changed. Satisfied by *database.Service.

type CredentialStore

type CredentialStore interface {
	CreateCredential(ctx context.Context, ownerType, ownerID string, row *secrets.CredentialRow) error
	ListCredentials(ctx context.Context, ownerType, ownerID string) ([]*secrets.CredentialRow, error)
	GetCredential(ctx context.Context, ownerType, ownerID, credID string) (*secrets.CredentialRow, error)
	UpdateCredential(ctx context.Context, ownerType, ownerID, credID string, row *secrets.CredentialRow) error
	DeleteCredential(ctx context.Context, ownerType, ownerID, credID string) error
}

CredentialStore is the unified DB interface for provider credential CRUD, scoped by (ownerType, ownerID) for multi-tenant isolation. All three credential handlers (admin/user/org) depend on it; their specialized stores (bindings, auto-apply) are wired separately.

type DEKUnlocker

type DEKUnlocker interface {
	UnlockDEKWithSigningKey(ctx context.Context, userID string, password []byte, sessionID string, ttl time.Duration, activeSigningKey []byte) error
}

DEKUnlocker is the caller-shaped subset of KeyService used by the soft-unlock endpoint. *secrets.KeyService satisfies it.

Returns a non-nil error only when the unlock could not be completed (wrong password, DB issue, no user keys). The durable jwt_sessions write inside UnlockDEKWithSigningKey is best-effort and surfaces via logs, not the return value — login-style behavior.

type EmailHandler

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

EmailHandler exposes admin-only email operations (test-send). It wraps the EmailService which resolves the configured provider (SES/Noop).

func NewEmailHandler

func NewEmailHandler(svc *emailsvc.Service, rl rateCounter, log emailLogger) *EmailHandler

NewEmailHandler constructs an EmailHandler. svc must be non-nil. rl may be nil — when nil, the per-endpoint test-send rate limit is skipped (the global RateLimitMiddleware still applies); this is intended for tests and for deployments that prefer to rely solely on the global limiter. log may be nil in tests; when nil, send-failure errors are not logged (the mapped category is still returned to the caller).

func (*EmailHandler) TestSend

func (h *EmailHandler) TestSend(c *gin.Context)

TestSend handles POST /api/v1/admin/email/test.

Sends a test email to the given address so an admin can verify the SES wiring end-to-end. The endpoint is admin-only (wired behind AdminGuard in the router) and rate-limited to testSendRateLimit per admin per hour.

Response contract:

  • SES success: 200 { "sent": true, "provider": "ses" }
  • Noop mode: 200 { "sent": false, "provider": "noop" }
  • Rate limited: 429 { "error": "rate limit exceeded", "limit": 5 }
  • Send failure: 502 { "error": "<mapped category>" }

Noop mode reports sent=false so the admin knows no real email was delivered even though the call "succeeded" (NoopProvider logs to stderr and returns nil).

type EmailVerifierAdapter

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

EmailVerifierAdapter implements auth.EmailVerifier by creating a token, storing the hash, and sending the verification email via EmailService. Wired in app.go and passed to auth.Service.SetEmailVerifier.

func NewEmailVerifierAdapter

func NewEmailVerifierAdapter(store emailTokenStore, email *emailsvc.Service, baseURL string) *EmailVerifierAdapter

NewEmailVerifierAdapter constructs the adapter.

func (*EmailVerifierAdapter) SendVerification

func (a *EmailVerifierAdapter) SendVerification(ctx context.Context, userID, emailAddr string) error

SendVerification creates a single-use email-verify token, stores the hash, and sends the verification link via the EmailService.

type EmailVerifyHandler

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

EmailVerifyHandler handles the email-verification flow: verify a token and resend the verification email.

func NewEmailVerifyHandler

func NewEmailVerifyHandler(
	store emailTokenStore,
	users emailVerifyUserLookup,
	email *emailsvc.Service,
	resender emailResender,
	log passwordResetLogger,
) *EmailVerifyHandler

NewEmailVerifyHandler constructs the handler.

func (*EmailVerifyHandler) Resend

func (h *EmailVerifyHandler) Resend(c *gin.Context)

Resend handles POST /api/v1/auth/verify-email/resend.

Public, returns 202 always (no enumeration). Looks up the user by email; if found and unverified, sends a new verification link.

func (*EmailVerifyHandler) Verify

func (h *EmailVerifyHandler) Verify(c *gin.Context)

Verify handles POST /api/v1/auth/verify-email.

Public (the token IS the credential). Verifies the hash, checks expiry + consumption + kind, sets email_verified=true.

type ErrDrainTimeout

type ErrDrainTimeout struct {
	BusySessions []string
}

ErrDrainTimeout is returned by WaitUntilIdle when the deadline elapses before all sessions become idle.

func (*ErrDrainTimeout) Error

func (e *ErrDrainTimeout) Error() string

type InternalOrgStatusHandler

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

InternalOrgStatusHandler serves GET /api/v1/internal/orgs/:orgID/status — the cluster-internal endpoint the workspace controller polls (with a 30s cache) to drive org-suspension of workspaces (D20, US-43.19).

This endpoint is intentionally NOT behind AuthMiddleware: the controller has no user identity. The PRIMARY boundary is a mandatory shared-secret header (X-Internal-Token) read from LLMSAFESPACES_INTERNAL_TOKEN (F5, US-43.19). When the env var is unset the endpoint FAILS CLOSED with 403: serving it unauthenticated would let any pod that can route to the API enumerate which orgs are suspended. The chart sets the token on BOTH the API and the controller so a single mounted Secret configures both sides. The comparison is constant-time to avoid a timing leak of the shared secret. It MUST only ever return org status, never secrets.

func NewInternalOrgStatusHandler

func NewInternalOrgStatusHandler(store internalOrgStatusReader) *InternalOrgStatusHandler

NewInternalOrgStatusHandler constructs the handler.

func (*InternalOrgStatusHandler) GetOrgStatus

func (h *InternalOrgStatusHandler) GetOrgStatus(c *gin.Context)

GetOrgStatus handles GET /api/v1/internal/orgs/:orgID/status.

Fail-safe: a missing org row (hard-deleted or never existed) returns {"status":"active"} so the controller does NOT suspend the workspace. Per D20, an unwarranted suspension (deleting a running pod) is more disruptive than leaving it active during an anomaly.

type InvitationsHandler

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

InvitationsHandler handles org invitation CRUD and the accept/decline flows.

func NewInvitationsHandler

func NewInvitationsHandler(store invitationStore, mailer email.EmailProvider, authSvc orgAuthService, baseURL string, logger invitationLogger) *InvitationsHandler

NewInvitationsHandler constructs the handler. email may be nil in which case Create/Resend succeed but no email is sent (dev mode). logger may be nil.

func (*InvitationsHandler) Accept

func (h *InvitationsHandler) Accept(c *gin.Context)

Accept handles POST /api/v1/invitations/:token/accept (JWT required).

func (*InvitationsHandler) Create

func (h *InvitationsHandler) Create(c *gin.Context)

Create handles POST /api/v1/orgs/:id/invitations.

func (*InvitationsHandler) Decline

func (h *InvitationsHandler) Decline(c *gin.Context)

Decline handles POST /api/v1/invitations/:token/decline (JWT required).

func (*InvitationsHandler) Delete

func (h *InvitationsHandler) Delete(c *gin.Context)

Delete handles DELETE /api/v1/orgs/:id/invitations/:invID.

func (*InvitationsHandler) GetByToken

func (h *InvitationsHandler) GetByToken(c *gin.Context)

GetByToken handles GET /api/v1/invitations/:token (public — no auth).

func (*InvitationsHandler) List

func (h *InvitationsHandler) List(c *gin.Context)

List handles GET /api/v1/orgs/:id/invitations.

func (*InvitationsHandler) Resend

func (h *InvitationsHandler) Resend(c *gin.Context)

Resend handles POST /api/v1/orgs/:id/invitations/:invID/resend. Generates a new token (invalidating the old one), resets the expiry, and re-sends.

func (*InvitationsHandler) SetCredentialBinder

func (h *InvitationsHandler) SetCredentialBinder(b orgCredentialBinder)

SetCredentialBinder wires the org credential binder used after invitation acceptance (F7). Optional — nil means no credential seeding on join.

func (*InvitationsHandler) VerifyUserForInvitation

func (h *InvitationsHandler) VerifyUserForInvitation(c *gin.Context)

VerifyUserForInvitation handles POST /api/v1/orgs/:id/invitations/:invID/verify-user.

Org-admin only (registered under orgAdminGroup). The "member force-verify" surface added in PR #343 only acted on already-accepted members; this handler closes the gap for *pending* invitations: an admin can flip the invitee's users.email_verified=true so the invitee (who already has an account but never completed email verification) can log in. The invitation row stays pending — the user must still click the invitation link to accept and join the org.

Behavior:

  • Invitation exists, belongs to this org, and is still pending → look up users.id by inv.email; if found, MarkUserEmailVerified(userID); audit.
  • User does NOT exist → 422 {"error":"no_account_for_email"}. The frontend uses the machine-parseable code to render a clear "user must sign up first" message rather than treating it as a transient error.
  • Cross-org invitation → 404 (do not leak invitation existence across orgs).
  • Already accepted/declined → 409 (use member.verify on the resulting member).
  • Expired → 410 (matches Accept's behavior).

Idempotent at the DB level. The audit event records the admin's intent regardless of whether the user was previously verified.

type KeyRotator

type KeyRotator interface {
	RotateKeyWithPassword(ctx context.Context, userID string, password []byte, sessionID string, ttl time.Duration) (secrets.RotationResult, error)
	ChangePassword(ctx context.Context, userID, sessionID string, oldPassword, newPassword []byte) error
	ResetWithRecoveryKey(ctx context.Context, userID string, recoveryKeyHex string, newPassword []byte) (string, error)
}

KeyRotator is the interface needed by the rotation handler.

type LoginDiscoveryHandler

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

LoginDiscoveryHandler implements POST /api/v1/auth/lookup — the email-led login discovery endpoint (Epic 54, US-54.1).

The endpoint resolves an email to a single redirect URL pointing at the user's org (subdomain if routing is enabled, direct SSO start URL otherwise). The response shape is uniform across all non-validation branches:

200 OK
{ "redirectUrl": "<url>" }

Found users with an org get a real redirect; everyone else (not found, no org, suspended, DB error) gets the same 200 with a not-found redirect URL. This matches the password_reset.go:119 enumeration-safe precedent exactly: uniform status, uniform body shape, no timing pad.

func NewLoginDiscoveryHandler

func NewLoginDiscoveryHandler(
	users loginDiscoveryUserLookup,
	orgs loginDiscoveryOrgLookup,
	baseDomain string,
	log loginDiscoveryLogger,
) *LoginDiscoveryHandler

NewLoginDiscoveryHandler constructs the handler. baseDomain is the subdomain base (e.g. "app.example.com"); when empty, the handler falls back to the direct SSO start URL (/api/v1/auth/sso/<slug>/start) which works regardless of chart config.

func (*LoginDiscoveryHandler) Lookup

func (h *LoginDiscoveryHandler) Lookup(c *gin.Context)

Lookup handles POST /api/v1/auth/lookup.

Enumeration-safe: always returns 200 with { redirectUrl } on valid input. DB errors are logged and masked — never surfaced as 5xx.

type MetricsRecorder

type MetricsRecorder interface {
	RecordAgentReload(result string, durationMs int64, drained bool)
	RecordAgentReloadDrainTimeout(elapsedMs int64)
	RecordAgentReloadBulk(total, succeeded, failed int)
}

MetricsRecorder is the minimal metrics interface for reload handlers.

type ModelAvailability

type ModelAvailability string
const (
	ModelAvailable   ModelAvailability = "available"
	ModelUnavailable ModelAvailability = "unavailable"
	ModelFreeTier    ModelAvailability = "free"
)

type ModelCache

type ModelCache interface {
	Get(workspaceID string) []byte
	Set(workspaceID string, data []byte)
	Evict(workspaceID string)
	Keys() []string
}

ModelCache abstracts model catalog caching. Default: in-process map.

func NewInMemoryModelCache

func NewInMemoryModelCache() ModelCache

type ModelCatalogParser

type ModelCatalogParser interface {
	Parse(raw []byte) (*Catalog, error)
}

ModelCatalogParser decodes an agent's model catalog response into the normalized Catalog shape. The concrete implementation is selected at construction time based on the agent type/version — callers never branch on the wire format.

Worklog 0377 H1-a′: the interface exists so that opencode schema drift across versions (and, if a future runtime runs a different agent, a different wire format) is handled by adding a parser variant rather than branching in every caller. One implementation exists today (opencodeProviderParser); the interface earns its keep because the opencode /provider schema is documented to vary by version.

func NewOpencodeProviderParser

func NewOpencodeProviderParser() ModelCatalogParser

NewOpencodeProviderParser returns a parser for opencode's /provider format.

type ModelClient

type ModelClient interface {
	ListModels(ctx context.Context, userID, workspaceID string) ([]byte, error)
	PatchConfig(ctx context.Context, userID, workspaceID string, config map[string]any) error
}

ModelClient is the caller-shaped interface ModelsHandler needs from an agent client: fetch the catalog and push config changes. Worklog 0377 H2-a: split from the fat AgentClient (5 methods) so the handler depends on exactly the 2 methods it uses, and fakes only need to stub 2.

type ModelSelectionRecorder

type ModelSelectionRecorder interface {
	RecordModelSelection(modelID, providerID string)
}

ModelSelectionRecorder records model selection events for billing/metering.

type ModelSelectionRequest

type ModelSelectionRequest struct {
	Model string `json:"model" binding:"required"`
}

ModelSelectionRequest is the request body for PUT /workspaces/:id/model.

type ModelStore

type ModelStore interface {
	UpdateWorkspace(ctx context.Context, workspaceID string, updates types.WorkspaceUpdates) error
	GetDefaultModel(ctx context.Context, workspaceID string) (string, error)
	GetWorkspace(ctx context.Context, workspaceID string) (*types.WorkspaceMetadata, error)
}

ModelStore provides all database operations needed by model endpoints. Satisfied by *database.Service.

type ModelsHandler

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

ModelsHandler handles GET /workspaces/:id/models and PUT /workspaces/:id/model (US-29.5). Extracted from SecretsHandler to enforce single responsibility. Consumes ModelClient (H2-a) for opencode HTTP communication and ModelCatalogParser (H1-a′) to decode the catalog response into a typed Catalog.

func NewModelsHandler

func NewModelsHandler(agentClient ModelClient) *ModelsHandler

NewModelsHandler creates a ModelsHandler with the required ModelClient. The parser defaults to opencodeProviderParser; override via SetCatalogParser for tests or a future agent variant. Optional deps via the Set methods.

func (*ModelsHandler) ListModels

func (h *ModelsHandler) ListModels(c *gin.Context)

ListModels handles GET /api/v1/workspaces/:id/models.

func (*ModelsHandler) SetAgentClient

func (h *ModelsHandler) SetAgentClient(ac ModelClient)

func (*ModelsHandler) SetCatalogParser

func (h *ModelsHandler) SetCatalogParser(p ModelCatalogParser)

func (*ModelsHandler) SetLogger

func (*ModelsHandler) SetMetricsRecorder

func (h *ModelsHandler) SetMetricsRecorder(r ModelSelectionRecorder)

func (*ModelsHandler) SetModel

func (h *ModelsHandler) SetModel(c *gin.Context)

SetModel handles PUT /api/v1/workspaces/:id/model.

func (*ModelsHandler) SetModelCache

func (h *ModelsHandler) SetModelCache(c ModelCache)

func (*ModelsHandler) SetModelStore

func (h *ModelsHandler) SetModelStore(s ModelStore)

func (*ModelsHandler) SetPolicyChecker

func (h *ModelsHandler) SetPolicyChecker(p OrgPolicyChecker)

func (*ModelsHandler) SetRelayActive

func (h *ModelsHandler) SetRelayActive(active bool)

func (*ModelsHandler) SetRelayChecker

func (h *ModelsHandler) SetRelayChecker(rc RelayStateChecker)

type OrgBilling

type OrgBilling interface {
	CreateCheckoutSession(ctx context.Context, customerID, planID, successURL, cancelURL string) (string, error)
	CreatePortalSession(ctx context.Context, customerID, returnURL string) (string, error)
}

OrgBilling creates Checkout/Portal sessions for org subscription management (plan upgrades, billing portal access). Customer creation was removed with the self-service org-creation flow (design 0031 D1).

func NewOrgBilling

func NewOrgBilling(p billing.CheckoutProvider) OrgBilling

NewOrgBilling adapts a billing.CheckoutProvider into an OrgBilling.

type OrgCredentialsHandler

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

OrgCredentialsHandler handles org credential endpoints.

func NewOrgCredentialsHandler

func NewOrgCredentialsHandler(store CredentialStore, orgOps orgBindingAndAutoApplyStore, provider secrets.RootKeyProvider, authSvc orgAuthService) *OrgCredentialsHandler

NewOrgCredentialsHandler creates a new OrgCredentialsHandler.

func (*OrgCredentialsHandler) Create

func (h *OrgCredentialsHandler) Create(c *gin.Context)

Create handles POST /api/v1/orgs/:id/credentials.

func (*OrgCredentialsHandler) CreateAutoApply

func (h *OrgCredentialsHandler) CreateAutoApply(c *gin.Context)

CreateAutoApply handles POST /api/v1/orgs/:id/credentials/:credID/auto-apply.

func (*OrgCredentialsHandler) Delete

func (h *OrgCredentialsHandler) Delete(c *gin.Context)

Delete handles DELETE /api/v1/orgs/:id/credentials/:credID.

func (*OrgCredentialsHandler) DeleteAutoApply

func (h *OrgCredentialsHandler) DeleteAutoApply(c *gin.Context)

DeleteAutoApply handles DELETE /api/v1/orgs/:id/credentials/:credID/auto-apply.

func (*OrgCredentialsHandler) List

func (h *OrgCredentialsHandler) List(c *gin.Context)

List handles GET /api/v1/orgs/:id/credentials.

func (*OrgCredentialsHandler) ListAutoApply

func (h *OrgCredentialsHandler) ListAutoApply(c *gin.Context)

ListAutoApply handles GET /api/v1/orgs/:id/credentials/:credID/auto-apply.

func (*OrgCredentialsHandler) ProbeModels

func (h *OrgCredentialsHandler) ProbeModels(c *gin.Context)

ProbeModels handles GET /api/v1/orgs/:id/credentials/:credID/models. It decrypts the stored credential and calls the provider's /v1/models (OpenAI-compatible) to discover available model IDs, merged with any saved context limits so the UI can pre-populate the config table.

func (*OrgCredentialsHandler) Update

func (h *OrgCredentialsHandler) Update(c *gin.Context)

Update handles PUT /api/v1/orgs/:id/credentials/:credID.

type OrgPolicyChecker

type OrgPolicyChecker interface {
	GetEffectivePolicy(ctx context.Context, orgID string) (*types.OrgPolicyValues, error)
}

OrgPolicyChecker is the minimal interface needed to filter models by org policy. The policy.Service implements it.

type OrgsHandler

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

OrgsHandler handles org CRUD endpoints.

func NewOrgsHandler

func NewOrgsHandler(
	store orgStore,
	authSvc orgAuthService,
) *OrgsHandler

NewOrgsHandler creates a new OrgsHandler.

func (*OrgsHandler) AddMember

func (h *OrgsHandler) AddMember(c *gin.Context)

AddMember handles POST /api/v1/orgs/:id/members.

func (*OrgsHandler) ChangeMemberRole

func (h *OrgsHandler) ChangeMemberRole(c *gin.Context)

ChangeMemberRole handles PUT /api/v1/orgs/:id/members/:userID.

func (*OrgsHandler) Checkout

func (h *OrgsHandler) Checkout(c *gin.Context)

Checkout handles POST /api/v1/orgs/:id/billing/checkout. Creates a Stripe Checkout Session for the requested plan and returns its hosted URL.

func (*OrgsHandler) Create

func (h *OrgsHandler) Create(c *gin.Context)

Create handles POST /api/v1/orgs.

Per design 0031 D1, org creation is platform-admin only. Non-admin callers receive 403. A platform admin supplies the intended owner's email; the backend resolves it to a user ID (single lookup, 404 when no such user) and creates the org already active with the requested plan (default enterprise), adding the resolved user as the org's first admin member. The caller admin is recorded as CreatedBy; the owner is the email-resolved user. No Stripe Checkout session is created at org-creation time — the self-service flow is deferred to the future billing-portal epic.

func (*OrgsHandler) Delete

func (h *OrgsHandler) Delete(c *gin.Context)

Delete handles DELETE /api/v1/orgs/:id.

func (*OrgsHandler) Get

func (h *OrgsHandler) Get(c *gin.Context)

Get handles GET /api/v1/orgs/:id.

func (*OrgsHandler) GetOrg

func (h *OrgsHandler) GetOrg(ctx context.Context, orgID string) (*types.Organization, error)

GetOrg exposes orgStore.GetOrg so middleware.FeatureGuard can read the org's plan without depending on the store directly. Satisfies orgPlanReader.

func (*OrgsHandler) IsOrgAdmin

func (h *OrgsHandler) IsOrgAdmin(ctx context.Context, orgID, userID string) (bool, error)

IsOrgAdmin satisfies the middleware.orgMemberChecker interface by delegating to orgStore.

func (*OrgsHandler) IsOrgMember

func (h *OrgsHandler) IsOrgMember(ctx context.Context, orgID, userID string) (bool, error)

IsOrgMember satisfies the middleware.orgMemberChecker interface by delegating to orgStore.

func (*OrgsHandler) List

func (h *OrgsHandler) List(c *gin.Context)

List handles GET /api/v1/orgs.

func (*OrgsHandler) ListMembers

func (h *OrgsHandler) ListMembers(c *gin.Context)

ListMembers handles GET /api/v1/orgs/:id/members.

func (*OrgsHandler) ListWorkspaces

func (h *OrgsHandler) ListWorkspaces(c *gin.Context)

ListWorkspaces handles GET /api/v1/orgs/:id/workspaces.

func (*OrgsHandler) Portal

func (h *OrgsHandler) Portal(c *gin.Context)

Portal handles POST /api/v1/orgs/:id/billing/portal. Creates a Stripe Customer Portal Session and returns its URL.

func (*OrgsHandler) RemoveMember

func (h *OrgsHandler) RemoveMember(c *gin.Context)

RemoveMember handles DELETE /api/v1/orgs/:id/members/:userID.

func (*OrgsHandler) SetBilling

func (h *OrgsHandler) SetBilling(b OrgBilling, successURL, cancelURL, portalURL string)

SetBilling wires the Stripe checkout/portal provider and redirect URLs. When not called (or passed a nil provider), org creation succeeds but produces no checkout URL — development mode without Stripe configured.

func (*OrgsHandler) SetLogger

func (h *OrgsHandler) SetLogger(l orgsLogger)

SetLogger wires an optional logger used to surface non-fatal audit emission failures (e.g. a VerifyMember action that succeeds but whose audit row could not be written). When not called, audit failures are silent.

func (*OrgsHandler) Update

func (h *OrgsHandler) Update(c *gin.Context)

Update handles PUT /api/v1/orgs/:id.

func (*OrgsHandler) VerifyMember

func (h *OrgsHandler) VerifyMember(c *gin.Context)

VerifyMember handles POST /api/v1/orgs/:id/members/:userID/verify.

Org-admin only (OrgAdminGuard middleware). Marks the member's user account as email_verified=true, bypassing the email-verification token flow. Use cases: the admin has confirmed the member's identity out-of-band (e.g. in person, via a trusted channel), or the member cannot receive the verification email and the admin chooses to override.

Idempotent: verifying an already-verified member returns 200. The action is recorded in the org audit log (domain='org', action='member.verify') with the actor and target IDs so the override is traceable.

type PasswordHashUpdater

type PasswordHashUpdater interface {
	UpdatePasswordHash(ctx context.Context, userID string, newPassword []byte) error
}

PasswordHashUpdater updates the user's bcrypt hash in the database.

type PasswordResetHandler

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

PasswordResetHandler handles the password-reset-via-email flow.

func NewPasswordResetHandler

func NewPasswordResetHandler(
	store passwordResetStore,
	users passwordResetUserLookup,
	keyInit passwordResetKeyInitializer,
	pwUpdate passwordResetPwUpdater,
	revoker passwordResetSessionRevoker,
	email *emailsvc.Service,
	log passwordResetLogger,
) *PasswordResetHandler

NewPasswordResetHandler constructs the handler. email may carry a nil provider (noop mode); in that case SendPasswordReset returns ErrNotConfigured and the request endpoint logs a warning but still returns 202 (the token is still created; the email is just not sent).

func (*PasswordResetHandler) Confirm

func (h *PasswordResetHandler) Confirm(c *gin.Context)

Confirm handles POST /api/v1/auth/password-reset/confirm.

Public (the token IS the credential). Verifies the token hash, checks expiry + consumption, then executes the reset:

  1. Consume token (single-use)
  2. Update bcrypt hash (FIRST — avoids unrecoverable state if DEK reinit fails)
  3. Reinitialise DEK (old DEK unrecoverable without old password/recovery key)
  4. Revoke all outstanding sessions
  5. Send "password changed" notification email

Returns the new recovery key so the user can save it.

func (*PasswordResetHandler) Request

func (h *PasswordResetHandler) Request(c *gin.Context)

Request handles POST /api/v1/auth/password-reset/request.

Always returns 202 (no email enumeration). Only sends a reset email if:

  • the user exists
  • the user's email is verified (don't send to unverified mailboxes)

func (*PasswordResetHandler) SetSecretPurger

func (h *PasswordResetHandler) SetSecretPurger(p passwordResetSecretPurger)

SetSecretPurger wires the secret-row purger. Optional: when not set, reset does not delete the (already-cryptographically-erased) rows. Mirrors the setter-injection pattern of secrets.KeyService.SetSecretStore.

func (*PasswordResetHandler) SetWorkspaceNeutralizer

func (h *PasswordResetHandler) SetWorkspaceNeutralizer(n passwordResetWorkspaceNeutralizer)

SetWorkspaceNeutralizer wires the workspace suspend/scrub step. Optional: when not set, reset does not touch running workspaces.

type PasswordVerifier

type PasswordVerifier interface {
	VerifyPassword(ctx context.Context, userID string, password []byte) error
}

PasswordVerifier confirms a user's password against the stored bcrypt hash. Used by RevealSecret to enforce a re-authentication gate before returning plaintext: a stolen JWT alone must not be sufficient to extract every secret. Implementations MUST run constant-time comparison (bcrypt.CompareHashAndPassword satisfies this) and MUST return a sentinel-typed error rather than the raw bcrypt error so the handler can map it to a uniform 403 without leaking timing or state information.

type PendingOrgCleaner

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

PendingOrgCleaner reaps pending_activation orgs whose Stripe checkout was never completed. It runs on a ticker; each run lists stale pending orgs, verifies the checkout state with Stripe, and either activates (paid but webhook lost) or hard-deletes (checkout expired/canceled). On any Stripe API failure for a given org it skips that org and retries next cycle — never deletes an org it cannot verify.

func NewPendingOrgCleaner

func NewPendingOrgCleaner(store pendingOrgStore, provider billing.CheckoutProvider, logger webHookLogger, interval, maxAge time.Duration) *PendingOrgCleaner

NewPendingOrgCleaner constructs the cleaner. interval is the tick period; maxAge is how old a pending_activation org must be before it is eligible. provider is used to build the default Stripe checkout lookup; pass nil only in tests that inject checkoutCompletedFn directly.

func (*PendingOrgCleaner) Run

func (c *PendingOrgCleaner) Run(ctx context.Context)

Run blocks until ctx is canceled, reaping on each tick.

type PendingReloadLister

type PendingReloadLister interface {
	ListPendingReloadWorkspaces(ctx context.Context, userID string) ([]*types.WorkspaceMetadata, error)
}

PendingReloadLister lists workspaces with pending credential reload.

type PlatformAdminHandler

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

PlatformAdminHandler implements the platform-admin org/user suspension endpoints (D19, US-43.19). All routes are mounted behind AuthMiddleware + AdminGuard (users.role='admin'), so every method here runs in a platform-admin context only.

func NewPlatformAdminHandler

func NewPlatformAdminHandler(orgs platformAdminOrgStore, users platformAdminUserStore, authSvc orgAuthService, revoker platformUserRevoker, logger policyLogger) *PlatformAdminHandler

NewPlatformAdminHandler constructs the handler. revoker wires the F4 token revocation primitive (pass nil only in tests that do not exercise it). logger surfaces audit-write failures (audit is best-effort: a failed audit log never blocks the mutation, matching the existing policy/audit pattern).

func (*PlatformAdminHandler) ListOrgs

func (h *PlatformAdminHandler) ListOrgs(c *gin.Context)

ListOrgs handles GET /api/v1/admin/orgs. Returns every organization with aggregated member + workspace counts for the platform-admin dashboard. Optional query params: limit (default 50, max 200), offset (>=0), status (one of pending_activation|active|suspended).

func (*PlatformAdminHandler) ListUsers

func (h *PlatformAdminHandler) ListUsers(c *gin.Context)

ListUsers handles GET /api/v1/admin/users. Returns every user (sans password hash) with their single org membership resolved. Optional query params: limit (default 50, max 200), offset (>=0), status (active|suspended).

func (*PlatformAdminHandler) SuspendOrg

func (h *PlatformAdminHandler) SuspendOrg(c *gin.Context)

SuspendOrg handles POST /api/v1/admin/orgs/:id/suspend.

Sets organizations.status='suspended'. Per D20 the operational effect (pod termination) is applied asynchronously by the controller querying the org-status cache on its next reconcile cycle — this endpoint only flips the authoritative status. The audit event is org-scoped so org admins see it in their own audit log.

func (*PlatformAdminHandler) SuspendUser

func (h *PlatformAdminHandler) SuspendUser(c *gin.Context)

SuspendUser handles POST /api/v1/admin/users/:id/suspend.

Atomically refuses when the user is the sole active admin of any org (unless ?force=true), then sets users.status='suspended' and revokes the user's live tokens. The check + update run in one transaction (F7) so concurrent admin operations cannot orphan an org; the revocation marker (F4) makes the suspension take effect immediately, even during a DB blip.

func (*PlatformAdminHandler) UnsuspendOrg

func (h *PlatformAdminHandler) UnsuspendOrg(c *gin.Context)

UnsuspendOrg handles POST /api/v1/admin/orgs/:id/unsuspend.

Sets organizations.status='active'. Per D20 the controller does NOT auto-resume workspaces; members/admins must manually resume each one.

func (*PlatformAdminHandler) UnsuspendUser

func (h *PlatformAdminHandler) UnsuspendUser(c *gin.Context)

UnsuspendUser handles POST /api/v1/admin/users/:id/unsuspend.

type PodBootstrapHandler

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

PodBootstrapHandler handles POST /internal/v1/pod-bootstrap — the secretless credential injection endpoint (Epic 35 US-35.3).

Auth is via K8s TokenReview (projected SA token, audience "llmsafespace-api"). No JWT middleware — the init container has no user identity. The handler verifies the SA name matches workspace-<workspaceID> AND the SA namespace matches the expected workspace namespace to enforce pod-to-workspace isolation: a compromised workspace pod can only retrieve its own credentials.

func NewPodBootstrapHandler

func NewPodBootstrapHandler(reviewer TokenReviewer, injector bootstrapInjector, lookup bootstrapWorkspaceLookup, promptSvc *prompt.Service, expectedNamespace string) *PodBootstrapHandler

NewPodBootstrapHandler constructs the handler. In production, pass a *k8sTokenReviewer wrapping the API's K8s clientset. expectedNamespace is the K8s namespace where workspace ServiceAccounts live — validated against the SA namespace in the TokenReview username (S1 defense-in-depth).

func NewPodBootstrapHandlerFromClientset

func NewPodBootstrapHandlerFromClientset(clientset kubernetes.Interface, injector bootstrapInjector, lookup bootstrapWorkspaceLookup, promptSvc *prompt.Service, expectedNamespace string) *PodBootstrapHandler

NewPodBootstrapHandlerFromClientset is the production constructor that wraps a kubernetes.Interface into a k8sTokenReviewer.

func (*PodBootstrapHandler) Bootstrap

func (h *PodBootstrapHandler) Bootstrap(c *gin.Context)

Bootstrap handles POST /internal/v1/pod-bootstrap.

func (*PodBootstrapHandler) HasLogger

func (h *PodBootstrapHandler) HasLogger() bool

HasLogger reports whether a logger has been wired. Used by the app-level wiring test to enforce that production constructs the handler with SetLogger called — without this, the underlying error in 5xx responses is silently dropped (the very gap PR #407 closed).

func (*PodBootstrapHandler) SetLogger

SetLogger installs a structured logger so the handler can emit diagnostic events for 5xx responses. Without this, the handler returns a generic "secret preparation failed" body and the underlying error (e.g. "DEK not available", "decrypt failed", "DB timeout") is silently dropped — exactly the observability gap that turned the 2026-06-24 outage into a 30-minute diagnosis exercise instead of a 1-minute one.

The logger is optional: when nil, the handler falls back to a silent no-op so unit tests that don't care about log emission can omit it. Production wiring (api/internal/app/app.go) MUST install one — see TestPodBootstrapHandler_LoggerWired in api/internal/app/ for the regression guard that enforces this.

func (*PodBootstrapHandler) SetPromptService

func (h *PodBootstrapHandler) SetPromptService(svc *prompt.Service)

SetPromptService wires the prompt resolution service after construction. Used when the prompt service is built later in the startup sequence.

type PodIPResolver

type PodIPResolver interface {
	GetWorkspacePodIP(ctx context.Context, userID, workspaceID string) (string, error)
}

PodIPResolver looks up the pod IP for a workspace.

type PolicyHandler

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

PolicyHandler handles GET/PUT/DELETE /api/v1/orgs/:id/policies (admin-only).

func NewPolicyHandler

func NewPolicyHandler(store policyStore, svc *policy.Service, authSvc orgAuthService, logger policyLogger) *PolicyHandler

NewPolicyHandler constructs the handler. svc is used for cache invalidation after mutations. logger is used to surface audit emission failures.

func (*PolicyHandler) Delete

func (h *PolicyHandler) Delete(c *gin.Context)

Delete handles DELETE /api/v1/orgs/:id/policies/:key. Removes a policy (reverts to default / unrestricted).

func (*PolicyHandler) Get

func (h *PolicyHandler) Get(c *gin.Context)

Get handles GET /api/v1/orgs/:id/policies. Returns all configured policies.

func (*PolicyHandler) Put

func (h *PolicyHandler) Put(c *gin.Context)

Put handles PUT /api/v1/orgs/:id/policies/:key. Upserts a single policy.

type ProbeModelEntry

type ProbeModelEntry struct {
	ID           string `json:"id"`
	ContextLimit int    `json:"contextLimit"` // 0 = unknown / not configured
	OutputLimit  int    `json:"outputLimit"`  // 0 = unknown / not configured
}

ProbeModelEntry is one model returned by the probe endpoint.

type ProbeModelsRequest

type ProbeModelsRequest struct {
	APIKey  string `json:"apiKey" binding:"required" log:"-"` //nolint:gosec // G117 false positive — field has log:"-" tag, never marshaled to response
	BaseURL string `json:"baseURL" binding:"required"`
}

ProbeModelsRequest is the body for POST /api/v1/probe-models — a credential-free probe for use before a credential is saved.

type ProbeModelsResponse

type ProbeModelsResponse struct {
	Models  []ProbeModelEntry `json:"models"`
	BaseURL string            `json:"baseURL,omitempty"`
	// Warning is set when the /v1/models call failed. The response still
	// succeeds (200) but Models is empty so the UI shows a friendly message.
	Warning string `json:"warning,omitempty"`
}

ProbeModelsResponse is the response body for GET /:id/models.

type PromptHandler

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

PromptHandler handles platform and org prompt CRUD endpoints.

func NewPromptHandler

func NewPromptHandler(store promptStore, svc *prompt.Service, authSvc orgAuthService, logger policyLogger) *PromptHandler

NewPromptHandler constructs the handler.

func (*PromptHandler) GetOrg

func (h *PromptHandler) GetOrg(c *gin.Context)

GetOrg handles GET /api/v1/orgs/:id/prompt.

func (*PromptHandler) GetPlatform

func (h *PromptHandler) GetPlatform(c *gin.Context)

GetPlatform handles GET /api/v1/admin/prompt.

func (*PromptHandler) GetWorkspacePrompt

func (h *PromptHandler) GetWorkspacePrompt(c *gin.Context)

GetWorkspacePrompt handles GET /api/v1/workspaces/:id/prompt.

func (*PromptHandler) SetOrg

func (h *PromptHandler) SetOrg(c *gin.Context)

SetOrg handles PUT /api/v1/orgs/:id/prompt.

func (*PromptHandler) SetPlatform

func (h *PromptHandler) SetPlatform(c *gin.Context)

SetPlatform handles PUT /api/v1/admin/prompt.

func (*PromptHandler) SetWorkspacePrompt

func (h *PromptHandler) SetWorkspacePrompt(c *gin.Context)

SetWorkspacePrompt handles PUT /api/v1/workspaces/:id/prompt. Respects the org's allow_user_prompt toggle — returns 403 when locked.

type ProviderModelCost

type ProviderModelCost struct {
	Input  float64 `json:"input"`
	Output float64 `json:"output"`
}

ProviderModelCost holds the per-token cost for a model. Zero on both fields identifies a free-tier model (e.g. opencode's built-in models).

type ProxyHandler

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

func NewProxyHandler

func NewProxyHandler(
	k8sClient pkginterfaces.KubernetesClient,
	logger pkginterfaces.LoggerInterface,
	namespace string,
	httpClient *http.Client,
	dialect agent.Dialect,
) (*ProxyHandler, error)

func (*ProxyHandler) AbortSession

func (h *ProxyHandler) AbortSession(c *gin.Context)

func (*ProxyHandler) BackfillSessionParents

func (h *ProxyHandler) BackfillSessionParents(ctx context.Context, workspaceID string)

func (*ProxyHandler) CreateSession

func (h *ProxyHandler) CreateSession(c *gin.Context)

func (*ProxyHandler) DeleteQueueMessage

func (h *ProxyHandler) DeleteQueueMessage(c *gin.Context)

func (*ProxyHandler) DeleteSession

func (h *ProxyHandler) DeleteSession(c *gin.Context)

func (*ProxyHandler) EnableSessionParentResolution

func (h *ProxyHandler) EnableSessionParentResolution()

func (*ProxyHandler) EnqueueMessage

func (h *ProxyHandler) EnqueueMessage(c *gin.Context)

func (*ProxyHandler) GetActiveSessions

func (h *ProxyHandler) GetActiveSessions(ctx context.Context, workspaceID string) []string

GetActiveSessions returns the IDs of all sessions currently marked active for the workspace. Public because it is called from outside the handlers package (admin tooling, canary checks).

func (*ProxyHandler) GetAllKnownPhases

func (h *ProxyHandler) GetAllKnownPhases() map[string]string

func (*ProxyHandler) GetBroker

func (h *ProxyHandler) GetBroker() BrokerPublisher

func (*ProxyHandler) GetCachedPasswordForTest

func (h *ProxyHandler) GetCachedPasswordForTest(workspaceID string) (string, bool)

GetCachedPasswordForTest returns whether a password is cached for the workspace (used by tests asserting cache invalidation).

func (*ProxyHandler) GetHistory

func (h *ProxyHandler) GetHistory(c *gin.Context)

GetHistory returns a chronological page of displayable messages for a session.

Query parameters:

  • limit: page size (default 50, max 200). Counts displayable messages only — system-role messages and messages whose parts collapse to nothing visible (e.g. only step-start/step-finish) do not count against the limit. Rejecting invalid limits (<=0 or non-numeric) surfaces client bugs early.
  • before: opaque cursor — the message id of the OLDEST message in the previously-rendered page. Returns messages strictly older than this cursor. Absent => return the newest `limit` messages.

Response:

  • body: JSON array of opencode message objects, oldest-first within the page. Schema preserved as-is so the frontend's transformHistory keeps working.
  • X-Next-Cursor header: present iff more (older) messages exist; its value is the id of the OLDEST message in the returned page. Absent means there are no more messages to fetch.

The handler fetches the FULL upstream array from opencode (which does not paginate), filters to displayable messages server-side, then slices. Filtering server-side prevents jumpy page sizes that would otherwise happen if the frontend filtered after receiving the page.

func (*ProxyHandler) GetMessageQueueService

func (h *ProxyHandler) GetMessageQueueService() interfaces.MessageQueueService

func (*ProxyHandler) GetPasswordGetter

func (h *ProxyHandler) GetPasswordGetter() interfaces.WorkspacePasswordProvider

func (*ProxyHandler) GetPriorPhaseForTest

func (h *ProxyHandler) GetPriorPhaseForTest(workspaceID string) (string, bool)

GetPriorPhaseForTest returns the prior-phase entry if present.

func (*ProxyHandler) GetSSETracker

func (h *ProxyHandler) GetSSETracker() *sse.Tracker

func (*ProxyHandler) GetSession

func (h *ProxyHandler) GetSession(c *gin.Context)

func (*ProxyHandler) GetWorkspaceConfigForTest

func (h *ProxyHandler) GetWorkspaceConfigForTest(workspaceID string) (wsstate.Config, bool)

GetWorkspaceConfigForTest returns the cached config for the workspace.

func (*ProxyHandler) GetWorkspaceOwner

func (h *ProxyHandler) GetWorkspaceOwner(workspaceID string) string

func (*ProxyHandler) HasActiveWorkspaceForTest

func (h *ProxyHandler) HasActiveWorkspaceForTest(workspaceID string) bool

HasActiveWorkspaceForTest reports whether the workspace currently has any active sessions (i.e. an active set was created and is non-empty). Used by tests asserting that the per-workspace entry is cleaned up after the last session is removed.

func (*ProxyHandler) ListPermissions

func (h *ProxyHandler) ListPermissions(c *gin.Context)

ListPermissions proxies GET /permission to the workspace pod.

func (*ProxyHandler) ListQuestions

func (h *ProxyHandler) ListQuestions(c *gin.Context)

ListQuestions proxies GET /question to the workspace pod.

func (*ProxyHandler) ListQueue

func (h *ProxyHandler) ListQueue(c *gin.Context)

func (*ProxyHandler) ListSessions

func (h *ProxyHandler) ListSessions(c *gin.Context)

func (*ProxyHandler) MarkSessionDeletedForTest

func (h *ProxyHandler) MarkSessionDeletedForTest(workspaceID, sessionID string)

MarkSessionDeletedForTest seeds a deleted-session tombstone.

func (*ProxyHandler) PermissionReply

func (h *ProxyHandler) PermissionReply(c *gin.Context)

PermissionReply proxies POST /permission/:requestID/reply to the workspace pod.

func (*ProxyHandler) QuestionReject

func (h *ProxyHandler) QuestionReject(c *gin.Context)

QuestionReject proxies POST /question/:requestID/reject to the workspace pod.

func (*ProxyHandler) QuestionReply

func (h *ProxyHandler) QuestionReply(c *gin.Context)

QuestionReply proxies POST /question/:requestID/reply to the workspace pod.

func (*ProxyHandler) RenameSessionInAgent

func (h *ProxyHandler) RenameSessionInAgent(ctx context.Context, workspaceID, sessionID, title string) error

RenameSessionInAgent sends a title update to the opencode agent running on the workspace pod so that the agent's in-memory session title matches the user-assigned title. Without this, the periodic title fetch (useSessionTitle hook in the frontend) retrieves the old agent-side title and overwrites the user's rename in PostgreSQL.

func (*ProxyHandler) SendMessage

func (h *ProxyHandler) SendMessage(c *gin.Context)

func (*ProxyHandler) SendPromptAsync

func (h *ProxyHandler) SendPromptAsync(c *gin.Context)

func (*ProxyHandler) SetActiveSessionsForTest

func (h *ProxyHandler) SetActiveSessionsForTest(workspaceID string, sessionIDs []string)

SetActiveSessionsForTest seeds the active-session set for the workspace. Test-only — production callers must use CheckAndAddActiveSession so the maxSessions limit is enforced atomically. Kept as a public method on ProxyHandler so existing tests that poked the activeSess map can be migrated with a one-line change.

Contract: the maxSessions argument passed to CheckAndAddActiveSession is `len(sessionIDs)+1` so all seeds succeed regardless of duplicate IDs in the input. Tests that need to exercise oversubscribe handling (i.e. seed a state that violates the maxSessions invariant) must call CheckAndAddActiveSession directly, not this helper.

func (*ProxyHandler) SetAgentStateChecker

func (h *ProxyHandler) SetAgentStateChecker(c AgentStateChecker)

func (*ProxyHandler) SetCachedPasswordForTest

func (h *ProxyHandler) SetCachedPasswordForTest(workspaceID, password string)

SetCachedPasswordForTest seeds the password cache for a workspace.

func (*ProxyHandler) SetMessageQueueService

func (h *ProxyHandler) SetMessageQueueService(svc interfaces.MessageQueueService)

func (*ProxyHandler) SetMeteringService

func (h *ProxyHandler) SetMeteringService(svc interfaces.MeteringService)

func (*ProxyHandler) SetParentBackfilledForTest

func (h *ProxyHandler) SetParentBackfilledForTest(workspaceID string)

SetParentBackfilledForTest seeds the parent-backfill marker.

func (*ProxyHandler) SetPriorPhaseForTest

func (h *ProxyHandler) SetPriorPhaseForTest(workspaceID, phase string)

SetPriorPhaseForTest seeds the prior-phase entry.

func (*ProxyHandler) SetRequestBufferConfig

func (h *ProxyHandler) SetRequestBufferConfig(maxSize int, timeout time.Duration)

SetRequestBufferConfig rebuilds the per-workspace request buffer with the configured size and timeout. Must be called before Start: request goroutines read h.requestBuffer without synchronization, so a late swap would race. Values <=0 fall back to the enabled defaults (size 10, timeout 30s) so the feature is on unless explicitly constructed disabled — the zero-value config must not silently turn buffering off in production.

func (*ProxyHandler) SetSessionIndex

func (h *ProxyHandler) SetSessionIndex(si interfaces.SessionIndexService)

func (*ProxyHandler) SetStateStore

func (h *ProxyHandler) SetStateStore(store wsstate.Store)

SetStateStore overrides the per-workspace state store. By default the ProxyHandler uses an InMemoryStore (single-replica); app.go swaps in a RedisStore when a Redis/Valkey client is available so multi-replica deployments share active-session state. Panics if called after Start() — request goroutines read stateStore without synchronization, so a late swap would race.

func (*ProxyHandler) SetSweepInterval added in v0.2.0

func (h *ProxyHandler) SetSweepInterval(d time.Duration)

SetSweepInterval overrides the periodic queue-sweep interval (default 30s). Primarily for tests that need the goroutine to fire quickly. Must be called before Start().

func (*ProxyHandler) SetVersionSyncCallback

func (h *ProxyHandler) SetVersionSyncCallback(cb workspace.VersionSyncCallback)

func (*ProxyHandler) SetWorkspaceConfigForTest

func (h *ProxyHandler) SetWorkspaceConfigForTest(workspaceID string, cfg wsstate.Config)

SetWorkspaceConfigForTest seeds the workspace-config cache.

func (*ProxyHandler) SetWorkspaceUpdateCallback

func (h *ProxyHandler) SetWorkspaceUpdateCallback(cb workspace.WorkspaceUpdateCallback)

SetWorkspaceUpdateCallback installs the per-CRD-event callback that powers the watcher-driven auto-push of user-DEK secrets after pod recreation (worklog 0591). Must be called before Start().

func (*ProxyHandler) Start

func (h *ProxyHandler) Start() error

func (*ProxyHandler) Stop

func (h *ProxyHandler) Stop() error

func (*ProxyHandler) StreamEvents

func (h *ProxyHandler) StreamEvents(c *gin.Context)

func (*ProxyHandler) StreamUserEvents

func (h *ProxyHandler) StreamUserEvents(c *gin.Context)

StreamUserEvents is the user-scoped SSE endpoint (GET /api/v1/events). It delivers workspace.phase events for ALL of the user's workspaces.

func (*ProxyHandler) WorkspacePassword

func (h *ProxyHandler) WorkspacePassword(ctx context.Context, workspaceID string) (string, error)

WorkspacePassword implements interfaces.WorkspacePasswordProvider (US-46.11).

type QueueClearer

type QueueClearer interface {
	PeekAllWorkspace(ctx context.Context, workspaceID string) ([]msgqueue.QueuedMessage, error)
	ClearWorkspace(ctx context.Context, workspaceID string) error
}

QueueClearer is the minimal queue interface needed by the reload handler.

type RelayAdminHandler

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

RelayAdminHandler serves the relay admin setup wizard and status dashboard endpoints. The relay fleet supports AWS (paid primary), OCI (free secondary), and GCP (optional) providers — matching the InferenceRelay CRD enum `aws;oci;gcp`.

func NewRelayAdminHandler

func NewRelayAdminHandler(clientset kubernetes.Interface, llmClient interfaces.LLMSafespacesV1Interface, namespace, routerNamespace, routerSvcURL string) *RelayAdminHandler

NewRelayAdminHandler creates a new relay admin handler. namespace is the workspace namespace (for Secrets, CRDs). routerNamespace is the namespace where the relay-router Deployment lives.

func (*RelayAdminHandler) Deploy

func (h *RelayAdminHandler) Deploy(c *gin.Context)

Deploy creates or updates the InferenceRelay CR. Valid providers are "aws", "oci", and "gcp" — matching the CRD enum validation.

func (*RelayAdminHandler) GetSetup

func (h *RelayAdminHandler) GetSetup(c *gin.Context)

GetSetup returns the prerequisite checklist state for the relay setup wizard.

The checklist is network-stack agnostic: it verifies LLMSafeSpaces-owned prerequisites (relay-router Deployment, InferenceRelay CRD, provider credentials) but does NOT probe the network path between the router and relay VMs. Post-WG-removal (worklog 0442) the router dials relay VMs by public IP over HTTP with per-VM token auth; reachability is verified downstream via instance health in GetStatus.

func (*RelayAdminHandler) GetStatus

func (h *RelayAdminHandler) GetStatus(c *gin.Context)

GetStatus returns the full fleet status by aggregating CR status + router metrics.

func (*RelayAdminHandler) Pause

func (h *RelayAdminHandler) Pause(c *gin.Context)

Pause pauses the relay fleet — stops provisioning/replacing VMs.

func (*RelayAdminHandler) Resume

func (h *RelayAdminHandler) Resume(c *gin.Context)

Resume removes the pause annotation — controller resumes provisioning/replacing VMs.

func (*RelayAdminHandler) Rotate

func (h *RelayAdminHandler) Rotate(c *gin.Context)

Rotate triggers manual rotation of a specific relay instance.

func (*RelayAdminHandler) SaveAWSCreds

func (h *RelayAdminHandler) SaveAWSCreds(c *gin.Context)

SaveAWSCreds saves AWS IAM access key credentials for EC2 provisioning.

func (*RelayAdminHandler) SaveGCPCreds

func (h *RelayAdminHandler) SaveGCPCreds(c *gin.Context)

SaveGCPCreds saves GCP service account JSON to a K8s Secret.

func (*RelayAdminHandler) SaveOCICreds

func (h *RelayAdminHandler) SaveOCICreds(c *gin.Context)

SaveOCICreds saves OCI credentials to a K8s Secret.

func (*RelayAdminHandler) SetHTTPClient

func (h *RelayAdminHandler) SetHTTPClient(client *http.Client)

SetHTTPClient overrides the HTTP client (for testing).

type RelayStateChecker

type RelayStateChecker func(ctx context.Context, userID, workspaceID string) bool

RelayStateChecker returns whether the relay injector has completed for the given workspace. Production implementation resolves podIP + password and calls /v1/readyz on the agentd admin port (4098). This is separate from AgentClient (which targets opencode port 4096 with Basic auth) because the relay check uses the admin port with Bearer auth.

type RotateKeyHandler

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

RotateKeyHandler handles account key management endpoints.

func NewRotateKeyHandler

func NewRotateKeyHandler(keySvc KeyRotator) *RotateKeyHandler

NewRotateKeyHandler creates a new RotateKeyHandler.

func (*RotateKeyHandler) ChangePassword

func (h *RotateKeyHandler) ChangePassword(c *gin.Context)

ChangePassword handles POST /api/v1/account/change-password

func (*RotateKeyHandler) RecoverAccount

func (h *RotateKeyHandler) RecoverAccount(c *gin.Context)

RecoverAccount handles POST /api/v1/account/recover

func (*RotateKeyHandler) RotateKey

func (h *RotateKeyHandler) RotateKey(c *gin.Context)

RotateKey handles POST /api/v1/account/rotate-key. On success the response includes the new keyVersion AND a freshly- issued recoveryKey: the old recovery key wraps the now-discarded old DEK, so the user must save the new one. This is a one-time display — the API does not store it anywhere recoverable.

func (*RotateKeyHandler) SetAuditFunc

func (h *RotateKeyHandler) SetAuditFunc(f func(userID, action string))

SetAuditFunc sets an optional audit callback for key operations.

func (*RotateKeyHandler) SetPasswordUpdater

func (h *RotateKeyHandler) SetPasswordUpdater(u PasswordHashUpdater)

SetPasswordUpdater sets the optional password hash updater.

type SSOHandler

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

SSOHandler exposes both the org-admin SSO config CRUD and the public OIDC login flow (start/callback) and domain discovery. The OIDC mechanics live in the sso.Service; this handler owns HTTP concerns (cookies, redirects, response shaping).

func NewSSOHandler

func NewSSOHandler(svc *sso.Service, store ssoStore, authSvc orgAuthService, sessionCookie, cookieDomain, frontendURL string, logger ssoLogger) *SSOHandler

NewSSOHandler constructs the handler. sessionCookie is the JWT cookie name used elsewhere in the app (e.g. "lsp_session"); frontendURL is the post-SSO browser landing URL (may be empty — the handler falls back to "/"). cookieDomain is the Domain attribute for the session cookie (empty = host-only; set when wildcard subdomain routing is enabled so the session survives the IdP→callback→subdomain redirect chain).

func (*SSOHandler) Callback

func (h *SSOHandler) Callback(c *gin.Context)

Callback handles GET /api/v1/auth/sso/:orgSlug/callback (public). Exchanges the code, sets the session JWT cookie, and redirects to the frontend.

func (*SSOHandler) Delete

func (h *SSOHandler) Delete(c *gin.Context)

Delete handles DELETE /api/v1/orgs/:id/sso (org admin).

func (*SSOHandler) Domains

func (h *SSOHandler) Domains(c *gin.Context)

Domains handles GET /api/v1/auth/sso/domains (public). Returns every claimed SSO domain for login-page discovery.

func (*SSOHandler) Get

func (h *SSOHandler) Get(c *gin.Context)

Get handles GET /api/v1/orgs/:id/sso (org admin).

func (*SSOHandler) OIDCEnabled

func (h *SSOHandler) OIDCEnabled(ctx context.Context) bool

OIDCEnabled reports whether any org has configured SSO, for the /auth/config feature flag. A DB error is treated as "disabled" so a transient failure does not advertise SSO when there is none to complete.

func (*SSOHandler) Put

func (h *SSOHandler) Put(c *gin.Context)

Put handles PUT /api/v1/orgs/:id/sso (org admin). Upserts the SSO config.

func (*SSOHandler) RotateToken

func (h *SSOHandler) RotateToken(c *gin.Context)

RotateToken handles POST /api/v1/orgs/:id/sso/verification-token/rotate (org admin). Replaces the DNS verification token with a fresh random value. Used for both initial creation (when no token exists) and rotation.

func (*SSOHandler) Start

func (h *SSOHandler) Start(c *gin.Context)

Start handles GET /api/v1/auth/sso/:orgSlug/start (public). Redirects the browser to the IdP authorization endpoint and sets the signed PKCE/state cookie.

func (*SSOHandler) VerifyDomain

func (h *SSOHandler) VerifyDomain(c *gin.Context)

VerifyDomain handles POST /api/v1/orgs/:id/sso/domains/:domain/verify (org admin). On-demand DNS verification: checks the TXT record at _llmsafespaces-verify.<domain> for the org's verification token and promotes the domain to verified on match.

type SecretsHandler

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

SecretsHandler handles HTTP requests for the secrets API.

func NewSecretsHandler

func NewSecretsHandler(svc *secrets.SecretService) *SecretsHandler

NewSecretsHandler creates a new SecretsHandler.

func (*SecretsHandler) CreateSecret

func (h *SecretsHandler) CreateSecret(c *gin.Context)

CreateSecret handles POST /api/v1/secrets

func (*SecretsHandler) DeleteSecret

func (h *SecretsHandler) DeleteSecret(c *gin.Context)

DeleteSecret handles DELETE /api/v1/secrets/:id

func (*SecretsHandler) GetAuditLog

func (h *SecretsHandler) GetAuditLog(c *gin.Context)

GetAuditLog handles GET /api/v1/secrets/audit

func (*SecretsHandler) GetBindings

func (h *SecretsHandler) GetBindings(c *gin.Context)

GetBindings handles GET /api/v1/workspaces/:id/bindings

func (*SecretsHandler) GetSecret

func (h *SecretsHandler) GetSecret(c *gin.Context)

GetSecret handles GET /api/v1/secrets/:id

func (*SecretsHandler) GetSecretBindings

func (h *SecretsHandler) GetSecretBindings(c *gin.Context)

GetSecretBindings handles GET /api/v1/secrets/:id/bindings

func (*SecretsHandler) HasPodIPResolver

func (h *SecretsHandler) HasPodIPResolver() bool

HasPodIPResolver reports whether a PodIPResolver has been configured. Used by wiring tests to verify the handler is fully constructed; without a resolver the reload-secrets endpoint and the SetBindings auto-push silently no-op (Bug 1 + Bug 2 in worklog 0085).

func (*SecretsHandler) ListSecrets

func (h *SecretsHandler) ListSecrets(c *gin.Context)

ListSecrets handles GET /api/v1/secrets

func (*SecretsHandler) ReloadSecrets

func (h *SecretsHandler) ReloadSecrets(c *gin.Context)

ReloadSecrets handles POST /api/v1/workspaces/:id/reload-secrets Decrypts bound secrets and pushes them to the running pod's agentd.

Two failure classes get different HTTP status codes:

  • InjectSecrets failures (bad workspaceID, DEK unavailable, wrapped ciphertext corrupted) are mapped by handleSecretError to 400/403/500.
  • Push transport / agentd failures map to 503/409/502.

Both flow through the same shared agentpush.Service (constructed once by the wiring layer, reused across every request) so on-wire behavior matches SetBindings and the workspace-service auto-push exactly. agentpush wraps InjectSecrets errors with "inject secrets: %w", so we can unwrap to recover the typed error for handleSecretError.

func (*SecretsHandler) RevealSecret

func (h *SecretsHandler) RevealSecret(c *gin.Context)

RevealSecret handles POST /api/v1/secrets/:id/reveal Requires password reconfirmation: a stolen JWT alone must not be sufficient to extract every secret. Without a configured PasswordVerifier the handler returns 503 — shipping without verification is exactly the security theater the validator audit flagged. The bcrypt.CompareHashAndPassword call inside the verifier is constant-time, so failed-password timing does not differentiate from missing-DEK timing in practice.

func (*SecretsHandler) SetAgentPusher

func (h *SecretsHandler) SetAgentPusher(p *agentpush.Service)

SetAgentPusher installs a pre-built agentpush.Service. Preferred over SetPodIPResolver + SetModelCache + SetLogger for new call sites, and used by app.New to share a single pusher instance across the handler and workspace.Service (the pod-recreation auto-push consumer).

func (*SecretsHandler) SetBindings

func (h *SecretsHandler) SetBindings(c *gin.Context)

func (*SecretsHandler) SetCredentialStateWriter

func (h *SecretsHandler) SetCredentialStateWriter(w CredentialStateWriter)

SetCredentialStateWriter installs the writer. If nil, MarkCredentialChanged is silently skipped (banner won't appear but no crash).

func (*SecretsHandler) SetLogger

SetLogger installs the logger used to surface non-fatal failures from the bind-time auto-push. Optional; if nil, failures are silent (which is exactly Bug 2 in worklog 0085 — do not leave nil in production).

func (*SecretsHandler) SetModelCache

func (h *SecretsHandler) SetModelCache(c ModelCache)

SetModelCache injects the shared model cache so SecretsHandler can evict a workspace's cache entry after credential binds (M2-a: replaces the former package-level global defaultModelCache).

func (*SecretsHandler) SetPasswordVerifier

func (h *SecretsHandler) SetPasswordVerifier(v PasswordVerifier)

SetPasswordVerifier installs the verifier used to confirm the caller's password on RevealSecret. If left nil the reveal handler rejects every request with 503; this is intentional because shipping without password verification is exactly the security theater we fixed (validator finding on RevealSecret in worklog 0094 audit).

func (*SecretsHandler) SetPodIPResolver

func (h *SecretsHandler) SetPodIPResolver(r PodIPResolver)

SetPodIPResolver sets the resolver for looking up pod IPs.

func (*SecretsHandler) UpdateSecret

func (h *SecretsHandler) UpdateSecret(c *gin.Context)

UpdateSecret handles PUT /api/v1/secrets/:id

type SettingsHandler

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

SettingsHandler handles admin and user settings API requests.

func NewSettingsHandler

func NewSettingsHandler(instanceSvc *settings.InstanceService, userSvc *settings.UserService) *SettingsHandler

NewSettingsHandler creates a new settings handler.

func (*SettingsHandler) GetAdminSettings

func (h *SettingsHandler) GetAdminSettings(c *gin.Context)

GetAdminSettings returns all instance settings merged with defaults.

func (*SettingsHandler) GetAdminSettingsSchema

func (h *SettingsHandler) GetAdminSettingsSchema(c *gin.Context)

GetAdminSettingsSchema returns the full Tier 2 schema definition.

func (*SettingsHandler) GetUserSettings

func (h *SettingsHandler) GetUserSettings(c *gin.Context)

GetUserSettings returns all user settings merged with defaults.

func (*SettingsHandler) GetUserSettingsSchema

func (h *SettingsHandler) GetUserSettingsSchema(c *gin.Context)

GetUserSettingsSchema returns the full Tier 3 schema definition.

func (*SettingsHandler) SetAdminSetting

func (h *SettingsHandler) SetAdminSetting(c *gin.Context)

SetAdminSetting updates a single instance setting.

func (*SettingsHandler) SetUserSetting

func (h *SettingsHandler) SetUserSetting(c *gin.Context)

SetUserSetting updates a single user setting.

type StripeWebhookHandler

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

StripeWebhookHandler receives and processes Stripe webhook deliveries. It verifies the HMAC signature, deduplicates via stripe_events, and dispatches by event type. It has no JWT auth — the signature is the credential.

func NewStripeWebhookHandler

func NewStripeWebhookHandler(provider billing.CheckoutProvider, store stripeEventStore, logger webHookLogger) *StripeWebhookHandler

NewStripeWebhookHandler constructs the handler. provider must be able to verify webhook signatures (i.e. a *billing.StripeProvider, not the noop).

func (*StripeWebhookHandler) HandleWebhook

func (h *StripeWebhookHandler) HandleWebhook(c *gin.Context)

HandleWebhook is POST /api/v1/webhooks/stripe.

type TerminalCache

type TerminalCache interface {
	Get(ctx context.Context, key string) (string, error)
	Set(ctx context.Context, key, value string, exp time.Duration) error
	Delete(ctx context.Context, key string) error
}

TerminalCache is the subset of CacheService needed by the terminal handler.

type TerminalHandler

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

TerminalHandler handles WebSocket terminal connections to workspace pods.

func NewTerminalHandler

func NewTerminalHandler(
	cache TerminalCache,
	wsGetter WorkspaceGetter,
	namespace string,
	logger pkginterfaces.LoggerInterface,
) *TerminalHandler

NewTerminalHandler creates a new terminal handler.

func (*TerminalHandler) HandleTerminal

func (h *TerminalHandler) HandleTerminal(c *gin.Context)

HandleTerminal handles GET /workspaces/:id/terminal?ticket=<ticket>.

func (*TerminalHandler) HandleTicket

func (h *TerminalHandler) HandleTicket(c *gin.Context)

HandleTicket handles POST /workspaces/:id/terminal/ticket.

func (*TerminalHandler) SetExecConfig

func (h *TerminalHandler) SetExecConfig(cfg *rest.Config, cs kubernetes.Interface)

SetExecConfig sets the K8s config for pod exec (call after construction).

type TerminalMessage

type TerminalMessage struct {
	Type    string `json:"type"` // input, resize, output, exit, error
	Data    string `json:"data,omitempty"`
	Cols    uint16 `json:"cols,omitempty"`
	Rows    uint16 `json:"rows,omitempty"`
	Code    int    `json:"code,omitempty"`
	Message string `json:"message,omitempty"`
}

TerminalMessage is the JSON frame for WebSocket communication.

type TicketResponse

type TicketResponse struct {
	Ticket    string    `json:"ticket"`
	ExpiresAt time.Time `json:"expiresAt"`
}

TicketResponse is returned by POST /terminal/ticket.

type TokenReviewer

type TokenReviewer interface {
	Review(ctx context.Context, token string) (string, error)
}

TokenReviewer validates a projected ServiceAccount token via K8s TokenReview. Returns the authenticated username (e.g. "system:serviceaccount:<ns>:workspace-<id>") on success.

type UnlockDEKHandler

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

UnlockDEKHandler handles POST /api/v1/auth/unlock-dek (Epic 56).

The "soft" in soft-unlock means: no JWT invalidation, no full re-login. The user re-enters their password to repopulate the per-session DEK when the durable rehydrate path fails for one of the residual cases the design enumerates (pre-feature backfill, US-50.4 DEK rotation, row corruption). On success the durable jwt_sessions row is rewritten under the user's MATCHED signing key — not the active key — so that a subsequent rehydrate after Valkey restart still works under the same JWT.

func NewUnlockDEKHandler

func NewUnlockDEKHandler(keys DEKUnlocker) *UnlockDEKHandler

NewUnlockDEKHandler creates a handler with the given key-service.

func (*UnlockDEKHandler) Unlock

func (h *UnlockDEKHandler) Unlock(c *gin.Context)

Unlock handles POST /auth/unlock-dek.

type UsageHandler

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

func NewUsageHandler

func NewUsageHandler(meteringSvc interfaces.MeteringService, dbSvc interfaces.DatabaseService) *UsageHandler

func (*UsageHandler) AdminBillingStatus

func (h *UsageHandler) AdminBillingStatus(c *gin.Context)

func (*UsageHandler) AdminDiscardDLQ

func (h *UsageHandler) AdminDiscardDLQ(c *gin.Context)

func (*UsageHandler) AdminGetDLQ

func (h *UsageHandler) AdminGetDLQ(c *gin.Context)

func (*UsageHandler) AdminGetUsage

func (h *UsageHandler) AdminGetUsage(c *gin.Context)

func (*UsageHandler) AdminRetryDLQ

func (h *UsageHandler) AdminRetryDLQ(c *gin.Context)

func (*UsageHandler) GetQuotaStatus

func (h *UsageHandler) GetQuotaStatus(c *gin.Context)

func (*UsageHandler) GetUsage

func (h *UsageHandler) GetUsage(c *gin.Context)

func (*UsageHandler) GetWorkspaceUsage

func (h *UsageHandler) GetWorkspaceUsage(c *gin.Context)

func (*UsageHandler) SetDB

func (h *UsageHandler) SetDB(db *sql.DB)

type UserProviderCredentialsHandler

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

UserProviderCredentialsHandler handles user-scoped provider credential CRUD.

func NewUserProviderCredentialsHandler

func NewUserProviderCredentialsHandler(store CredentialStore, bindings CredentialBindingStore, keys *secrets.KeyService, keyStore secrets.KeyStore) *UserProviderCredentialsHandler

NewUserProviderCredentialsHandler creates a new handler. The CRUD store provides owner-scoped credential rows; the binding store handles the user-only credential↔workspace operations.

func (*UserProviderCredentialsHandler) Bind

Bind handles POST /api/v1/provider-credentials/:id/bind/:workspaceId.

func (*UserProviderCredentialsHandler) Create

Create handles POST /api/v1/provider-credentials.

NOTE — DEK rotation (L-2 known limitation): User credentials are encrypted with the user's DEK at creation time. If the user later rotates their password (re-wrapping the DEK), existing provider credentials in provider_credentials are NOT re-encrypted, because the server cannot access the old DEK without an active session holding it. Credentials whose key_version is stale will fail to decrypt after a DEK rotation. A future improvement should re-encrypt provider_credentials as part of the password-rotation flow.

func (*UserProviderCredentialsHandler) Delete

Delete handles DELETE /api/v1/provider-credentials/:id. Notifies all workspaces that had this credential bound so running pods pick up the revocation on their next secret reload (C-3 fix).

func (*UserProviderCredentialsHandler) Get

Get handles GET /api/v1/provider-credentials/:id.

func (*UserProviderCredentialsHandler) List

List handles GET /api/v1/provider-credentials.

func (*UserProviderCredentialsHandler) ListBindings

func (h *UserProviderCredentialsHandler) ListBindings(c *gin.Context)

ListBindings handles GET /api/v1/provider-credentials/:id/bindings. Returns workspace IDs with their binding source type (explicit vs auto) so the UI can show which workspaces have user-initiated vs seeded bindings (M-1 fix).

func (*UserProviderCredentialsHandler) ProbeModels

func (h *UserProviderCredentialsHandler) ProbeModels(c *gin.Context)

ProbeModels handles GET /api/v1/provider-credentials/:id/models. User variant — uses the session DEK to decrypt.

func (*UserProviderCredentialsHandler) SetCredentialStateWriter

func (h *UserProviderCredentialsHandler) SetCredentialStateWriter(w CredentialStateWriter)

SetCredentialStateWriter installs the reload banner trigger.

func (*UserProviderCredentialsHandler) SetWorkspaceOwnerChecker

func (h *UserProviderCredentialsHandler) SetWorkspaceOwnerChecker(fn WorkspaceOwnerChecker)

SetWorkspaceOwnerChecker installs the ownership verification function.

func (*UserProviderCredentialsHandler) Unbind

Unbind handles DELETE /api/v1/provider-credentials/:id/bind/:workspaceId. Returns 409 Conflict if the binding is auto-managed (H-1 fix: auto-bindings protected).

type WorkspaceEnvHandler

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

WorkspaceEnvHandler handles PUT/GET/DELETE /api/v1/workspaces/:id/env. Extracted from SecretsHandler (US-29.4) — env-var management is a distinct responsibility from secret CRUD, binding management, and model selection. Each env var is stored as an env-secret-bound-to-workspace; the handler orchestrates create-or-update-by-name + binding management.

func NewWorkspaceEnvHandler

func NewWorkspaceEnvHandler(svc WorkspaceEnvService) *WorkspaceEnvHandler

NewWorkspaceEnvHandler creates a WorkspaceEnvHandler backed by the given service. The service must be non-nil — it is a required dependency (US-29.8 principle: fail at construction, not at request time).

func (*WorkspaceEnvHandler) DeleteWorkspaceEnv

func (h *WorkspaceEnvHandler) DeleteWorkspaceEnv(c *gin.Context)

DeleteWorkspaceEnv handles DELETE /api/v1/workspaces/:id/env/:name

func (*WorkspaceEnvHandler) GetWorkspaceEnv

func (h *WorkspaceEnvHandler) GetWorkspaceEnv(c *gin.Context)

GetWorkspaceEnv handles GET /api/v1/workspaces/:id/env Returns env var names (never values) bound to this workspace.

func (*WorkspaceEnvHandler) SetLogger

SetLogger installs the logger used to surface non-fatal failures from secret create/update/delete operations. Optional; if nil, failures are silent (do not leave nil in production).

func (*WorkspaceEnvHandler) SetWorkspaceEnv

func (h *WorkspaceEnvHandler) SetWorkspaceEnv(c *gin.Context)

SetWorkspaceEnv handles PUT /api/v1/workspaces/:id/env

Creates or updates env-secret type secrets bound to this workspace.

Concurrency: SetWorkspaceEnv only ADDs bindings (it never removes — that's what DeleteWorkspaceEnv is for), so we can use the store's AddBindings primitive which holds a workspace-scoped advisory lock for the duration of the binding write. Two concurrent SetWorkspaceEnv calls on the same workspace serialize at the AddBindings step and neither's secrets are lost.

Error handling: every UpdateSecret/CreateSecret/AddBindings failure surfaces as 500 with the offending var name. Pre-fix the handler returned 204 even when the writes silently failed.

type WorkspaceEnvService

type WorkspaceEnvService interface {
	// GetSecretByName returns the named secret for the user.
	//
	// Contract: a not-found secret is reported as (nil, nil) — NOT
	// (nil, ErrNotFound). SetWorkspaceEnv and DeleteWorkspaceEnv rely on
	// this: they branch on `existing != nil` to decide create-vs-update
	// (and to short-circuit a no-op delete), so any implementation that
	// returned a non-nil error for the absence case would surface as a
	// spurious 500 and break env-var creation. Backed by
	// pg_secret_store.go, which maps pgx.ErrNoRows → (nil, nil).
	GetSecretByName(ctx context.Context, userID, name string) (*secrets.SecretResponse, error)
	UpdateSecret(ctx context.Context, userID, sessionID string, matchedSigningKey []byte, secretID string, req secrets.UpdateSecretRequest) error
	CreateSecret(ctx context.Context, userID, sessionID string, matchedSigningKey []byte, req secrets.CreateSecretRequest) (*secrets.SecretResponse, error)
	AddBindings(ctx context.Context, userID, workspaceID string, secretIDs []string) (secrets.BindingsMutationResult, error)
	GetBindings(ctx context.Context, userID, workspaceID string) (*secrets.BindingsResponse, error)
	DeleteSecret(ctx context.Context, userID, secretID string) error
}

WorkspaceEnvService is the caller-shaped subset of SecretService methods used by the workspace-env endpoints. *secrets.SecretService satisfies it.

type WorkspaceGetter

type WorkspaceGetter interface {
	GetWorkspace(ctx context.Context, id string) (*v1.Workspace, error)
}

WorkspaceGetter resolves workspace CRDs.

type WorkspaceOwnerChecker

type WorkspaceOwnerChecker func(ctx context.Context, userID, workspaceID string) error

WorkspaceOwnerChecker verifies workspace ownership for bind operations.

type WorkspaceServicer

type WorkspaceServicer interface {
	GetWorkspace(ctx context.Context, userID, workspaceID string) (*types.Workspace, error)
}

WorkspaceServicer is the minimal workspace service surface for reload.

Jump to

Keyboard shortcuts

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