api

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: Apache-2.0 Imports: 76 Imported by: 0

Documentation

Overview

Package api implements TASKS.md 1.9: the HTTP API the web frontend (1.10) and, later, the MCP layer both build on. Scope for this pass, per TASKS.md 1.9 literally: GET /api/v1/brand, apps CRUD, deploy trigger and deploy history, single-admin-user session auth. No teams, no RBAC: explicitly out of scope until Phase 4.

Routing uses the standard library's Go 1.22+ pattern-based http.ServeMux ("GET /api/v1/apps/{name}") rather than a router framework like Chi, per the project's rule against pulling in a heavy framework; the one thing a framework would earn its weight on here is auth middleware, and requireAuth below is a plain http.HandlerFunc-wrapping closure that stdlib's mux composes with directly, so there is no ergonomics gap stdlib doesn't already close.

"App" here is layered directly on store.DesiredService (internal/store/service.go), the closest existing resource, rather than a new, speculative domain type. That surfaces real gaps instead of papering over them with a fuller model TASKS.md 1.9 doesn't actually ask this package to build yet:

  • No replicas or strategy fields on an app: internal/spec's app.yaml Service has them, store.DesiredService doesn't yet, so this API can't expose what the store can't hold. Adding them is a store-schema and deploy-pipeline change, not something this package should invent on its own. Domains closed once TASKS.md 1.6 added the column: appResource now carries Domains too.
  • Deploy trigger (deploys.go) only updates desired_services.image; it doesn't build anything. TASKS.md 1.4's internal/build and internal/deploy already exist and do that, but they're owned by a different concurrent session as this package was written, so this endpoint takes an already-built image tag as input, the same mechanism TASKS.md 1.3 documents for rollback, run forward instead of backward. POST /api/v1/apps/{name}/builds (builds.go, handleTriggerBuild) closes this gap: it invokes the same internal/deploy.Pipeline the git webhook receiver uses, given a git source (repo_url/ref) in the request body, since no app has a stored git/build config anywhere in this codebase yet (see specServiceFromDesired's own doc comment for what that means for fidelity versus the original app.yaml).
  • Deploy history (deploys.go) returns the latest reconcile condition per (controller, condition type) pair, which is genuinely all internal/store.UpsertConditions persists today, not a row-per- deploy-attempt log. A real deploy history table is a store-schema addition this package deliberately didn't invent speculatively.

cmd/levelrail/main.go's reconcile engine closed the gap noted above in an earlier draft of this comment: it now derives a dynamic controller set from the store every pass (reconcile.Engine.Source), so a deploy triggered through this API does reconcile on the next pass.

Secrets (TASKS.md 1.7): PUT /api/v1/apps/{name}/secrets/{key} sets a value, encrypted at rest via internal/secrets.Manager. Deliberately set-only, no GET: this package never decrypts a value for a response body, only internal/reconcile/application does, immediately before container creation. Available only when the control plane was started with a master key (WithSecretSetter); without one, the route returns 501.

Auth foundation ("Dashboard & auth", TASKS.md): POST /api/v1/auth/register is the interactive first-run counterpart to BootstrapAdmin's env-var path, gated on "no admin row exists yet" at both the route and the mutation layer. Session auth stays exactly what TASKS.md 1.9 scoped it as (single admin user, no teams, no RBAC); API tokens (POST/GET /api/v1/auth/tokens, DELETE .../{id}) are a separate, additive credential type for non-interactive callers (a future CLI, an MCP server), scoped to abilities (abilities.go: read, read:sensitive, write, deploy, root) checked fresh on every call by requireAbility, never a cached decision. Token management itself is session-only via requireAuth: a token can never mint or revoke another token on its own behalf. See docs-local/research/ theauth-go-fit-assessment.md and competitor-onboarding-auth-ux.md for why this shape (not theauth-go, not an all-or-nothing key) was chosen.

Index

Constants

View Source
const (
	AbilityRead           = "read"
	AbilityReadSensitive  = "read:sensitive"
	AbilityWrite          = "write"
	AbilityWriteSensitive = "write:sensitive"
	AbilityDeploy         = "deploy"
	AbilityRoot           = "root"
)

Ability strings an api_tokens row can be scoped to (TASKS.md "Backend auth foundation"), adopted from Coolify's own model per docs-local/research/competitor-onboarding-auth-ux.md finding 9: a small, legible permission surface an MCP-issued token can be provably scoped to at the token layer itself, not just by convention in what a caller chooses to call. AbilityRoot is exclusive of the rest (checked at mint time, see validateAbilities), not additive with them.

View Source
const (
	ClientKindCLI       = "cli"
	ClientKindDashboard = "dashboard"
	ClientKindMCP       = "mcp"
	ClientKindAPI       = "api"
)

Client kind values audit_log.client_kind (migrations/0077) is normalized into: which surface actually made the request, not just which auth method it used (store.AuditEntry.ActorType already covers session-vs-token).

View Source
const (
	RoleAdmin    = "admin"
	RoleOperator = "operator"
	RoleViewer   = "viewer"
)

Curated role names, roles's own Name values.

Variables

This section is empty.

Functions

func BootstrapAdmin

func BootstrapAdmin(ctx context.Context, s AuthStore, username, password string) error

BootstrapAdmin ensures at least one user exists, creating one from username/password if none does yet, with AbilityRoot: it's the only user on the instance, so it has to be. No-op once any user exists, so a restart with the same env vars never resets a changed password. username becomes the new user's email verbatim (no "@" required), matching migrations/0035's backfill leniency for a pre-existing admin_user row.

func MaybeBootstrapDevAdmin

func MaybeBootstrapDevAdmin(ctx context.Context, s AuthStore, logger *slog.Logger) error

MaybeBootstrapDevAdmin is cmd/levelrail/main.go's entry point, called unconditionally on every startup the same way BootstrapAdmin already is: a no-op unless APP_DEV_MODE=1 in a non -tags embedweb build (see devmode_debug.go/devmode_release.go). BootstrapAdmin itself is a no-op once any admin account exists, so this only ever creates the fixed dev/dev account on a genuinely empty data directory; an operator who already set APP_ADMIN_USERNAME/APP_ADMIN_PASSWORD keeps that account regardless of call order between the two.

func MaybeSeedDevFixturesFromFile

func MaybeSeedDevFixturesFromFile(ctx context.Context, s TokenStore, logger *slog.Logger, path string) error

MaybeSeedDevFixturesFromFile is cmd/levelrail/main.go's entry point, called unconditionally on every startup the same way MaybeBootstrapDevAdmin already is. A missing fixtures file is not an error: dev mode works without one (only the fixed dev/dev admin account), a dev-fixtures.yml is an opt-in convenience layered on top.

Types

type AlertRules

type AlertRules interface {
	SaveRule(ctx context.Context, r alerting.Rule) error
	GetRule(ctx context.Context, id string) (*alerting.Rule, error)
	ListRulesForResource(ctx context.Context, resourceID string) ([]alerting.Rule, error)
	DeleteRule(ctx context.Context, id string) error
}

AlertRules is the surface the alert-rule handlers need from internal/alerting.DB (TASKS.md 2.5/2.7). *alerting.DB satisfies this structurally, the same "narrow consumer-defined interface" convention TelemetryQuerier and SecretSetter already establish in this package.

type AppComposeStore

type AppComposeStore interface {
	SaveApp(ctx context.Context, a store.App) error
	GetAppByName(ctx context.Context, name string) (store.App, error)
	SaveDesiredService(ctx context.Context, svc store.DesiredService) error
}

AppComposeStore is the store surface POST /api/v1/apps/{name}/compose needs (apps_compose.go): create-or-reuse the owning store.App, then save each of its member services.

type AppGroupLister

type AppGroupLister interface {
	ListServicesByApp(ctx context.Context, appID string) ([]store.DesiredService, error)
	DeleteApp(ctx context.Context, id string) error
	SaveApp(ctx context.Context, a store.App) error
	GetAppByName(ctx context.Context, name string) (store.App, error)
}

AppGroupLister is the store surface GET /api/v1/apps/{name}/group (apps_group.go) and the app-linking write path (apps_multi.go's ensureAppLinked, apps.go's deleteAppIfOrphaned) need: stage 1 of multi-service apps (migrations/0039_apps.sql) started this as a read-only interface; stage 2 (per-app Docker networking) added the store.App CRUD every create/delete path needs to keep that App row's lifecycle in sync with its member services, since internal/reconcile/application.NetworkCleanupController diffs exactly the App rows this interface reports against Docker's own observed networks. *store.DB satisfies this structurally.

type AppStore

type AppStore interface {
	SaveDesiredService(ctx context.Context, svc store.DesiredService) error
	GetDesiredService(ctx context.Context, name string) (*store.DesiredService, error)
	ListDesiredServices(ctx context.Context) ([]store.DesiredService, error)
	DeleteDesiredService(ctx context.Context, name string) error
	// UpdateServiceNode is TASKS.md 3.3's placement mutation, separate
	// from SaveDesiredService on purpose: see store.DB.SaveDesiredService's
	// own doc comment for why an ordinary app update must never be able
	// to silently move a service between nodes.
	UpdateServiceNode(ctx context.Context, name, nodeID string) error
	// ListDesiredServicesByNode is TASKS.md 3.7's drain and
	// delete-guard primitive (handleDrainNode, handleDeleteNode): find
	// what's placed on a node without listing every service.
	ListDesiredServicesByNode(ctx context.Context, nodeID string) ([]store.DesiredService, error)
	// RestartService is the only way to force a running container to be
	// recreated without an image change: a redeploy of the same image
	// tag is otherwise a genuine reconciler no-op (see
	// internal/reconcile/application.ContainerName's own doc comment).
	// See store.DB.RestartService's own doc comment for why this is
	// deliberately excluded from SaveDesiredService's full-record-replace
	// semantics, the same reasoning UpdateServiceNode already establishes
	// for NodeID.
	RestartService(ctx context.Context, name string) error
	// UpdateServiceProject is UpdateServiceNode's project-kind
	// counterpart (projects.go): same separation-from-ordinary-update
	// reasoning, see store.DB.UpdateServiceProject's own doc comment.
	UpdateServiceProject(ctx context.Context, name, projectID string) error
	// UpdateServiceStorageTarget backs PUT/DELETE
	// /api/v1/apps/{name}/storage (apps_storage.go): which store.BackupTarget
	// (already-established interface, BackupTargetStore below) this app's
	// own object-storage credentials resolve from. Same
	// separation-from-ordinary-update reasoning as UpdateServiceNode/
	// UpdateServiceProject, see store.DB.UpdateServiceStorageTarget's own
	// doc comment.
	UpdateServiceStorageTarget(ctx context.Context, name, storageTargetID string) error
	// UpdateServiceSuspended backs POST /api/v1/apps/{name}/stop and
	// .../start (handleStopApp/handleStartApp): same
	// separation-from-ordinary-update reasoning as UpdateServiceNode/
	// UpdateServiceProject/UpdateServiceStorageTarget, see
	// store.DB.UpdateServiceSuspended's own doc comment.
	UpdateServiceSuspended(ctx context.Context, name string, suspended bool) error
	// UpdateServiceApp is stage 2 of multi-service apps
	// (migrations/0039_apps.sql)'s own service-side setter: which
	// store.App a service belongs to. Same separation-from-ordinary-
	// update reasoning as UpdateServiceNode/UpdateServiceProject/
	// UpdateServiceStorageTarget/UpdateServiceSuspended, see
	// store.DB.UpdateServiceApp's own doc comment. Called from
	// ensureAppLinked (apps_multi.go), the shared create-or-reuse-and-link
	// path both handleCreateApp (an ordinary single-service app) and
	// handleDeploySpec (a multi-service fan-out) go through.
	UpdateServiceApp(ctx context.Context, name, appID string) error
	// UpdateServiceLogDrain backs PUT/DELETE
	// /api/v1/apps/{name}/log-drain (apps_log_drain.go): which external
	// sink (internal/telemetry.DrainForwarder) this app's container logs
	// additionally forward to. Same separation-from-ordinary-update
	// reasoning as UpdateServiceStorageTarget, see
	// store.DB.UpdateServiceLogDrain's own doc comment.
	UpdateServiceLogDrain(ctx context.Context, name string, drain *store.LogDrain) error
	// UpdateServiceDatabaseAttachment backs PUT/DELETE
	// /api/v1/apps/{name}/database (apps_database.go): which managed
	// database (DatabaseStore below) this app resolves one connection env
	// var from. Same separation-from-ordinary-update reasoning as
	// UpdateServiceStorageTarget, see store.DB.UpdateServiceDatabaseAttachment's
	// own doc comment.
	UpdateServiceDatabaseAttachment(ctx context.Context, name string, att *store.DatabaseAttachment) error
}

AppStore is the store surface the apps and deploys handlers need. *store.DB satisfies this structurally; tests use a real temp-file store the same way internal/store's own tests do, not a mock.

type AuditStore

type AuditStore interface {
	SaveAuditEntry(ctx context.Context, e store.AuditEntry) error
	ListAuditEntries(ctx context.Context, limit int, before *time.Time, filter store.AuditEntryFilter) ([]store.AuditEntry, error)
	DeleteAuditEntriesOlderThan(ctx context.Context, cutoff time.Time) (int64, error)
}

AuditStore is the store surface requireAbility's audit hook (internal/api/auth.go), GET /api/v1/audit-log (audit.go), and the retention sweep/manual purge (audit_retention.go) need: insert-only recording, newest-first cursor-paginated reading, and age-based deletion. Always set, the same "core Store interface, not an optional plug-in" shape certs/staticSites/backupTargets already have.

type AuthStore

type AuthStore interface {
	GetUserByEmail(ctx context.Context, email string) (*store.User, error)
	GetUserByID(ctx context.Context, id string) (*store.User, error)
	CreateUser(ctx context.Context, u store.User) error
	UpdateUserPasswordHash(ctx context.Context, id string, hash *string) error
	UpdateUserLastLogin(ctx context.Context, id string, when time.Time) error
	CountUsers(ctx context.Context) (int, error)
	ListUsers(ctx context.Context) ([]store.User, error)
	DeleteUser(ctx context.Context, id string) error
	UpdateUserAbilities(ctx context.Context, id string, abilities []string) error
	EnableUserTOTP(ctx context.Context, id string, confirmedAt time.Time) error
	DisableUserTOTP(ctx context.Context, id string) error
}

AuthStore is the store surface the auth handlers need.

type BackupDownloader

type BackupDownloader interface {
	Download(ctx context.Context, historyID string) (io.ReadCloser, error)
}

BackupDownloader is the surface the backup download handler needs from internal/backup.DownloadRunner: resolve one already-succeeded backup attempt's live object into a stream, the read-only counterpart of BackupRunner and RestoreRunner. *backup.DownloadRunner satisfies this structurally; internal/api never imports internal/backup directly, the same boundary BackupRunner's own doc comment describes.

type BackupHistoryStore

type BackupHistoryStore interface {
	ListBackupHistory(ctx context.Context, databaseName string, limit int, before *time.Time) ([]store.BackupHistory, error)
	GetBackupHistory(ctx context.Context, id string) (store.BackupHistory, error)
}

BackupHistoryStore is the store surface the backup history handler needs. GetBackupHistory was added for handleTriggerRestore (restore.go): resolving the one backup attempt a restore names, not just listing every attempt for a database, the same "look up one row by ID" need GetBackupTarget already serves for backup targets.

type BackupRunner

type BackupRunner interface {
	RunBackup(ctx context.Context, historyID, databaseName, engine, containerName, targetID string) error
}

BackupRunner is the surface the backup trigger handler needs from internal/backup.Runner: run one backup attempt end to end, from an already-minted history ID through to a finished store.BackupHistory row. *backup.Runner satisfies this structurally; internal/api never imports internal/backup directly (the same reconciler-internals boundary databaseContainerName's own doc comment describes), only this narrow interface, wired in by cmd/levelrail at startup.

type BackupSecretsSetter

type BackupSecretsSetter interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
}

BackupSecretsSetter is the surface backup target creation needs from internal/secrets.Manager: store a credential value, the same "set a value, never read one back through this interface" shape SecretSetter already establishes for app secrets. A distinct type from SecretSetter even though both are structurally satisfied by *secrets.Manager, because a backup target's credentials do get resolved back out server-side, just not through this interface or this package: that's internal/backup.Runner's own, separate, narrower internal/backup.SecretsResolver, wired directly in cmd/levelrail, not through the Router at all.

type BackupTargetStore

type BackupTargetStore interface {
	SaveBackupTarget(ctx context.Context, t store.BackupTarget) error
	GetBackupTarget(ctx context.Context, id string) (store.BackupTarget, error)
	ListBackupTargets(ctx context.Context) ([]store.BackupTarget, error)
	UpdateBackupTarget(ctx context.Context, id, name, provider, endpoint, region, bucket string) error
	DeleteBackupTarget(ctx context.Context, id string) error
}

BackupTargetStore is the store surface the backup target handlers need.

type BackupTargetTester

type BackupTargetTester interface {
	TestTarget(ctx context.Context, targetID string) error
}

BackupTargetTester is the surface the test-connection handler needs from internal/backup.TargetTester: probe one target's configured bucket over its stored credentials, without uploading or deleting anything. *backup.TargetTester satisfies this structurally; internal/api never resolves a backup target's credentials directly, the same boundary BackupSecretsSetter's own doc comment describes.

type BackupVerificationStore

type BackupVerificationStore interface {
	ListBackupVerifications(ctx context.Context, backupHistoryID string, limit int) ([]store.BackupVerification, error)
}

BackupVerificationStore is the store surface the backup verification handlers need: listing past verification attempts for one backup, the same "core Store interface, no runner configuration needed" shape BackupHistoryStore's own doc comment establishes for backup history itself.

type BackupVerifier

type BackupVerifier interface {
	VerifyBackup(ctx context.Context, verificationID, backupHistoryID, engine, checkedBy string) error
}

BackupVerifier is the surface the verify-trigger handler needs from internal/backup.VerifyRunner: run one verification attempt end to end, from an already-minted verification ID through to a finished store.BackupVerification row. *backup.VerifyRunner satisfies this structurally; internal/api never imports internal/backup directly, the same boundary BackupRunner's own doc comment describes.

type BitbucketAppClient

type BitbucketAppClient interface {
	ExchangeCode(ctx context.Context, key, secret, code string) (bitbucketapp.Tokens, error)
	RefreshToken(ctx context.Context, key, secret, refreshToken string) (bitbucketapp.Tokens, error)
	ListRepos(ctx context.Context, accessToken string) ([]bitbucketapp.Repo, error)
	GetRepo(ctx context.Context, accessToken, fullName string) (bitbucketapp.Repo, error)
	ListBranches(ctx context.Context, accessToken, fullName string) ([]bitbucketapp.Branch, error)
	CreateRepoWebhook(ctx context.Context, accessToken, fullName, hookURL, secret string) error
}

BitbucketAppClient is the surface internal/api needs from internal/bitbucketapp.Client: OAuth code exchange/refresh, and repo listing/lookup/webhook registration once connected. *bitbucketapp.Client satisfies this structurally; tests substitute a hand-written fake.

type BitbucketAppSecrets

type BitbucketAppSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Resolve(ctx context.Context, serviceName, envKey string) (string, error)
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
	DeleteAll(ctx context.Context, serviceName string) error
}

BitbucketAppSecrets is the surface the Bitbucket App handlers need from internal/secrets.Manager, the same "writes and reads back through internal/secrets" shape GitLabAppSecrets's own doc comment describes: the consumer's own secret is set once at connect time, access_token/refresh_token/token_expires_at are written and re-read on the same schedule by bitbucketAccessToken.

type BitbucketAppStore

type BitbucketAppStore interface {
	GetBitbucketAppConnection(ctx context.Context) (store.BitbucketAppConnection, error)
	SaveBitbucketAppConnection(ctx context.Context, c store.BitbucketAppConnection) error
	DeleteBitbucketAppConnection(ctx context.Context) error
}

BitbucketAppStore is the store surface the Bitbucket App connection handlers need. *store.DB satisfies this structurally, the same "core store" shape GitHubAppStore/GitLabAppStore's own doc comments establish.

type Builder

type Builder interface {
	Deploy(ctx context.Context, req deploy.Request, progress func(build.ProgressEvent)) (string, error)
	// DeploySpec is handleDeploySpec's (apps_multi.go) own narrow need:
	// the multi-service fan-out entry point. Same *deploy.Pipeline
	// satisfies both methods, so this stays one interface rather than a
	// second router field, the same "one concrete type, one interface"
	// shape every other narrow store/builder interface in this codebase
	// already follows.
	DeploySpec(ctx context.Context, req deploy.MultiRequest, progress func(serviceKey string, ev build.ProgressEvent)) ([]deploy.ServiceOutcome, error)
}

Builder is the surface the manual build trigger handler (handleTriggerBuild) needs from internal/deploy.Pipeline, the exact same narrow-interface shape internal/webhook.Deployer already establishes for the identical method, so both call sites can be satisfied by the one real *deploy.Pipeline cmd/levelrail/main.go builds. *deploy.Pipeline satisfies this structurally.

type CertStore

type CertStore interface {
	ListCertStorageKeys(ctx context.Context, prefix string, recursive bool) ([]string, error)
	GetCertStorageValue(ctx context.Context, key string) (*store.CertStorageValue, error)
}

CertStore is the store surface handleListCertificates needs: the same two read methods internal/ingress.SQLiteStorage already calls through its own CertStore interface, narrowed further since this handler never writes, deletes, or locks. *store.DB satisfies this structurally, the same consumer-defined-interface convention every other Store sub-interface in this package follows. Identical in shape to alerting.CertSource (a kind=cert_expiry rule's own read surface): the certificate-listing computation itself lives once, in alerting.ListCertificates, and this handler just maps its result to the wire shape below, so the dashboard's TLS card and an alert rule can never silently disagree about a certificate's status.

type CloneRestoreHistoryStore

type CloneRestoreHistoryStore interface {
	ListCloneRestores(ctx context.Context, sourceDatabaseName string) ([]store.CloneRestore, error)
}

CloneRestoreHistoryStore is the store surface the clone-restore history handler needs, the "restore as new database" counterpart to RestoreHistoryStore (restore.go).

type CloneRestoreRunner

type CloneRestoreRunner interface {
	RunCloneRestore(ctx context.Context, historyID, sourceDatabaseName, newDatabaseName, backupHistoryID, engine, containerName, controllerName string) error
}

CloneRestoreRunner is the surface the clone-restore trigger handler needs from internal/backup.CloneRestoreRunner: wait for a newly created database to come up and restore a backup into it, from an already-minted history ID through to a finished store.CloneRestore row. *backup.CloneRestoreRunner satisfies this structurally; internal/api never imports internal/backup directly, the same boundary RestoreRunner's own doc comment describes.

type CloudflareDNSSecrets

type CloudflareDNSSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
	DeleteAll(ctx context.Context, serviceName string) error
}

CloudflareDNSSecrets is the surface the Cloudflare DNS-01 settings handlers need from internal/secrets.Manager, the same shape CloudflareTunnelSecrets already establishes for a distinct credential (a scoped Cloudflare API token, not the cloudflared connector token).

type CloudflareDNSStore

type CloudflareDNSStore interface {
	GetCloudflareDNSSettings(ctx context.Context) (store.CloudflareDNSSettings, error)
	UpdateCloudflareDNSSettings(ctx context.Context, s store.CloudflareDNSSettings) error
}

CloudflareDNSStore is the store surface GET/PUT /api/v1/settings/cloudflare-dns need, the same "single platform-wide row" shape CloudflareTunnelStore already establishes.

type CloudflareTunnelSecrets

type CloudflareTunnelSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
	DeleteAll(ctx context.Context, serviceName string) error
}

CloudflareTunnelSecrets is the surface the Cloudflare Tunnel settings handlers need from internal/secrets.Manager: set the token (PUT), check whether one exists (GET's has_token, and PUT's "was a token already set" check for the omitted-token case), and clear it (DELETE). *secrets.Manager satisfies this structurally, the same "narrow, consumer-defined interface" shape GitHubAppSecrets already establishes.

type CloudflareTunnelStore

type CloudflareTunnelStore interface {
	GetCloudflareTunnelSettings(ctx context.Context) (store.CloudflareTunnelSettings, error)
	UpdateCloudflareTunnelSettings(ctx context.Context, s store.CloudflareTunnelSettings) error
}

CloudflareTunnelStore is the store surface GET/PUT/DELETE /api/v1/settings/cloudflare-tunnel need: the single platform-wide row, always present, the same shape EmailSettingsStore has for its own row.

type ComposeSecretStore

type ComposeSecretStore interface {
	Resolve(ctx context.Context, serviceName, envKey string) (string, error)
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
}

ComposeSecretStore is the surface POST /api/v1/apps/{name}/compose needs to resolve a compose file's SERVICE_ magic vars (compose.ResolveMagicVars): a canonical generate-once-per-app value (Resolve/SetValue keyed by the app's own name, a synthetic bucket distinct from any real service), plus one SetValue per real service that references it, since internal/secrets.Manager itself is keyed per real service, not per app.

type ContainerLister

type ContainerLister interface {
	ListByPrefix(ctx context.Context, prefix string) ([]docker.ContainerState, error)
}

ContainerLister is the surface GET /api/v1/system/containers needs: every container on this node, Levelrail-managed or not, the same "structurally satisfied by *docker.Client, no second divergent surface" reasoning ImageLister above already documents. An empty prefix argument matches every container name (docker.Client's own ListByPrefix implementation), which is exactly this route's point: unlike every other Runtime-backed feature in this codebase, which scopes to one app/database's own containers, this one deliberately wants everything.

type DBPinger

type DBPinger interface {
	PingContext(ctx context.Context) error
}

DBPinger is the surface GET /api/v1/system/doctor needs to report the control plane's own SQLite reachability, the same narrow, single-purpose shape DockerPinger above already establishes. *store.DB satisfies this structurally via its embedded *sql.DB's PingContext method.

type DatabaseStore

type DatabaseStore interface {
	SaveDesiredDatabase(ctx context.Context, d store.DesiredDatabase) error
	GetDesiredDatabase(ctx context.Context, name string) (*store.DesiredDatabase, error)
	ListDesiredDatabases(ctx context.Context) ([]store.DesiredDatabase, error)
	DeleteDesiredDatabase(ctx context.Context, name string) error
	// UpdateDatabaseNode is AppStore.UpdateServiceNode's counterpart,
	// same separation-from-ordinary-update reasoning. Originally added
	// unexposed by 3.3; handleDrainNode is the first caller with an
	// actual route to reuse it through.
	UpdateDatabaseNode(ctx context.Context, name, nodeID string) error
	// ListDesiredDatabasesByNode is the database-kind counterpart to
	// AppStore.ListDesiredServicesByNode.
	ListDesiredDatabasesByNode(ctx context.Context, nodeID string) ([]store.DesiredDatabase, error)
	// UpdateDatabaseProject is AppStore.UpdateServiceProject's
	// counterpart (projects.go).
	UpdateDatabaseProject(ctx context.Context, name, projectID string) error
	// SetDatabaseBackupSchedule backs
	// PUT/DELETE /api/v1/databases/{name}/backup-schedule (backups.go):
	// wave-2 roadmap item 6, scheduled backups. Same "own endpoint, own
	// store method" separation UpdateDatabaseNode/UpdateDatabaseProject
	// already establish for their own single-purpose updates.
	SetDatabaseBackupSchedule(ctx context.Context, name, targetID, schedule string, retain, retainDays int) error
	// SetDatabasePublicAccess backs
	// PUT/DELETE /api/v1/databases/{name}/public-access
	// (database_public_access.go): the same "own endpoint, own store
	// method" separation SetDatabaseBackupSchedule already establishes,
	// applied to whether this database's container port is bound to a
	// host port. Returns the port actually assigned (0 when disabling).
	SetDatabasePublicAccess(ctx context.Context, name string, enabled bool, requestedPort int) (int, error)
}

DatabaseStore is the store surface the databases handlers need, the database-kind counterpart to AppStore.

type DeployAttemptStore

type DeployAttemptStore interface {
	SaveDeployAttempt(ctx context.Context, a store.DeployAttempt) error
	FinishDeployAttempt(ctx context.Context, id, status string, finishedAt time.Time, errMsg string) error
	GetDeployAttempt(ctx context.Context, id string) (*store.DeployAttempt, error)
	ListDeployAttempts(ctx context.Context, serviceName string) ([]store.DeployAttempt, error)
}

DeployAttemptStore is the store surface real deploy-attempt history needs: row-per-attempt CRUD, layered alongside DeployStore above rather than replacing it. See handleDeployHistory's own doc comment for why GET /api/v1/apps/{name}/deploys keeps returning reconcile conditions unchanged (an existing, real frontend consumer, web/src/queries/deploys.ts's useDeployStatus, already depends on that shape) while this interface backs a new, additional endpoint, GET /api/v1/apps/{name}/deploy-attempts, instead of overloading the old one. *store.DB satisfies this structurally.

type DeployLogQuerier

type DeployLogQuerier interface {
	QueryDeployLog(ctx context.Context, attemptID string) ([]telemetry.DeployLogEntry, error)
}

DeployLogQuerier is the telemetry-side read surface handleDeployLogStream needs to serve a full replay once an attempt has already finished (see that handler's own doc comment for the in-progress-vs-finished split). *telemetry.DB satisfies this structurally. Optional (nil is valid, the same "not configured" shape WithTelemetryQuerier's own absence already has): without one, a finished attempt's log route returns 501; an in-progress attempt's live tail (backed by deployRecorder below, a separate optional field) is unaffected by this one being unset.

type DeployNotifier

type DeployNotifier interface {
	Dispatch(ctx context.Context, resourceID string, ev alerting.DeployOutcome)
}

DeployNotifier is the narrow surface recordPlainDeployAttempt (deploys.go) and beginBuildDeployAttempt (deploy_attempts.go) need to fire a deploy-outcome notification once a deploy attempt reaches a terminal state. *alerting.DeployDispatcher satisfies this structurally. resourceID is resourceIDForApp(ev.AppName), computed by the caller (see alerting.DeployDispatcher.Dispatch's own doc comment for why it takes resourceID as a parameter rather than importing resourceIDForApp itself).

type DeployNotifyTargets

type DeployNotifyTargets interface {
	SaveDeployTarget(ctx context.Context, t alerting.DeployTarget) error
	GetDeployTarget(ctx context.Context, id string) (*alerting.DeployTarget, error)
	ListDeployTargetsForResource(ctx context.Context, resourceID string) ([]alerting.DeployTarget, error)
	DeleteDeployTarget(ctx context.Context, id string) error
}

DeployNotifyTargets is the surface the deploy-notify-target handlers need from internal/alerting.DB. *alerting.DB satisfies this structurally, the same convention AlertRules already establishes.

type DeployStore

type DeployStore interface {
	GetConditions(ctx context.Context, controllerName string) ([]reconcile.Condition, error)
	// GetConditionsForControllers is handleListApps' batched status
	// source (apps.go): one query for every app's application
	// controller, not a GetConditions call per app.
	GetConditionsForControllers(ctx context.Context, controllerNames []string) (map[string][]reconcile.Condition, error)
}

DeployStore is the store surface the deploy-history handler needs.

type DevFixtureToken

type DevFixtureToken struct {
	Name      string   `yaml:"name"`
	Plaintext string   `yaml:"plaintext"`
	Abilities []string `yaml:"abilities"`
}

DevFixtureToken is one entry in a dev-fixtures.yml file: a fixed plaintext API token, minted with a known ability set, so local testing of the auth flow (does a read-only token get 403'd on a write route, does a write:sensitive token reach the secrets handler, and so on) doesn't require registering, logging in, and minting a real token by hand every time. Only ever seeded under dev mode, see devmode_debug.go/devmode_release.go's devModeEnabled gate.

type DevFixtures

type DevFixtures struct {
	Tokens []DevFixtureToken `yaml:"tokens"`
}

DevFixtures is the top-level shape of dev-fixtures.yml.

func ParseDevFixtures

func ParseDevFixtures(data []byte) (*DevFixtures, error)

ParseDevFixtures validates as much as validateAbilities already validates a real token mint request: an empty or unrecognized ability list is rejected here too, so a typo in dev-fixtures.yml fails loudly at startup instead of silently minting a token nobody can use as intended.

type DeviceAuthStore

type DeviceAuthStore interface {
	SaveDeviceAuthRequest(ctx context.Context, r store.DeviceAuthRequest) error
	GetDeviceAuthRequestByDeviceCode(ctx context.Context, deviceCode string) (*store.DeviceAuthRequest, error)
	GetDeviceAuthRequestByUserCode(ctx context.Context, userCode string) (*store.DeviceAuthRequest, error)
	ListPendingDeviceAuthRequests(ctx context.Context, now time.Time) ([]store.DeviceAuthRequest, error)
	SetDeviceAuthRequestStatus(ctx context.Context, userCode, status, approvedByUserID string) (int64, error)
	RedeemDeviceAuthRequest(ctx context.Context, deviceCode, tokenID string, redeemedAt time.Time) error
}

DeviceAuthStore is the store surface the device-login flow needs, defined here next to the handlers that use it, the same "single-file feature" shape PolicyStore/OnboardingStore already use.

type DockerDiskUsager

type DockerDiskUsager interface {
	DiskUsage(ctx context.Context) (docker.DiskUsage, error)
}

DockerDiskUsager is the surface GET /api/v1/system/status needs for Docker's own storage accounting (images/containers/volumes/build cache), a materially different number from DataDirTotalBytes/ DataDirFreeBytes above: see docker.DiskUsage's own doc comment for why. Narrow and single-purpose like DockerPinger, not docker.Runtime, same "adding to Runtime would ripple into 6+ fakes for one read-only signal" reasoning DockerPinger's own doc comment gives. *docker.Client satisfies this structurally via the DiskUsage method added directly to that concrete type (internal/docker/prune.go).

type DockerPinger

type DockerPinger interface {
	Ping(ctx context.Context) error
}

DockerPinger is the surface GET /api/v1/system/status needs to report Docker daemon connectivity: a liveness check, nothing else. *docker.Client satisfies this structurally via the Ping method added directly to that concrete type (internal/docker/client.go), not through docker.Runtime: Runtime has 6+ fake implementations across internal/reconcile's controllers and internal/agent's test files, plus the real client and the agent-side remote transport, so adding a method there for the sake of this one read-only health signal would ripple into all of them. This narrow interface is the actual consumer-defined boundary instead, the same shape SecretSetter and TelemetryQuerier already establish for their own single-purpose surfaces.

type DockerPruner

type DockerPruner interface {
	Prune(ctx context.Context, keep []string) docker.PruneResult
}

DockerPruner is the surface POST /api/v1/system/prune needs: run every cleanup stage internal/docker/prune.go supports and report what happened. Unlike DockerDiskUsager (a read), this is a destructive-ish action, gated at AbilityRoot the same way handleDrainNode is (see router.go's own route registration), so it gets its own interface rather than being folded into DockerDiskUsager: a read and a destructive action deserve independently swappable seams, the same split Builder/ImageLister already have. *docker.Client satisfies this structurally via the Prune method (internal/docker/prune.go).

type Document

type Document struct {
	Statement []Statement `json:"Statement"`
}

Document is the whole parsed policy body stored as JSON in store.Policy.Document.

func ParseDocument

func ParseDocument(raw string) (*Document, error)

ParseDocument unmarshals and validates a policy document. A document must have at least one statement, every statement must have an Effect of Allow or Deny, at least one Action (each either "*" or a known ability string from validAbilities), and at least one Resource (each either "*" or a non-empty resource identifier).

type DomainBasicAuthSecrets

type DomainBasicAuthSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
	DeleteAll(ctx context.Context, serviceName string) error
}

DomainBasicAuthSecrets is the surface these handlers need from internal/secrets.Manager for a domain's basic-auth password: set it (PUT), check whether one exists (GET's has_password, and PUT's "already set" check for the omitted-password case), and clear it (DELETE). *secrets.Manager satisfies this structurally, the same narrow shape CloudflareTunnelSecrets already establishes.

type DomainBasicAuthStore

type DomainBasicAuthStore interface {
	GetDomainBasicAuth(ctx context.Context, domain string) (store.DomainBasicAuth, bool, error)
	SetDomainBasicAuth(ctx context.Context, domain, username string) error
	DeleteDomainBasicAuth(ctx context.Context, domain string) error
}

DomainBasicAuthStore is the store surface GET/PUT/DELETE .../domains/{domain}/auth need: the username claimed for a domain, always set, same "core Store interface" shape as DomainStore above.

type DomainMaintenanceStore

type DomainMaintenanceStore interface {
	GetDomainMaintenance(ctx context.Context, domain string) (bool, error)
	SetDomainMaintenance(ctx context.Context, domain string) error
	DeleteDomainMaintenance(ctx context.Context, domain string) error
}

DomainMaintenanceStore is the store surface GET/PUT/DELETE .../domains/{domain}/maintenance need: whether a domain currently has maintenance mode enabled, always set, same "core Store interface" shape as DomainBasicAuthStore above.

type DomainStore

type DomainStore interface {
	ListServiceDomains(ctx context.Context) ([]store.ServiceDomain, error)
}

DomainStore is the store surface GET /api/v1/domains needs: every domain currently claimed by a service, for the centralized cross-app domains list (web/src/routes/domains). Reuses internal/store's existing service_domains table (kept in sync by SaveDesiredService on every write, see that method's own doc comment); this is a plain read of already-tracked state, not a new tracking mechanism.

type DomainTLSCertSecrets

type DomainTLSCertSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	DeleteAll(ctx context.Context, serviceName string) error
}

DomainTLSCertSecrets is the surface these handlers need from internal/secrets.Manager for a domain's certificate and private key: set both (PUT) and clear both (DELETE). *secrets.Manager satisfies this structurally, the same narrow shape DomainBasicAuthSecrets already establishes.

type DomainTLSCertStore

type DomainTLSCertStore interface {
	GetDomainTLSCert(ctx context.Context, domain string) (store.DomainTLSCert, bool, error)
	SetDomainTLSCert(ctx context.Context, domain string, uploadedAt, expiresAt time.Time) error
	DeleteDomainTLSCert(ctx context.Context, domain string) error
}

DomainTLSCertStore is the store surface GET/PUT/DELETE .../domains/{domain}/tls-cert need: the upload/expiry metadata for a domain's BYO certificate, same "core Store interface" shape as DomainBasicAuthStore.

type Effect

type Effect string

Effect is a statement's outcome when it matches: Allow or Deny, exactly AWS IAM's own two-value vocabulary.

const (
	EffectAllow Effect = "Allow"
	EffectDeny  Effect = "Deny"
)

EffectAllow and EffectDeny are the only two valid Statement.Effect values.

type EmailSecretsStore

type EmailSecretsStore interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
}

EmailSecretsStore is the surface the email settings handlers need from internal/secrets.Manager. *secrets.Manager satisfies this structurally.

type EmailSettingsStore

type EmailSettingsStore interface {
	GetEmailSettings(ctx context.Context) (store.EmailSettings, error)
	UpdateEmailSettings(ctx context.Context, s store.EmailSettings) error
}

EmailSettingsStore is the store surface GET/PUT /api/v1/settings/email need: the single platform-wide row, always present, the same shape IngressSettingsStore has for its own row.

type EnvironmentStore

type EnvironmentStore interface {
	SaveEnvironment(ctx context.Context, e store.Environment) error
	GetEnvironment(ctx context.Context, id string) (store.Environment, error)
	ListEnvironmentsByProject(ctx context.Context, projectID string) ([]store.Environment, error)
	DeleteEnvironment(ctx context.Context, id string) error
	SetEnvironmentProtected(ctx context.Context, id string, protected bool) error
	SetServiceEnvironment(ctx context.Context, serviceName, envID string) error
	// SetEnvironmentEnvVars/ListEnvironmentEnvVars back GET/PUT
	// /api/v1/environments/{id}/env (environment_env.go): shared env vars
	// every service tagged with this environment inherits, sitting
	// between ProjectStore's own project_env_vars tier and a service's
	// own env (internal/reconcile/application's resolveEnv), full-replace
	// on write, mirroring SetOrganizationEnvVars/ListOrganizationEnvVars.
	SetEnvironmentEnvVars(ctx context.Context, environmentID string, vars map[string]string) error
	ListEnvironmentEnvVars(ctx context.Context, environmentID string) (map[string]string, error)
}

EnvironmentStore is the store surface the environments handlers need.

type FeatureFlagStore

type FeatureFlagStore interface {
	SaveFeatureFlag(ctx context.Context, f store.FeatureFlag) error
	GetFeatureFlag(ctx context.Context, id string) (store.FeatureFlag, error)
	GetFeatureFlagByKey(ctx context.Context, key string) (store.FeatureFlag, error)
	ListFeatureFlagsForService(ctx context.Context, serviceName string) ([]store.FeatureFlag, error)
	UpdateFeatureFlag(ctx context.Context, id, name, description string, enabled bool, rolloutPercentage int, updatedAt time.Time) error
	DeleteFeatureFlag(ctx context.Context, id string) error
}

FeatureFlagStore is the store surface the feature flag handlers need, mirroring ScheduledTaskStore's own shape for a different child resource.

type GitHubAppClient

type GitHubAppClient interface {
	CheckInstanceReachable(ctx context.Context, instanceURL string) error
	ExchangeManifestCode(ctx context.Context, instanceURL, code string) (githubapp.Credentials, error)
	GetInstallation(ctx context.Context, instanceURL, appJWT string, installationID int64) (githubapp.InstallationInfo, error)
	MintInstallationToken(ctx context.Context, instanceURL, appJWT string, installationID int64) (githubapp.InstallationToken, error)
	ListInstallationRepos(ctx context.Context, instanceURL, token string) ([]githubapp.Repo, error)
	GetRepo(ctx context.Context, instanceURL, token, owner, repo string) (githubapp.Repo, error)
	ListBranches(ctx context.Context, instanceURL, token, owner, repo string) ([]githubapp.Branch, error)
	CreateRepoWebhook(ctx context.Context, instanceURL, token, owner, repo, hookURL, secret string) error
	CreateIssueComment(ctx context.Context, instanceURL, token, owner, repo string, number int, body string) error
	CreateCommitStatus(ctx context.Context, instanceURL, token, owner, repo, sha string, state githubapp.CommitStatusState, targetURL, description, statusContext string) error
}

GitHubAppClient is the surface internal/api needs from internal/githubapp.Client: manifest code exchange, installation lookup and token minting, repo/branch listing, repo webhook registration, and posting a preview deploy's own PR comment/commit status (preview_environments_github.go). *githubapp.Client satisfies this structurally; tests substitute a hand-written fake (github_app_test.go), the same "narrow, consumer-defined interface" shape every other external-system boundary in this package uses (Builder, DockerPinger, BackupRunner).

type GitHubAppSecrets

type GitHubAppSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Resolve(ctx context.Context, serviceName, envKey string) (string, error)
	DeleteAll(ctx context.Context, serviceName string) error
}

GitHubAppSecrets is the surface the GitHub App handlers need from internal/secrets.Manager: both SetValue (storing client_secret/ webhook_secret/private_key right after a successful code exchange) and Resolve (decrypting the private key back out, in memory only, immediately before signing a JWT). A different shape from BackupSecretsSetter (set-only) and internal/backup.SecretsResolver (resolve-only): this is the one credential-bearing feature in this codebase that both writes and reads back through internal/secrets from inside internal/api itself, because minting a fresh installation token on every repo/branch-listing call (see github_app_repos.go) needs the private key synchronously in the same request, not on a separate worker path the way internal/backup.Runner's own Resolve calls are. *secrets.Manager satisfies this structurally.

type GitHubAppStore

type GitHubAppStore interface {
	GetGitHubAppConnection(ctx context.Context) (store.GitHubAppConnection, error)
	SaveGitHubAppConnection(ctx context.Context, c store.GitHubAppConnection) error
	UpdateGitHubAppInstallation(ctx context.Context, installationID int64, accountLogin string) error
	DeleteGitHubAppConnection(ctx context.Context) error
}

GitHubAppStore is the store surface the GitHub App connection handlers need. *store.DB satisfies this structurally, part of the core Store interface (github_app_connections always exists as a table even when empty, the same "core store, not an optional plug-in" shape ingressSettings/domains already establish).

type GitLabAppClient

type GitLabAppClient interface {
	ExchangeCode(ctx context.Context, instanceURL, clientID, clientSecret, redirectURI, code string) (gitlabapp.Tokens, error)
	RefreshToken(ctx context.Context, instanceURL, clientID, clientSecret, refreshToken string) (gitlabapp.Tokens, error)
	ListProjects(ctx context.Context, instanceURL, accessToken string) ([]gitlabapp.Project, error)
	GetProject(ctx context.Context, instanceURL, accessToken string, projectID int64) (gitlabapp.Project, error)
	ListBranches(ctx context.Context, instanceURL, accessToken string, projectID int64) ([]gitlabapp.Branch, error)
	CreateProjectWebhook(ctx context.Context, instanceURL, accessToken string, projectID int64, hookURL, secretToken string) error
}

GitLabAppClient is the surface internal/api needs from internal/gitlabapp.Client: OAuth code exchange/refresh, and project listing/lookup/webhook registration once connected. *gitlabapp.Client satisfies this structurally; tests substitute a hand-written fake.

type GitLabAppSecrets

type GitLabAppSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Resolve(ctx context.Context, serviceName, envKey string) (string, error)
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
	DeleteAll(ctx context.Context, serviceName string) error
}

GitLabAppSecrets is the surface the GitLab App handlers need from internal/secrets.Manager: client_secret is set once at connect time and resolved back out on every OAuth token exchange/refresh and project API call; access_token/refresh_token/token_expires_at are written and re-read on the same schedule by gitlabAccessToken. The same "this feature both writes and reads back through internal/secrets" shape GitHubAppSecrets's own doc comment describes.

type GitLabAppStore

type GitLabAppStore interface {
	GetGitLabAppConnection(ctx context.Context) (store.GitLabAppConnection, error)
	SaveGitLabAppConnection(ctx context.Context, c store.GitLabAppConnection) error
	DeleteGitLabAppConnection(ctx context.Context) error
}

GitLabAppStore is the store surface the GitLab App connection handlers need. *store.DB satisfies this structurally, the same "core store" shape GitHubAppStore's own doc comment establishes.

type GitSourceSecrets

type GitSourceSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Resolve(ctx context.Context, serviceName, envKey string) (string, error)
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
}

GitSourceSecrets is the surface a git source's connect flow and the git-push webhook route (git_webhook.go) both need from internal/secrets.Manager: unlike SecretSetter and BackupSecretsSetter (write-only, a secret's value never resolved back through internal/api at all), this package genuinely does call Resolve, but only for its own server-side use, HMAC-verifying an incoming webhook and authenticating a git clone, never to populate an HTTP response body. See handleSetGitSource's and handleGitPushWebhook's own doc comments for that boundary; *secrets.Manager satisfies this structurally.

type GitSourceStore

type GitSourceStore interface {
	SaveGitSource(ctx context.Context, g store.GitSource) error
	GetGitSource(ctx context.Context, serviceName string) (*store.GitSource, error)
	DeleteGitSource(ctx context.Context, serviceName string) error
	// SetGitSourcePreviewEnabled backs PUT
	// /api/v1/apps/{name}/preview-settings (preview_environments_handlers.go):
	// the opt-in toggle for preview environments per pull request.
	SetGitSourcePreviewEnabled(ctx context.Context, serviceName string, enabled bool) error
	// SetGitSourcePostPRComments backs the same PUT
	// /api/v1/apps/{name}/preview-settings route: the opt-in toggle for
	// posting a GitHub PR comment/commit status about a preview deploy.
	SetGitSourcePostPRComments(ctx context.Context, serviceName string, enabled bool) error
}

GitSourceStore is the store surface the git source handlers need. *store.DB satisfies this structurally.

type HookRunStore

type HookRunStore interface {
	GetHookRuns(ctx context.Context, serviceName string) ([]store.HookRun, error)
}

HookRunStore is the store surface GET /api/v1/apps/{name}/hook-runs (apps_hooks.go) needs: the most recent outcome of each of a service's pre/post-deploy hooks (internal/reconcile/application's own HookRunRecorder, migrations/0083_service_hook_runs.sql). Always set, the same "core Store interface, not an optional plug-in" shape domainMaintenance above establishes.

type ImageLister

type ImageLister interface {
	ListImages(ctx context.Context, repo string) ([]docker.ImageInfo, error)
}

ImageLister is the surface GET /api/v1/apps/{name}/images needs: discover previously-built tags under a repo, so the deploy trigger form (web/src/components/DeployTriggerForm.tsx) can offer a dropdown instead of forcing an operator to hand-type a full image reference every time. Unlike DockerPinger above, this mirrors an existing docker.Runtime method (internal/docker/runtime.go) exactly rather than inventing a narrower one: ListImages is already the consumer-defined boundary every reconciler controller depends on for rollback candidate discovery, so redeclaring it here (structurally satisfied by *docker.Client, same as Runtime) avoids a second, divergent surface for the same one capability. *docker.Client satisfies this structurally.

type IngressSettingsStore

type IngressSettingsStore interface {
	GetIngressSettings(ctx context.Context) (store.IngressSettings, error)
	UpdateIngressSettings(ctx context.Context, s store.IngressSettings) error
}

IngressSettingsStore is the store surface the ingress settings handlers need. *store.DB satisfies this structurally, the same consumer-defined interface convention every other Store sub-interface in this package follows (see CertStore, StaticSiteStore).

type InviteStore

type InviteStore interface {
	SaveInvite(ctx context.Context, inv store.Invite) error
	GetInviteByHash(ctx context.Context, hash string) (*store.Invite, error)
	GetInviteByID(ctx context.Context, id string) (*store.Invite, error)
	ListPendingInvites(ctx context.Context) ([]store.Invite, error)
	RevokeInvite(ctx context.Context, id string) error
	ClaimInvite(ctx context.Context, id string) error
}

InviteStore is the store surface the team-invite flow needs: always set, part of the core Store interface, same shape as PasswordResetTokenStore above.

type MasterKeyRotator

type MasterKeyRotator interface {
	RotateMasterKey(ctx context.Context, newMasterKey string) (rotatedAt time.Time, err error)
	GetMasterKeyRotatedAt(ctx context.Context) (rotatedAt time.Time, ok bool, err error)
}

MasterKeyRotator is the surface POST /api/v1/system/master-key/rotate and the doctor's rotation-age check need from internal/secrets.Manager: rotate every stored DEK to a new master key (given as its serialized string, so this interface never imports internal/secrets' own MasterKey type), and report when that last succeeded. A distinct interface from SecretSetter above since a token scoped to AbilityWrite (SecretSetter's own gate) has no business rotating the one key every other secret in this control plane depends on; this is gated at AbilityRoot instead (see routes.go).

type NodeRuntimeResolver

type NodeRuntimeResolver func(nodeID string) (docker.Runtime, error)

NodeRuntimeResolver picks the docker.Runtime that owns a given nodeID: the control plane's own local Docker daemon for "" (the store's established "empty NodeID means this control plane's own local node" convention, store.DesiredService.NodeID's own doc comment), or a connected remote agent's Transport for anything else. This is a func type, not an interface, the same pattern fetchFunc (builds.go) already establishes for a single-method seam: the real implementation is cmd/levelrail/main.go's own resolveNodeTransport (used identically by every reconciler controller to pick which node's Runtime it drives), passed in as a closure over that function's own registry rather than this package importing internal/agent.Registry directly.

handleExecApp (exec.go) is the first internal/api handler that needs live, per-request node routing: DockerPinger/ImageLister/ DockerDiskUsager/DockerPruner above are all fixed to this control plane's own local daemon, a deliberate choice their own doc comments explain (adding a method to docker.Runtime itself would ripple into every one of Runtime's 6+ fake implementations across internal/reconcile and internal/agent's test files, for the sake of one read-only or narrowly-scoped signal). Exec has the opposite requirement: it must reach whichever node the target app's container is actually running on, so it needs the resolver, not a fixed Runtime.

type NodeStore

type NodeStore interface {
	ListNodes(ctx context.Context) ([]store.Node, error)
	GetNode(ctx context.Context, id string) (*store.Node, error)
	DeleteNode(ctx context.Context, id string) error
	SaveNodeJoinToken(ctx context.Context, t store.NodeJoinToken) error
	UpdateNodeWorkloads(ctx context.Context, id string, acceptsApp, acceptsBuild bool) error
	// SetNodeSchedulable is TASKS.md 3.7's cordon/uncordon mutation.
	SetNodeSchedulable(ctx context.Context, id string, schedulable bool) error
}

NodeStore is the store surface the node-management handlers need. *store.DB satisfies this structurally, the same narrow consumer-defined interface convention every other Store sub-interface in this package already follows.

type NotificationChannelTester

type NotificationChannelTester interface {
	SendTest(ctx context.Context, kind alerting.NotifyKind, notifyURL string) error
}

NotificationChannelTester is the surface the test-send routes need: a real send through kind/notifyURL, not just a format check. *alerting.DeployDispatcher satisfies this structurally.

type NotificationChannels

type NotificationChannels interface {
	SaveNotificationChannel(ctx context.Context, c alerting.NotificationChannel) error
	GetNotificationChannel(ctx context.Context, id string) (*alerting.NotificationChannel, error)
	ListNotificationChannels(ctx context.Context) ([]alerting.NotificationChannel, error)
	DeleteNotificationChannel(ctx context.Context, id string) error
}

NotificationChannels is the store surface the channel handlers need. *alerting.DB satisfies this structurally.

type NotificationDeliveryStore

type NotificationDeliveryStore interface {
	RecordNotificationDelivery(ctx context.Context, d alerting.NotificationDelivery) error
	ListNotificationDeliveries(ctx context.Context, channelID string, limit int, before *time.Time) ([]alerting.NotificationDelivery, error)
}

NotificationDeliveryStore is the surface the delivery-history route and test-send recording need. *alerting.DB satisfies this structurally.

type OAuthIdentityStore

type OAuthIdentityStore interface {
	GetOAuthIdentity(ctx context.Context, provider, providerUserID string) (*store.OAuthIdentity, error)
	SaveOAuthIdentity(ctx context.Context, i store.OAuthIdentity) error
	ListOAuthIdentitiesForUser(ctx context.Context, userID string) ([]store.OAuthIdentity, error)
}

OAuthIdentityStore is the store surface the sign-in and account- linking handlers need. *store.DB satisfies this structurally.

type OAuthSecrets

type OAuthSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Resolve(ctx context.Context, serviceName, envKey string) (string, error)
}

OAuthSecrets is the surface the OAuth flow needs from internal/secrets.Manager: unlike SecretSetter (write-only), a provider's client secret must be resolved on every sign-in attempt.

type OAuthSettingsStore

type OAuthSettingsStore interface {
	GetOAuthProviderSettings(ctx context.Context, provider string) (store.OAuthProviderSettings, error)
	ListOAuthProviderSettings(ctx context.Context) ([]store.OAuthProviderSettings, error)
	UpdateOAuthProviderSettings(ctx context.Context, s store.OAuthProviderSettings) error
}

OAuthSettingsStore is the store surface the OAuth settings and sign-in handlers need for provider configuration (oauth_settings.go, this file). *store.DB satisfies this structurally.

type OnboardingStore

type OnboardingStore interface {
	GetOnboardingCompleted(ctx context.Context) (bool, error)
	MarkOnboardingCompleted(ctx context.Context) error
}

OnboardingStore is the store surface GET /api/v1/onboarding and POST /api/v1/onboarding/complete need: the single platform-wide row, always present, the same shape IngressSettingsStore has for its own row.

type Option

type Option func(*Router)

Option configures optional Router behavior.

func WithAPIRateLimit

func WithAPIRateLimit(readPerMinute, writePerMinute int) Option

WithAPIRateLimit enables a general per-actor rate limit across every requireAbility-gated route (api_rate_limit.go): read-tier (AbilityRead) requests get readPerMinute budget, everything else (write, write:sensitive, deploy, root) gets the stricter writePerMinute budget, keyed per bearer token, per session user, or per client IP for an unauthenticated caller. Either argument <= 0 disables that tier. Without this option (the default), the whole check is skipped: existing tests and any embedder that never opts in see unthrottled behavior, the same "nil is valid" shape WithSecretSetter's own absence already establishes. This package never reads the environment directly: cmd/levelrail/main.go reads APP_API_RATE_LIMIT_READ_RPM / APP_API_RATE_LIMIT_WRITE_RPM and calls this unconditionally with their resolved (env-or-default) values, so the real control plane is always protected even when an operator never sets either env var.

func WithAlertRules

func WithAlertRules(a AlertRules) Option

WithAlertRules enables POST/GET /api/v1/apps/{name}/alerts and DELETE /api/v1/apps/{name}/alerts/{id} (TASKS.md 2.5/2.7). Without one configured (the default), all three routes return 501, the same "not configured" shape WithSecretSetter and WithTelemetryQuerier's absence already produce: this control plane still starts and serves everything else without an alerting.DB wired in.

func WithAuditLogRetention

func WithAuditLogRetention(d time.Duration) Option

WithAuditLogRetention overrides how long an audit_log row survives before PurgeOldAuditEntries removes it. Without one configured (or passed as 0), defaultAuditLogRetention (90 days) applies. Same "no hardcoded thresholds, use env vars" shape as WithPreviewTTL: this package never reads the environment directly, cmd/levelrail/main.go reads APP_AUDIT_LOG_RETENTION_DAYS and passes the parsed duration here.

func WithBackupDownloader

func WithBackupDownloader(d BackupDownloader) Option

WithBackupDownloader enables GET /api/v1/databases/{name}/backups/{historyId}/download. Without one configured (the default), that route returns 501, the same "not configured" shape WithBackupRunner's absence produces: both need a live secretsManager to resolve a target's credentials, WithBackupRunner to upload with them, this to download with them.

func WithBackupRunner

func WithBackupRunner(r BackupRunner) Option

WithBackupRunner enables POST /api/v1/databases/{name}/backups. Without one configured (the default), that route returns 501, the same "not configured" shape WithBackupSecrets' absence produces; listing backup history (GET .../backups) works regardless, since it only reads store.BackupHistory rows a runner already wrote in the past, nothing about it needs a live runner today.

func WithBackupSecrets

func WithBackupSecrets(s BackupSecretsSetter) Option

WithBackupSecrets enables POST /api/v1/backup-targets. Without one configured (the default), that route returns 501, the same "not configured" shape WithSecretSetter's absence produces; listing, getting, and deleting an already-connected backup target work regardless, since none of those need to write a credential.

func WithBackupTargetTester

func WithBackupTargetTester(t BackupTargetTester) Option

WithBackupTargetTester enables POST /api/v1/backup-targets/{id}/test. Without one configured (the default), that route returns 501, the same "not configured" shape WithRegistryAuthTester's absence produces for registry credentials: both need a live secretsManager to resolve a stored credential before using it.

func WithBackupVerifier

func WithBackupVerifier(v BackupVerifier) Option

WithBackupVerifier enables POST /api/v1/databases/{name}/backups/{historyId}/verify. Without one configured (the default), that route returns 501, the same "not configured" shape WithBackupRunner's absence produces: verifying needs the identical live secretsManager WithBackupRunner and WithBackupDownloader already depend on, to resolve a target's credentials and re-download the object. Listing past verification attempts (GET .../verifications) works regardless, the same "listing needs no live runner" reasoning WithBackupRunner's own doc comment gives for backup history.

func WithBitbucketAppSecrets

func WithBitbucketAppSecrets(s BitbucketAppSecrets) Option

WithBitbucketAppSecrets enables the Bitbucket App routes that read or write a credential (connect, connect-start, callback, repo/branch listing, use-as-source). Without one configured, those return 501; GET/DELETE /api/v1/bitbucket-app work regardless.

func WithBuilder

func WithBuilder(b Builder) Option

WithBuilder enables POST /api/v1/apps/{name}/builds: a manual build trigger for an operator with no working git webhook configured (see internal/webhook.Config's own doc comment on why that path is deliberately static, single-app configuration). Without one configured (the default), that route returns 501, the same "not configured" shape WithSecretSetter/WithTelemetryQuerier/WithAlertRules absence already share. cmd/levelrail/main.go passes the same *deploy.Pipeline the git webhook receiver uses, when a BuildKit connection was successfully established at startup.

func WithCertExpiryWarningWindow

func WithCertExpiryWarningWindow(d time.Duration) Option

WithCertExpiryWarningWindow overrides how far ahead of a certificate's expiry GET /api/v1/certificates starts reporting "expiring_soon" instead of "healthy" (see alerting.CertExpiryStatus). Without one configured (or passed as 0), alerting.DefaultCertExpiryWarningWindow (14 days) applies. Same "no hardcoded thresholds, use env vars" shape as WithSessionTTL: this package never reads the environment directly, cmd/levelrail/main.go reads APP_CERT_EXPIRY_WARNING_WINDOW and passes the parsed duration here, and to alerting.NewEngine for a kind=cert_expiry rule's own evaluation.

func WithCloneRestoreRunner

func WithCloneRestoreRunner(r CloneRestoreRunner) Option

WithCloneRestoreRunner enables POST /api/v1/databases/{name}/restore-as-new. Without one configured (the default), that route returns 501, the same "not configured" shape WithRestoreRunner's absence produces; listing clone-restore history (GET .../clone-restores) works regardless, the same "listing needs no live runner" reasoning WithRestoreRunner's own doc comment gives.

func WithCloudflareDNSSecrets

func WithCloudflareDNSSecrets(s CloudflareDNSSecrets) Option

WithCloudflareDNSSecrets enables PUT/DELETE /api/v1/settings/cloudflare-dns. Without one configured (the default), both return 501; GET works regardless, the same shape WithCloudflareTunnelSecrets establishes.

func WithCloudflareTunnelSecrets

func WithCloudflareTunnelSecrets(s CloudflareTunnelSecrets) Option

WithCloudflareTunnelSecrets enables PUT/DELETE /api/v1/settings/cloudflare-tunnel. Without one configured (the default), both return 501; GET works regardless, the same shape WithEmailSecrets establishes.

func WithComposeSecrets

func WithComposeSecrets(s ComposeSecretStore) Option

WithComposeSecrets enables POST /api/v1/apps/{name}/compose to resolve a compose file's generatable SERVICE_ magic vars. Without one configured (the default), that endpoint still works for compose files with no such vars, and fails with a clear error for ones that need one.

func WithContainerLister

func WithContainerLister(l ContainerLister) Option

WithContainerLister enables GET /api/v1/system/containers. Without one configured (the default), that route returns 501, the same "not configured" shape WithExecRuntime's own absence already has.

func WithDBPinger

func WithDBPinger(p DBPinger) Option

WithDBPinger enables the database check on GET /api/v1/system/doctor. Without one configured (the default), that check reports unknown, the same "optional signal, absence is not an error" shape WithDockerPinger's own absence already has.

func WithDataDir

func WithDataDir(path string) Option

WithDataDir enables disk-usage reporting on GET /api/v1/system/status. path should be the same APP_DATA_DIR the control plane itself was started with. Without one configured (the default), the status response simply omits the two data-dir byte fields, the same "optional signal, absence is not an error" shape WithSecretSetter's own absence already has for secret-setting.

func WithDeployLogQuerier

func WithDeployLogQuerier(s DeployLogQuerier) Option

WithDeployLogQuerier enables a finished deploy attempt's full log replay on GET /api/v1/apps/{name}/deploys/{deployId}/logs. Without one configured (the default), that route returns 501 for an attempt that has already finished; an in-progress attempt's live tail is unaffected (see WithDeployRecorder). cmd/levelrail/main.go passes the same *telemetry.DB internal/deploylog.Recorder writes to.

func WithDeployNotifier

func WithDeployNotifier(n DeployNotifier) Option

WithDeployNotifier enables the actual send: without one configured (the default), a deploy attempt still finishes and is still recorded exactly as it is today, it just never dispatches a notification, even if WithDeployNotifyTargets has targets configured. Kept as a separate option from WithDeployNotifyTargets on purpose, the same "listing doesn't need a live runner" split WithBackupRunner/WithRestoreRunner already establish: a control plane can serve deploy-notify-target CRUD without necessarily having a dispatcher wired (e.g. in a test), and vice versa cmd/levelrail/main.go always wires both together from the same *alerting.DeployDispatcher, which itself embeds the *alerting.DB that WithDeployNotifyTargets is given.

func WithDeployNotifyTargets

func WithDeployNotifyTargets(t DeployNotifyTargets) Option

WithDeployNotifyTargets enables POST/GET /api/v1/apps/{name}/deploy-notify-targets and DELETE .../deploy-notify-targets/{id}: CRUD for deploy-outcome notification destinations, the sibling surface to WithAlertRules for TASKS.md wave-2 deploy-outcome notifications. Without one configured (the default), all three routes return 501, the same "not configured" shape WithAlertRules' own absence produces.

func WithDeployRecorder

func WithDeployRecorder(r *deploylog.Recorder) Option

WithDeployRecorder enables live build-log fan-out for the manual build trigger (handleTriggerBuild) and the in-progress side of GET /api/v1/apps/{name}/deploys/{deployId}/logs. Without one configured (the default), handleTriggerBuild falls back to build.SlogProgress (no persisted log, matching this package's pre-existing behavior) and the SSE log route returns 501 for any attempt still in progress. r must be the exact same *deploylog.Recorder instance passed to internal/webhook.New when a webhook handler is also wired: see that package's own doc comment for why a shared instance, not two independent ones, is required for a webhook-triggered attempt's live log to be visible through this router's SSE route.

func WithDockerDiskUsager

func WithDockerDiskUsager(u DockerDiskUsager) Option

WithDockerDiskUsager enables the docker_disk_usage field on GET /api/v1/system/status. Without one configured (the default), that field is simply omitted, the same "optional signal, absence is not an error" shape WithDockerPinger's own absence already has.

func WithDockerPinger

func WithDockerPinger(p DockerPinger) Option

WithDockerPinger enables Docker daemon connectivity reporting on GET /api/v1/system/status. Without one configured (the default), the response's DockerConnected field simply stays false, the same "optional signal, absence is not an error" shape WithDataDir and WithSecretSetter's own absence already have.

func WithDockerPruner

func WithDockerPruner(p DockerPruner) Option

WithDockerPruner enables POST /api/v1/system/prune. Without one configured (the default), that route returns 501, the same "not configured" shape WithBuilder's own absence produces.

func WithDoctorDiskWarningBytes

func WithDoctorDiskWarningBytes(n int64) Option

WithDoctorDiskWarningBytes overrides the free-space floor GET /api/v1/system/doctor's disk_space check warns below. Without one configured (or passed as 0), defaultDoctorDiskWarningBytes (1GiB) applies. Same "no hardcoded thresholds, use env vars" shape as WithCertExpiryWarningWindow: this package never reads the environment directly, cmd/levelrail/main.go reads APP_DOCTOR_DISK_WARNING_BYTES and passes the parsed value here.

func WithDoctorMasterKeyRotationWarnAge

func WithDoctorMasterKeyRotationWarnAge(d time.Duration) Option

WithDoctorMasterKeyRotationWarnAge overrides the age GET /api/v1/system/doctor's master_key_rotation check warns beyond, the same env-var-overridable-threshold shape WithDoctorDiskWarningBytes already establishes for disk_space.

func WithDomainBasicAuthSecrets

func WithDomainBasicAuthSecrets(s DomainBasicAuthSecrets) Option

WithDomainBasicAuthSecrets enables PUT/DELETE /api/v1/apps/{name}/domains/{domain}/auth. Without one configured (the default), both return 501; GET works regardless, the same shape WithCloudflareTunnelSecrets establishes.

func WithDomainTLSCertSecrets

func WithDomainTLSCertSecrets(s DomainTLSCertSecrets) Option

WithDomainTLSCertSecrets enables PUT/DELETE /api/v1/apps/{name}/domains/{domain}/tls-cert. Without one configured (the default), both return 501; GET works regardless, the same shape WithDomainBasicAuthSecrets establishes.

func WithEmailSecrets

func WithEmailSecrets(s EmailSecretsStore) Option

WithEmailSecrets enables PUT /api/v1/settings/email. Without one configured (the default), that route returns 501; GET works regardless.

func WithEmailSender

func WithEmailSender(s email.Sender) Option

WithEmailSender enables the actual send behind POST /api/v1/auth/forgot-password. Without one, that route still returns its generic success response, it just never sends anything.

func WithExecRuntime

func WithExecRuntime(r NodeRuntimeResolver) Option

WithExecRuntime enables POST /apps/{name}/exec (exec.go's handleExecApp). Without one configured (the default), that route returns 501, the same "not configured" shape WithDockerPruner's absence produces: a control plane can run every other route with no resolver wired in, exec is the one action that needs live node routing (NodeRuntimeResolver's own doc comment).

func WithGitHubAppManifestConfig

func WithGitHubAppManifestConfig(cfg githubapp.ManifestConfig) Option

WithGitHubAppManifestConfig overrides the permissions/events a fresh App registration requests (githubapp.BuildManifest's own doc comment). Without one configured, NewRouter defaults to githubapp.DefaultManifestConfig(), the same request this codebase has always sent.

func WithGitHubAppSecrets

func WithGitHubAppSecrets(s GitHubAppSecrets) Option

WithGitHubAppSecrets enables the GitHub App routes that read or write a credential (manifest callback, installation callback, repo/branch listing). Without one configured, those return 501; GET/DELETE /api/v1/github-app work regardless.

func WithGitLabAppSecrets

func WithGitLabAppSecrets(s GitLabAppSecrets) Option

WithGitLabAppSecrets enables the GitLab App routes that read or write a credential (connect, connect-start, callback, project listing, use-as-source). Without one configured, those return 501; GET/DELETE /api/v1/gitlab-app work regardless.

func WithGitSourceSecrets

func WithGitSourceSecrets(s GitSourceSecrets) Option

WithGitSourceSecrets enables PUT /api/v1/apps/{name}/git-source and the git-push webhook route. Without one configured (the default), both return 501; listing/getting/deleting a git source works regardless, since neither needs to resolve a credential.

func WithImageLister

func WithImageLister(l ImageLister) Option

WithImageLister enables GET /api/v1/apps/{name}/images. Without one configured (the default), that route returns an empty list rather than 501 or 404: the deploy trigger form's manual text input always works regardless, so a missing image lister is "no suggestions available" (an empty array), not a request error, the same "optional signal, absence is not an error" shape WithDockerPinger's own absence already has.

func WithInviteTTL

func WithInviteTTL(d time.Duration) Option

WithInviteTTL overrides how long a team invite stays acceptable before GetInviteByHash's expiry check rejects it. Without one configured (or passed as 0), defaultInviteTTL (7 days) applies. Same "no hardcoded thresholds, use env vars" shape as WithPreviewTTL: this package never reads the environment directly, cmd/levelrail/main.go reads APP_INVITE_TTL and passes the parsed duration here.

func WithLogBroadcaster

func WithLogBroadcaster(b *telemetry.LogBroadcaster) Option

WithLogBroadcaster enables GET /api/v1/apps/{name}/logs/stream, the live-tailing counterpart to WithTelemetryQuerier's historical GET .../logs. Without one configured (the default), that route returns 501, the same "not configured" shape WithDeployRecorder's absence produces for the in-progress side of a deploy attempt's log. Deliberately a separate option from WithTelemetryQuerier even though both are telemetry-flavored and cmd/levelrail/main.go always configures them together in practice: a control plane could in principle want historical log search without live tailing (or the reverse), and keeping them as two options leaves that possible instead of one silently implying the other. b must be the exact same *telemetry.LogBroadcaster instance passed to telemetry.NewLogCollector: see that constructor's own doc comment for why a shared instance, not two independent ones, is required for a line StreamOne receives from Docker to ever reach a viewer connected through this router's SSE route.

func WithMasterKeyRotation

func WithMasterKeyRotation(r MasterKeyRotator, masterKeyFilePath string) Option

WithMasterKeyRotation enables POST /api/v1/system/master-key/rotate and the doctor's rotation-age check. masterKeyFilePath should be the on-disk path the running control plane loaded its master key from, or "" if it came from APP_MASTER_KEY instead: see MasterKeyRotator's own doc comment for what that distinction changes about a rotation's response. Without this option configured (the default), the route returns 501, the same shape WithSecretSetter's own absence produces.

func WithNodeAlertThresholds

func WithNodeAlertThresholds(patchStatus, nodeDiskSpace, nodeCPU, nodeMemory float64) Option

WithNodeAlertThresholds sets the thresholds GET /api/v1/nodes/{id} uses for its live alert_status section, the same values cmd/levelrail/main.go already resolves (from APP_ALERT_PATCH_STATUS_THRESHOLD, APP_ALERT_NODE_DISK_SPACE_THRESHOLD_PERCENT, APP_ALERT_NODE_CPU_THRESHOLD_PERCENT, APP_ALERT_NODE_MEMORY_THRESHOLD_BYTES) and passes to alerting.NewEngine, so a rule's scheduled evaluation and a node's on-demand check never disagree.

func WithNotificationChannelTester

func WithNotificationChannelTester(t NotificationChannelTester) Option

WithNotificationChannelTester enables the test-send routes: a real send, not just a format check. Without one, they return 501.

func WithNotificationChannels

func WithNotificationChannels(c NotificationChannels) Option

WithNotificationChannels enables the notification-channel CRUD routes. Without one configured (the default), they return 501.

func WithNotificationDeliveries

func WithNotificationDeliveries(d NotificationDeliveryStore) Option

WithNotificationDeliveries enables the delivery-history route and records a delivery row for every existing-channel test-send. Without one, the deliveries route returns 501 and test-send simply skips recording.

func WithOAuthSecrets

func WithOAuthSecrets(s OAuthSecrets) Option

WithOAuthSecrets enables OAuth sign-in end to end. Without one configured (the default), those routes return 501, the same "not configured" shape WithSecretSetter's absence produces.

func WithPreviewTTL

func WithPreviewTTL(d time.Duration) Option

WithPreviewTTL overrides how long a preview environment can go without a webhook update before SweepStalePreviewEnvironments tears it down. Without one configured (or passed as 0), defaultPreviewTTL (7 days) applies. Same "no hardcoded thresholds, use env vars" shape as WithCertExpiryWarningWindow: this package never reads the environment directly, cmd/levelrail/main.go reads APP_PREVIEW_TTL and passes the parsed duration here.

func WithPublicHost

func WithPublicHost(host string) Option

WithPublicHost sets the IP or hostname GET /api/v1/apps/{name}/domains/{domain}/check tells an operator to point their DNS record at (domain_check.go's advertisedHost). Without one configured (the default), that handler falls back to a best-effort guess from the request's own Host header instead of failing: this package never reads the environment directly, cmd/levelrail/main.go reads APP_PUBLIC_HOST and passes it here, the same shape WithCertExpiryWarningWindow already follows for its own env var.

func WithRegistryAuthTester

func WithRegistryAuthTester(t RegistryAuthTester) Option

WithRegistryAuthTester enables POST /api/v1/registry-credentials/{id}/test. Without one configured (the default), that route returns 501, the same shape WithDockerPinger's own absence produces for GET /api/v1/system/status.

func WithRegistryCredentialSecrets

func WithRegistryCredentialSecrets(s RegistryCredentialSecretsSetter) Option

WithRegistryCredentialSecrets enables POST /api/v1/registry-credentials. Without one configured (the default), that route returns 501; GET and DELETE work regardless, the same shape WithBackupSecrets establishes.

func WithRegistrySecrets

func WithRegistrySecrets(s RegistrySecrets) Option

WithRegistrySecrets enables PUT/DELETE /api/v1/settings/registry. Without one configured (the default), both return 501; GET works regardless, the same shape WithCloudflareTunnelSecrets establishes.

func WithResourceRecommendationLookback

func WithResourceRecommendationLookback(d time.Duration) Option

WithResourceRecommendationLookback overrides how far back GET /api/v1/apps/{name}/resource-recommendation looks for usage history (handleAppResourceRecommendation). Without one configured (or passed as 0), defaultResourceRecommendationLookback (7 days) applies. Same "no hardcoded thresholds, use env vars" shape as WithSessionTTL/ WithCertExpiryWarningWindow: this package never reads the environment directly, cmd/levelrail/main.go reads APP_RESOURCE_RECOMMENDATION_LOOKBACK and passes the parsed duration here.

func WithRestoreRunner

func WithRestoreRunner(r RestoreRunner) Option

WithRestoreRunner enables POST /api/v1/databases/{name}/restore. Without one configured (the default), that route returns 501, the same "not configured" shape WithBackupRunner's absence produces; listing restore history (GET .../restores) works regardless, the same "listing needs no live runner" reasoning WithBackupRunner's own doc comment gives for backup history.

func WithScheduledTaskRunner

func WithScheduledTaskRunner(r ScheduledTaskRunner) Option

WithScheduledTaskRunner enables POST /api/v1/apps/{name}/scheduled-tasks/{id}/run. Without one configured (the default), that route returns 501: scheduled task CRUD still works either way (rt.scheduledTasks is always set), only actually running one on demand needs this. r should be the exact same *scheduledtask.Runner instance cmd/levelrail/main.go wires into scheduledtask.NewScheduler, the same "construct once, share by reference" reasoning WithBackupRunner's own doc comment gives for backupRunner.

func WithSecretSetter

func WithSecretSetter(s SecretSetter) Option

WithSecretSetter enables PUT /api/v1/apps/{name}/secrets/{key}. Without one configured (the default), that route returns 501: an operator running Levelrail without APP_MASTER_KEY set gets a clear "not configured" response instead of the route not existing at all or silently discarding the value.

func WithServiceVolumeBackupRunner

func WithServiceVolumeBackupRunner(r ServiceVolumeBackupRunner) Option

WithServiceVolumeBackupRunner enables POST /api/v1/apps/{name}/volumes/{volume}/backups. Without one configured (the default), that route returns 501, the same "not configured" shape WithBackupRunner's absence produces; listing history and reading the schedule work regardless, the same "no live runner needed" reasoning WithBackupRunner's own doc comment gives.

func WithServiceVolumeRestoreRunner

func WithServiceVolumeRestoreRunner(r ServiceVolumeRestoreRunner) Option

WithServiceVolumeRestoreRunner enables POST /api/v1/apps/{name}/volumes/{volume}/restore. Without one configured (the default), that route returns 501, the same "not configured" shape WithRestoreRunner's absence produces.

func WithSessionTTL

func WithSessionTTL(d time.Duration) Option

WithSessionTTL overrides how long a session cookie stays valid. Without one configured, defaultSessionTTL (24h) applies. The project's "no hardcoded thresholds, use env vars" rule is honored one layer up, at cmd/levelrail/main.go, which reads APP_SESSION_TTL and passes the parsed duration here; this package itself never reads the environment directly (NewRouter takes everything as constructor args, matching every other option here).

func WithTelemetryQuerier

func WithTelemetryQuerier(q TelemetryQuerier) Option

WithTelemetryQuerier enables GET /api/v1/apps/{name}/metrics and GET /api/v1/apps/{name}/logs (TASKS.md 2.3). Without one configured, both routes return 501, the same "not configured" shape WithSecretSetter's absence produces, rather than the routes not existing at all or panicking on a nil dereference.

func WithTwoFactorSecrets

func WithTwoFactorSecrets(s TwoFactorSecrets) Option

WithTwoFactorSecrets enables the 2FA setup/confirm/disable/regenerate routes (twofactor.go), which need to write and read back a user's TOTP secret. Without one configured, those return 501; GET /api/v1/auth/2fa (status) works regardless, since it only reads store.User.TOTPEnabled.

func WithVolumeCloneRestoreRunner

func WithVolumeCloneRestoreRunner(r VolumeCloneRestoreRunner) Option

WithVolumeCloneRestoreRunner enables POST /api/v1/apps/{name}/volumes/{volume}/restore-as-new. Without one configured (the default), that route returns 501, the same "not configured" shape WithServiceVolumeRestoreRunner's absence produces; listing clone-restore history (GET .../clone-restores) works regardless, the same "listing needs no live runner" reasoning WithCloneRestoreRunner's own doc comment gives.

type OrganizationStore

type OrganizationStore interface {
	SaveOrganization(ctx context.Context, o store.Organization) error
	GetOrganization(ctx context.Context, id string) (store.Organization, error)
	ListOrganizations(ctx context.Context) ([]store.Organization, error)
	DeleteOrganization(ctx context.Context, id string) error
	SetProjectOrganization(ctx context.Context, projectID, orgID string) error
	// SetOrganizationEnvVars/ListOrganizationEnvVars back GET/PUT
	// /api/v1/organizations/{id}/env (organization_env.go): shared env
	// vars every project filed under this organization inherits as the
	// base layer beneath its own project_env_vars tier
	// (internal/reconcile/application's resolveEnv), full-replace on
	// write, mirroring ProjectStore's SetProjectEnvVars/ListProjectEnvVars
	// one level up.
	SetOrganizationEnvVars(ctx context.Context, orgID string, vars map[string]string) error
	ListOrganizationEnvVars(ctx context.Context, orgID string) (map[string]string, error)
}

OrganizationStore is the store surface the organizations handlers need.

type PasswordResetTokenStore

type PasswordResetTokenStore interface {
	SavePasswordResetToken(ctx context.Context, t store.PasswordResetToken) error
	GetPasswordResetTokenByHash(ctx context.Context, hash string) (*store.PasswordResetToken, error)
	ClaimPasswordResetToken(ctx context.Context, id string) error
}

PasswordResetTokenStore is the store surface the forgot-password flow needs: always set, part of the core Store interface.

type PolicyStore

type PolicyStore interface {
	SavePolicy(ctx context.Context, p store.Policy) error
	UpdatePolicy(ctx context.Context, id, name, description, document string) error
	GetPolicy(ctx context.Context, id string) (*store.Policy, error)
	ListPolicies(ctx context.Context) ([]store.Policy, error)
	DeletePolicy(ctx context.Context, id string) error
	AttachPolicy(ctx context.Context, id, policyID, principalType, principalID string) error
	DetachPolicy(ctx context.Context, policyID, principalType, principalID string) error
	ListPoliciesForPrincipal(ctx context.Context, principalType, principalID string) ([]store.Policy, error)
	ListAttachmentsForPolicy(ctx context.Context, policyID string) ([]store.PolicyAttachment, error)
}

PolicyStore is the store surface the IAM policy handlers need, defined here next to the handlers that use it, the same "single-file feature" shape OnboardingStore/AuditStore already use rather than every store interface living in store_interfaces.go.

type PreviewEnvironmentStore

type PreviewEnvironmentStore interface {
	SavePreviewEnvironment(ctx context.Context, p store.PreviewEnvironment) error
	UpdatePreviewEnvironment(ctx context.Context, p store.PreviewEnvironment) error
	GetPreviewEnvironmentByAppAndPR(ctx context.Context, appName string, prNumber int) (*store.PreviewEnvironment, error)
	ListPreviewEnvironmentsByApp(ctx context.Context, appName string) ([]store.PreviewEnvironment, error)
	DeletePreviewEnvironment(ctx context.Context, id string) error
	ListStalePreviewEnvironments(ctx context.Context, cutoff time.Time) ([]store.PreviewEnvironment, error)
}

PreviewEnvironmentStore is the store surface preview environment webhook handling and its HTTP routes (preview_environments_handlers.go) need. *store.DB satisfies this structurally.

type ProjectStore

type ProjectStore interface {
	SaveProject(ctx context.Context, p store.Project) error
	GetProject(ctx context.Context, id string) (store.Project, error)
	ListProjects(ctx context.Context) ([]store.Project, error)
	DeleteProject(ctx context.Context, id string) error
	// SetProjectEnvVars/ListProjectEnvVars back GET/PUT
	// /api/v1/projects/{id}/env (project_env.go): shared env vars every
	// app filed under this project inherits as its resolveEnv base
	// layer (internal/reconcile/application), full-replace on write, the
	// same shape PUT /api/v1/apps/{name}'s own env field already has.
	SetProjectEnvVars(ctx context.Context, projectID string, vars map[string]string) error
	ListProjectEnvVars(ctx context.Context, projectID string) (map[string]string, error)
}

ProjectStore is the store surface the projects handlers need (projects.go). See that file's own package doc comment for why a project is deliberately not the start of the deferred Phase 4 teams/ RBAC work (repo-plan section 6's own scope note): no owner, no member list, no per-project ability, just create/list/get/delete.

type RecoveryCodeStore

type RecoveryCodeStore interface {
	ReplaceUserRecoveryCodes(ctx context.Context, userID string, hashes []string) error
	ConsumeUserRecoveryCode(ctx context.Context, userID, hash string) (bool, error)
	CountUnusedUserRecoveryCodes(ctx context.Context, userID string) (int, error)
	DeleteUserRecoveryCodes(ctx context.Context, userID string) error
}

RecoveryCodeStore is the store surface the 2FA handlers (twofactor.go) need for single-use fallback codes: always set, part of the core Store interface, the same "no secrets configuration needed" shape AuthStore itself has (a recovery code's hash is an ordinary column, unlike a TOTP secret which goes through TwoFactorSecrets below).

type RegistryAuthTester

type RegistryAuthTester interface {
	TestRegistryAuth(ctx context.Context, host, username, password string) error
}

RegistryAuthTester is the surface POST /api/v1/registry-credentials/{id}/test needs: ask the control plane's own local Docker daemon to authenticate against a registry host with a username/password pair, without pulling anything. Fixed to this control plane's own local daemon, the same DockerPinger/ImageLister/ DockerDiskUsager/DockerPruner reasoning above applies: adding this to docker.Runtime would ripple into 6+ fakes for one narrowly-scoped signal. *docker.Client satisfies this structurally via TestRegistryAuth (internal/docker/client.go).

type RegistryCredentialSecretsSetter

type RegistryCredentialSecretsSetter interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Resolve(ctx context.Context, serviceName, envKey string) (string, error)
}

RegistryCredentialSecretsSetter is the surface registry credential create/update/test needs from internal/secrets.Manager. Resolve is used only by handleTestRegistryCredential, to authenticate against the real registry on the operator's behalf; it is never used to echo a password back in an HTTP response, the same boundary GitSourceSecrets already draws for its own Resolve use.

type RegistryCredentialStore

type RegistryCredentialStore interface {
	SaveRegistryCredential(ctx context.Context, c store.RegistryCredential) error
	GetRegistryCredential(ctx context.Context, id string) (store.RegistryCredential, error)
	GetRegistryCredentialByName(ctx context.Context, name string) (store.RegistryCredential, error)
	ListRegistryCredentials(ctx context.Context) ([]store.RegistryCredential, error)
	UpdateRegistryCredential(ctx context.Context, id, name, registryHost, username string, expiresAt *time.Time) error
	DeleteRegistryCredential(ctx context.Context, id string) error
}

RegistryCredentialStore is the store surface the registry credential handlers need.

type RegistrySecrets

type RegistrySecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
	DeleteAll(ctx context.Context, serviceName string) error
}

RegistrySecrets is the surface the built-in registry settings handlers need from internal/secrets.Manager: set the generated password (first enable), check whether one exists, and clear it (disable). *secrets.Manager satisfies this structurally, the same "narrow, consumer-defined interface" shape CloudflareTunnelSecrets already establishes.

type RegistryStore

type RegistryStore interface {
	GetRegistrySettings(ctx context.Context) (store.RegistrySettings, error)
	UpdateRegistrySettings(ctx context.Context, s store.RegistrySettings) error
}

RegistryStore is the store surface GET/PUT/DELETE /api/v1/settings/registry need: the single platform-wide row, always present, the same shape CloudflareTunnelStore has for its own row.

type RestoreHistoryStore

type RestoreHistoryStore interface {
	ListRestoreHistory(ctx context.Context, databaseName string) ([]store.RestoreHistory, error)
}

RestoreHistoryStore is the store surface the restore history handler needs.

type RestoreRunner

type RestoreRunner interface {
	RunRestore(ctx context.Context, historyID, databaseName, backupHistoryID, engine, containerName string) error
}

RestoreRunner is the surface the restore trigger handler needs from internal/backup.RestoreRunner: run one restore attempt end to end, from an already-minted history ID through to a finished store.RestoreHistory row. *backup.RestoreRunner satisfies this structurally; internal/api never imports internal/backup directly, the same boundary BackupRunner's own doc comment describes.

type Role

type Role struct {
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Abilities   []string `json:"abilities"`
}

Role is a curated, named preset over the raw ability list (abilities.go): picking one sets a user's Abilities to exactly this set in one action, instead of hand-picking abilities individually. It is a convenience layer, not a second permission model: every existing ability check still only ever sees the resolved Abilities.

type Router

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

Router wires every internal/api handler onto one http.Handler.

func NewRouter

func NewRouter(logger *slog.Logger, b *brand.Brand, s Store, opts ...Option) *Router

NewRouter builds a Router. logger defaults to slog.Default() if nil.

func (*Router) CheckDomainStatus

func (rt *Router) CheckDomainStatus(ctx context.Context, domain string) (string, error)

CheckDomainStatus exposes runDomainCheck to internal/alerting's kind=domain_health evaluator (see alerting.DomainCheckSource), the same DNS check handleCheckDomain itself runs. Uses rt.publicHost only: unlike an HTTP handler, an engine tick has no request Host header to fall back on for advertisedHost's own best-effort guess, so a domain_health rule needs APP_PUBLIC_HOST configured to evaluate meaningfully; without it every domain simply reports domainCheckStatusUnconfigured, which EvaluateDomainHealth treats as inconclusive, not unhealthy.

func (*Router) Handler

func (rt *Router) Handler() http.Handler

Handler builds the *http.ServeMux with every route registered. Called once at startup; the returned handler is what gets wrapped in an *http.Server. Route registration is split across this file (core: brand, auth, users, oauth, tokens, apps, databases) and routes_platform.go (nodes, certs, ingress, email, domains, backups, restore, storage, integrations, audit) purely to keep each function under a readable size; there is no other meaning to the split.

func (*Router) PurgeOldAuditEntries

func (rt *Router) PurgeOldAuditEntries(ctx context.Context) (int64, error)

PurgeOldAuditEntries deletes every audit_log row older than effectiveAuditLogRetention, returning how many rows were removed.

func (*Router) RunAuditLogSweeper

func (rt *Router) RunAuditLogSweeper(ctx context.Context, interval time.Duration) error

RunAuditLogSweeper calls PurgeOldAuditEntries on interval until ctx is done, the same ticker shape RunPreviewSweeper already establishes.

func (*Router) RunPreviewSweeper

func (rt *Router) RunPreviewSweeper(ctx context.Context, interval time.Duration) error

RunPreviewSweeper calls SweepStalePreviewEnvironments on interval until ctx is done, matching the shape of every other periodic loop in this codebase (backup.Scheduler.Run, scheduledtask.Scheduler.Run, alerting.Engine.Run).

func (*Router) SweepStalePreviewEnvironments

func (rt *Router) SweepStalePreviewEnvironments(ctx context.Context) (swept int, err error)

SweepStalePreviewEnvironments tears down every preview environment last updated before now minus effectivePreviewTTL, reusing teardownPreviewRecord, the exact same deletion path the pull-request- closed webhook and the manual teardown route already use. One preview's teardown failure never blocks the rest, the same "one broken resource must not block others" principle every other periodic loop in this codebase follows (alerting.Engine.Tick, backup.Scheduler.Tick, scheduledtask.Scheduler.Tick).

A partial teardown (http.StatusMultiStatus) counts as handled, not an error: teardownPreviewRecord already marks the row Failed with a reason and bumps its UpdatedAt, so the next sweep won't immediately retry the same broken preview every tick.

type ScheduledTaskRunner

type ScheduledTaskRunner interface {
	Run(ctx context.Context, task store.ScheduledTask) error
}

ScheduledTaskRunner is the surface POST .../scheduled-tasks/{id}/run needs to actually run a task on demand: identical in shape to internal/scheduledtask.TaskRunner, redeclared here rather than imported across the package boundary, the same reasoning backup.ScheduledBackupRunner's own doc comment gives for its own redeclared interfaces. *scheduledtask.Runner satisfies this structurally, and cmd/levelrail/main.go wires that exact same instance into both this option and scheduledtask.NewScheduler, so a scheduled tick and a manual "run now" are always the one exec-and-record implementation, never two copies of it.

type ScheduledTaskStore

type ScheduledTaskStore interface {
	SaveScheduledTask(ctx context.Context, t store.ScheduledTask) error
	GetScheduledTask(ctx context.Context, id string) (store.ScheduledTask, error)
	ListScheduledTasksForService(ctx context.Context, serviceName string) ([]store.ScheduledTask, error)
	UpdateScheduledTask(ctx context.Context, id string, command []string, schedule string, enabled bool, updatedAt time.Time) error
	DeleteScheduledTask(ctx context.Context, id string) error
}

ScheduledTaskStore is the store surface the scheduled task handlers need, mirroring BackupTargetStore's own shape for a different child resource.

type SecretSetter

type SecretSetter interface {
	SetValueGuarded(ctx context.Context, serviceName, envKey, plaintext string, overwriteLocked bool) error
	ListKeys(ctx context.Context, serviceName string) ([]store.SecretKeyInfo, error)
	SetLocked(ctx context.Context, serviceName, envKey string, locked bool) error
}

SecretSetter is the surface the secrets handlers need from internal/secrets.Manager: set a value (with a reversible per-key lock guard), list which keys exist, toggle a key's lock, never read one back. Every other secret-backed feature in this file keeps using Manager's plain SetValue directly, unaffected by this narrower interface.

type ServiceVolumeBackupHistoryStore

type ServiceVolumeBackupHistoryStore interface {
	ListServiceVolumeBackupHistory(ctx context.Context, serviceName, volumeName string, limit int, before *time.Time) ([]store.BackupHistory, error)
}

ServiceVolumeBackupHistoryStore is the store surface the volume backup history handler needs, the volume counterpart of BackupHistoryStore (backups.go): GetBackupHistory is already generic by ID (shared with the database path via rt.backupHistory), so only the listing method needs its own volume-scoped query.

type ServiceVolumeBackupRunner

type ServiceVolumeBackupRunner interface {
	RunVolumeBackup(ctx context.Context, historyID, serviceName, volumeName, dockerVolumeName, targetID string) error
}

ServiceVolumeBackupRunner is the surface the volume backup trigger handler needs from internal/backup.Runner: the volume counterpart of BackupRunner (backups.go). *backup.Runner satisfies this structurally, the same boundary BackupRunner's own doc comment describes.

type ServiceVolumeBackupScheduleStore

type ServiceVolumeBackupScheduleStore interface {
	SetServiceVolumeBackupSchedule(ctx context.Context, serviceName, volumeName, targetID, schedule string, retain, retainDays int) error
	GetServiceVolumeBackupSchedule(ctx context.Context, serviceName, volumeName string) (store.ServiceVolumeBackupConfig, error)
}

ServiceVolumeBackupScheduleStore is the store surface the volume backup schedule handlers need: the volume counterpart of SetDatabaseBackupSchedule/GetDatabaseBackupSchedule-shaped access, kept as its own table (service_volume_backups) rather than columns on desired_services, see migrations/0075's own doc comment.

type ServiceVolumeRestoreHistoryStore

type ServiceVolumeRestoreHistoryStore interface {
	ListServiceVolumeRestoreHistory(ctx context.Context, serviceName, volumeName string) ([]store.RestoreHistory, error)
}

ServiceVolumeRestoreHistoryStore is the store surface the volume restore history handler needs, the volume counterpart of RestoreHistoryStore (restore.go).

type ServiceVolumeRestoreRunner

type ServiceVolumeRestoreRunner interface {
	RunVolumeRestore(ctx context.Context, historyID, serviceName, volumeName, dockerVolumeName, backupHistoryID string) error
}

ServiceVolumeRestoreRunner is the surface the volume restore trigger handler needs from internal/backup.RestoreRunner: the volume counterpart of RestoreRunner (restore.go). *backup.RestoreRunner satisfies this structurally, the same boundary RestoreRunner's own doc comment describes.

type Statement

type Statement struct {
	Effect   Effect   `json:"Effect"`
	Action   []string `json:"Action"`
	Resource []string `json:"Resource"`
}

Statement is one Allow/Deny rule inside a Document: Action holds ability strings (abilities.go's AbilityRead etc.) or "*", Resource holds resource identifiers ("app:myapp", "database:main") or "*". Both are lists so one statement can cover several actions or resources at once, matching how a real IAM statement reads.

type StaticSiteStore

type StaticSiteStore interface {
	ListStaticSites(ctx context.Context) ([]store.StaticSite, error)
}

StaticSiteStore is the store surface handleListStaticSites needs. *store.DB satisfies this structurally, the same consumer-defined interface convention every other Store sub-interface in this package follows (see CertStore, DatabaseStore).

type TelemetryQuerier

type TelemetryQuerier interface {
	QueryMetrics(ctx context.Context, resourceID, metric string, from, to time.Time) ([]telemetry.Sample, error)
	QueryLogs(ctx context.Context, resourceID string, from, to time.Time, query string) ([]telemetry.LogEntry, error)
}

TelemetryQuerier is the surface the metrics and logs query handlers need (TASKS.md 2.3). *telemetry.Federator satisfies this structurally: today it fans out to exactly one source (this node's own local store), the same single-node-now shape already established for the reconcile agent transport, so Phase 3's real multi-node federation slots in here without this package changing.

type TokenStore

type TokenStore interface {
	SaveAPIToken(ctx context.Context, t store.APIToken) error
	GetAPITokenByHash(ctx context.Context, hash string) (*store.APIToken, error)
	ListAPITokens(ctx context.Context) ([]store.APIToken, error)
	RevokeAPIToken(ctx context.Context, id string) error
	TouchAPITokenLastUsed(ctx context.Context, id string) error
}

TokenStore is the store surface the API-token handlers and the ability-aware auth middleware need (TASKS.md "Backend auth foundation").

type TwoFactorSecrets

type TwoFactorSecrets interface {
	SetValue(ctx context.Context, serviceName, envKey, plaintext string) error
	Resolve(ctx context.Context, serviceName, envKey string) (string, error)
	DeleteAll(ctx context.Context, serviceName string) error
}

TwoFactorSecrets is the surface these handlers need from internal/secrets.Manager: SetValue at setup time, Resolve to check a submitted code against the stored secret (confirm, disable, regenerate, and the login-time verify handler all need this), and DeleteAll when 2FA is turned off. *secrets.Manager satisfies this structurally, the same set-then-resolve shape GitHubAppSecrets already documents for the identical need.

type VolumeCloneRestoreHistoryStore

type VolumeCloneRestoreHistoryStore interface {
	ListVolumeCloneRestores(ctx context.Context, serviceName, volumeName string) ([]store.VolumeCloneRestore, error)
}

VolumeCloneRestoreHistoryStore is the store surface the volume clone-restore history handler needs, the app service volume counterpart of CloneRestoreHistoryStore (database_clone_restore.go).

type VolumeCloneRestoreRunner

type VolumeCloneRestoreRunner interface {
	RunVolumeCloneRestore(ctx context.Context, historyID, sourceServiceName, sourceVolumeName, newVolumeName, backupHistoryID string) error
}

VolumeCloneRestoreRunner is the surface the volume clone-restore trigger handler needs from internal/backup.VolumeCloneRestoreRunner, the app service volume counterpart of CloneRestoreRunner. *backup. VolumeCloneRestoreRunner satisfies this structurally, the same boundary CloneRestoreRunner's own doc comment describes.

type WebhookDeliveryStore

type WebhookDeliveryStore interface {
	SaveWebhookDelivery(ctx context.Context, d store.WebhookDelivery) error
	GetWebhookDelivery(ctx context.Context, id string) (*store.WebhookDelivery, error)
	ListWebhookDeliveries(ctx context.Context, serviceName string, limit int, before *time.Time) ([]store.WebhookDelivery, error)
}

WebhookDeliveryStore is the store surface real inbound webhook delivery history needs: row-per-delivery CRUD backing GET /api/v1/apps/{name}/webhook-deliveries and its replay endpoint. *store.DB satisfies this structurally.

Source Files

Jump to

Keyboard shortcuts

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