Files
browser/platform/api/resource_handlers.go
T

2687 lines
97 KiB
Go

package api
import (
"net/http"
"strconv"
"strings"
"browser.local/platform/domain"
"browser.local/platform/dto"
"browser.local/platform/service"
"browser.local/platform/validator"
)
type coreHandlers struct {
core service.Core
enforceAuthorization bool
}
func newCoreHandlers(core service.Core, enforceAuthorization bool) *coreHandlers {
return &coreHandlers{core: core, enforceAuthorization: enforceAuthorization}
}
func (h *coreHandlers) register(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/auth/register", h.authRegister)
mux.HandleFunc("/api/v1/auth/login", h.authLogin)
mux.HandleFunc("/api/v1/auth/logout", h.authLogout)
mux.HandleFunc("/api/v1/auth/rotate", h.authRotate)
mux.HandleFunc("/api/v1/users/current", h.currentUser)
mux.HandleFunc("/api/v1/users/current/profile", h.currentUserProfile)
mux.HandleFunc("/api/v1/users/current/theme", h.currentUserTheme)
mux.HandleFunc("/api/v1/users", h.users)
mux.HandleFunc("/api/v1/users/{id}", h.userDetail)
mux.HandleFunc("/api/v1/ai-providers", h.aiProviders)
mux.HandleFunc("/api/v1/ai-providers/{id}", h.aiProviderDetail)
mux.HandleFunc("/api/v1/ai-providers/{id}/status", h.aiProviderStatus)
mux.HandleFunc("/api/v1/ai-providers/{id}/test", h.aiProviderTest)
mux.HandleFunc("/api/v1/ai-providers/{id}/models", h.aiProviderModels)
mux.HandleFunc("/api/v1/ai/invocations", h.aiInvocation)
mux.HandleFunc("/api/v1/ai/config-suggestions", h.aiConfigSuggestion)
mux.HandleFunc("/api/v1/game-plugins", h.gamePlugins)
mux.HandleFunc("/api/v1/game-plugins/register-manifest", h.gamePluginManifestRegistration)
mux.HandleFunc("/api/v1/plugin-marketplace/plugins", h.marketplacePlugins)
mux.HandleFunc("/api/v1/plugin-marketplace/plugins/{id}/state", h.marketplacePluginState)
mux.HandleFunc("/api/v1/plugin-marketplace/plugins/{id}", h.marketplacePluginDetail)
mux.HandleFunc("/api/v1/plugin-bridge/authorize", h.pluginBridgeAuthorize)
mux.HandleFunc("/api/v1/plugin-bridge/execute", h.pluginBridgeExecute)
mux.HandleFunc("/api/v1/game-plugins/{id}", h.gamePluginDetail)
mux.HandleFunc("/api/v1/metrics/platform", h.platformMetrics)
mux.HandleFunc("/api/v1/metrics/server-instances", h.serverInstanceMetrics)
mux.HandleFunc("/api/v1/production/capacity", h.productionCapacity)
mux.HandleFunc("/api/v1/production/capacity/admission", h.productionCapacityAdmission)
mux.HandleFunc("/api/v1/alerts", h.alerts)
mux.HandleFunc("/api/v1/alerts/{id}/acknowledge", h.alertAcknowledge)
mux.HandleFunc("/api/v1/alerts/{id}/resolve", h.alertResolve)
mux.HandleFunc("/api/v1/alerts/{id}/retry", h.alertRetry)
mux.HandleFunc("/api/v1/plugin-lifecycles", h.pluginLifecycles)
mux.HandleFunc("/api/v1/plugin-lifecycles/{pluginId}/actions", h.pluginLifecycleAction)
mux.HandleFunc("/api/v1/ai/config-diffs", h.aiConfigDiffs)
mux.HandleFunc("/api/v1/ai/config-diffs/{id}/approve", h.aiConfigDiffApprove)
mux.HandleFunc("/api/v1/metrics/server-instances/history", h.metricHistory)
mux.HandleFunc("/api/v1/run/metrics/batches", h.requireRunSignature(h.runMetricBatchIngest))
mux.HandleFunc("/api/v1/backups", h.backups)
mux.HandleFunc("/api/v1/backups/{id}", h.backupDetail)
mux.HandleFunc("/api/v1/server-instances", h.serverInstances)
mux.HandleFunc("/api/v1/server-instances/workflows/create", h.serverInstanceCreateWorkflow)
mux.HandleFunc("/api/v1/server-instances/{id}/start", h.serverInstanceStart)
mux.HandleFunc("/api/v1/server-instances/{id}/stop", h.serverInstanceStop)
mux.HandleFunc("/api/v1/server-instances/{id}/process/status", h.serverInstanceProcessStatus)
mux.HandleFunc("/api/v1/server-instances/{id}/runtime/actions", h.serverRuntimeActions)
mux.HandleFunc("/api/v1/server-instances/{id}/runtime-binding", h.serverRuntimeBinding)
mux.HandleFunc("/api/v1/server-instances/{id}/deployment/reveal", h.serverDeploymentReveal)
mux.HandleFunc("/api/v1/server-instances/{id}/deployment", h.serverDeployment)
mux.HandleFunc("/api/v1/server-instances/{id}/deploy", h.serverInstanceDeploy)
mux.HandleFunc("/api/v1/server-instances/{id}/remote-adapters", h.remoteAdapters)
mux.HandleFunc("/api/v1/server-instances/{id}/rcon/commands", h.sourceRCONCommands)
mux.HandleFunc("/api/v1/server-instances/{id}/run/generate", h.serverRunGenerate)
mux.HandleFunc("/api/v1/server-instances/{id}/run/download", h.serverRunDownload)
mux.HandleFunc("/api/v1/server-instances/{id}/run/key/reset", h.serverRunKeyReset)
mux.HandleFunc("/api/v1/server-instances/{id}/run/update", h.serverRunUpdate)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/generate", h.serverClientManagerGenerate)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/download", h.serverClientManagerDownload)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/key/reset", h.serverClientManagerKeyReset)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers", h.serverClientManagerLifecycles)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/{profileKey}", h.serverClientManagerLifecycleDetail)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/deploy", h.serverClientManagerDeploy)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/control", h.serverClientManagerControl)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/update", h.serverClientManagerUpdateLifecycle)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/retry", h.serverClientManagerRetry)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/revoke-session", h.serverClientManagerRevokeSession)
mux.HandleFunc("/api/v1/server-instances/{id}/client-managers/uninstall", h.serverClientManagerUninstall)
mux.HandleFunc("/api/v1/server-instances/{id}/game-client-bridge", h.serverGameClientBridgeStatus)
mux.HandleFunc("/api/v1/server-instances/{id}/game-client-bridge/commands", h.serverGameClientBridgeCommands)
mux.HandleFunc("/api/v1/server-instances/{id}/game-client-bridge/commands/{commandId}/cancel", h.serverGameClientBridgeCommandCancel)
mux.HandleFunc("/api/v1/server-instances/{id}/game-client-bridge/commands/{commandId}", h.serverGameClientBridgeCommandDetail)
mux.HandleFunc("/api/v1/server-instances/{id}/game-client-bridge/snapshots", h.serverGameClientBridgeSnapshots)
mux.HandleFunc("/api/v1/server-instances/{id}/game-players", h.serverGamePlayers)
mux.HandleFunc("/api/v1/server-instances/{id}/game-players/{playerId}", h.serverGamePlayerDetail)
mux.HandleFunc("/api/v1/server-instances/{id}/game-players/{playerId}/state", h.serverGamePlayerState)
mux.HandleFunc("/api/v1/server-instances/{id}/game-players/{playerId}/state-patches", h.serverGamePlayerStatePatches)
mux.HandleFunc("/api/v1/server-instances/{id}/game-players/{playerId}/state-patches/{patchId}/approve", h.serverGamePlayerStatePatchApprove)
mux.HandleFunc("/api/v1/server-instances/{id}/game-map-trajectories", h.serverGameMapTrajectories)
mux.HandleFunc("/api/v1/server-instances/{id}/game-gifts", h.serverGameGiftCatalogs)
mux.HandleFunc("/api/v1/server-instances/{id}/game-gifts/{catalogId}/publish", h.serverGameGiftCatalogPublish)
mux.HandleFunc("/api/v1/server-instances/{id}/game-gifts/{catalogId}/revisions", h.serverGameGiftCatalogRevisions)
mux.HandleFunc("/api/v1/server-instances/{id}/game-gift-grants", h.serverGameGiftGrants)
mux.HandleFunc("/api/v1/server-instances/{id}/game-gift-grants/{grantId}/approve", h.serverGameGiftGrantApprove)
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies/check", h.serverDependenciesCheck)
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies/install", h.serverDependenciesInstall)
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies", h.serverDependencies)
mux.HandleFunc("/api/v1/server-instances/{id}/logs/live", h.serverLiveLogs)
mux.HandleFunc("/api/v1/server-instances/{id}/logs/events", h.serverLogEvents)
mux.HandleFunc("/api/v1/server-instances/{id}/logs/backfill", h.serverLogsBackfill)
mux.HandleFunc("/api/v1/server-instances/{id}/files/read-snapshot", h.serverDeclaredFileReadSnapshot)
mux.HandleFunc("/api/v1/server-instances/{id}/config/diff", h.serverInstanceConfigDiff)
mux.HandleFunc("/api/v1/server-instances/{id}/config/approve", h.serverInstanceConfigApprove)
mux.HandleFunc("/api/v1/server-instances/{id}/config", h.serverInstanceConfig)
mux.HandleFunc("/api/v1/server-instances/{id}/administrators/candidates", h.serverAdministratorCandidates)
mux.HandleFunc("/api/v1/server-instances/{id}/administrators", h.serverAdministrators)
mux.HandleFunc("/api/v1/server-instances/{id}/administrators/{userId}", h.serverAdministratorDetail)
mux.HandleFunc("/api/v1/server-instances/{id}", h.serverInstanceDetail)
mux.HandleFunc("/api/v1/run/control/hello", h.runControlHello)
mux.HandleFunc("/api/v1/run/control/heartbeat", h.requireRunSignature(h.runControlHeartbeat))
mux.HandleFunc("/api/v1/run/jobs/claim", h.requireRunSignature(h.runJobClaim))
mux.HandleFunc("/api/v1/run/jobs/ack", h.requireRunSignature(h.runJobAck))
mux.HandleFunc("/api/v1/run/jobs/progress", h.requireRunSignature(h.runJobProgress))
mux.HandleFunc("/api/v1/run/jobs/result", h.requireRunSignature(h.runJobResult))
mux.HandleFunc("/api/v1/run/jobs/build-input", h.requireRunSignature(h.runJobBuildInput))
mux.HandleFunc("/api/v1/run/jobs/dependency-input", h.requireRunSignature(h.runJobDependencyInput))
mux.HandleFunc("/api/v1/run/jobs/source-rcon-input", h.requireRunSignature(h.runSourceRCONInput))
mux.HandleFunc("/api/v1/run/jobs/protected-request-input", h.requireRunSignature(h.runProtectedRequestInput))
mux.HandleFunc("/api/v1/run/jobs/update-input", h.requireRunSignature(h.runJobUpdateInput))
mux.HandleFunc("/api/v1/run/jobs/update-chunk", h.requireRunSignature(h.runJobUpdateChunk))
mux.HandleFunc("/api/v1/run/jobs/update-health", h.requireRunSignature(h.runJobUpdateHealth))
mux.HandleFunc("/api/v1/run/jobs/client-manager-input", h.requireRunSignature(h.runClientManagerLifecycleInput))
mux.HandleFunc("/api/v1/run/jobs/client-manager-chunk", h.requireRunSignature(h.runClientManagerLifecycleChunk))
mux.HandleFunc("/api/v1/run/jobs/cancel", h.requireRunSignature(h.runJobCancelPoll))
mux.HandleFunc("/api/v1/run/jobs/reconcile", h.requireRunSignature(h.runJobReconcile))
mux.HandleFunc("/api/v1/run/logs/batches", h.requireRunSignature(h.runLogBatchIngest))
mux.HandleFunc("/api/v1/run/artifacts/open", h.requireRunSignature(h.runArtifactOpen))
mux.HandleFunc("/api/v1/run/artifacts/chunks", h.requireRunSignature(h.runArtifactChunkUpload))
mux.HandleFunc("/api/v1/run/artifacts/status", h.requireRunSignature(h.runArtifactStatus))
mux.HandleFunc("/api/v1/run/artifacts/complete", h.requireRunSignature(h.runArtifactComplete))
mux.HandleFunc("/api/v1/run/endpoints", h.runEndpoints)
mux.HandleFunc("/api/v1/run/endpoints/{id}", h.runEndpointDetail)
mux.HandleFunc("/api/v1/jobs", h.jobs)
mux.HandleFunc("/api/v1/jobs/{id}/cancel", h.jobCancel)
mux.HandleFunc("/api/v1/jobs/{id}", h.jobDetail)
mux.HandleFunc("/api/v1/file-operations/dispatch", h.fileOperationDispatch)
mux.HandleFunc("/api/v1/artifacts", h.artifacts)
mux.HandleFunc("/api/v1/artifacts/{id}/download", h.artifactDownload)
mux.HandleFunc("/api/v1/artifacts/{id}/content", h.artifactContent)
mux.HandleFunc("/api/v1/artifacts/{id}", h.artifactDetail)
mux.HandleFunc("/api/v1/log-streams", h.logStreams)
mux.HandleFunc("/api/v1/log-streams/query", h.logStreamQuery)
mux.HandleFunc("/api/v1/log-streams/{id}", h.logStreamDetail)
mux.HandleFunc("/api/v1/audit-events", h.auditEvents)
mux.HandleFunc("/api/v1/audit-events/{id}", h.auditEventDetail)
mux.HandleFunc("/api/v1/client-managers/register", h.clientManagerRegister)
mux.HandleFunc("/api/v1/client-managers/heartbeat", h.clientManagerHeartbeat)
mux.HandleFunc("/api/v1/game-client-bridge/companion/commands/claim", h.gameClientBridgeCompanionClaim)
mux.HandleFunc("/api/v1/game-client-bridge/companion/commands/{commandId}/ack", h.gameClientBridgeCompanionAck)
mux.HandleFunc("/api/v1/game-client-bridge/companion/commands/{commandId}/result", h.gameClientBridgeCompanionResult)
mux.HandleFunc("/api/v1/game-client-bridge/companion/snapshots", h.gameClientBridgeCompanionSnapshot)
mux.HandleFunc("/api/v1/game-client-bridge/companion/diagnostics", h.gameClientBridgeCompanionDiagnostics)
}
// productionCapacity godoc
// @Summary Get production capacity governance state
// @Description Returns bounded Run endpoint capacity and durable pressure counts visible to the current operator.
// @Tags production-operations
// @Produce json
// @Success 200 {object} dto.ProductionCapacitySummaryResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/production/capacity [get]
func (h *coreHandlers) productionCapacity(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
summary, err := h.core.GetProductionCapacityForSession(bearerToken(r))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ProductionCapacityFromDomain(summary))
}
// productionCapacityAdmission godoc
// @Summary Check production capacity admission
// @Description Evaluates endpoint heartbeat, capability, durable jobs, and bounded backlog pressure without dispatching work.
// @Tags production-operations
// @Accept json
// @Produce json
// @Param body body dto.CapacityAdmissionRequest true "Capacity admission request"
// @Success 200 {object} dto.CapacityAdmissionDecisionResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/production/capacity/admission [post]
func (h *coreHandlers) productionCapacityAdmission(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.CapacityAdmissionRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
decision, err := h.core.CheckCapacityAdmissionForSession(bearerToken(r), request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.CapacityDecisionFromDomain(decision))
}
// alerts godoc
// @Summary List durable production alerts
// @Description Lists alerts visible to the current operator with optional safe state/source/severity filters.
// @Tags production-operations
// @Produce json
// @Success 200 {object} dto.AlertListResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/alerts [get]
func (h *coreHandlers) alerts(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
alerts, err := h.core.ListAlertsForSession(bearerToken(r), domain.AlertFilter{State: domain.AlertState(r.URL.Query().Get("state")), SourceKind: r.URL.Query().Get("sourceKind"), SourceID: r.URL.Query().Get("sourceId"), Severity: domain.AlertSeverity(r.URL.Query().Get("severity"))})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AlertListFromDomain(alerts))
}
// alertAcknowledge godoc
// @Summary Acknowledge a durable alert
// @Description Persists acknowledgement actor, timestamp, and linked audit evidence for one alert.
// @Tags production-operations
// @Accept json
// @Produce json
// @Param id path string true "Alert ID"
// @Param body body dto.AlertAcknowledgeRequest true "Acknowledgement request"
// @Success 200 {object} dto.AlertResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/alerts/{id}/acknowledge [post]
func (h *coreHandlers) alertAcknowledge(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.AlertAcknowledgeRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
alert, err := h.core.AcknowledgeAlertForSession(bearerToken(r), domain.AlertAcknowledgeRequest{AlertID: r.PathValue("id"), Note: request.Note})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AlertFromDomain(alert))
}
// alertResolve godoc
// @Summary Resolve a durable alert
// @Description Resolves one alert with a safe operator note and linked audit evidence.
// @Tags production-operations
// @Accept json
// @Produce json
// @Param id path string true "Alert ID"
// @Param body body dto.AlertResolveRequest true "Resolution request"
// @Success 200 {object} dto.AlertResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/alerts/{id}/resolve [post]
func (h *coreHandlers) alertResolve(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.AlertResolveRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
alert, err := h.core.ResolveAlertForSession(bearerToken(r), domain.AlertResolveRequest{AlertID: r.PathValue("id"), Note: request.Note})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AlertFromDomain(alert))
}
// alertRetry godoc
// @Summary Retry one durable alert source
// @Description Retries only the bounded source represented by an alert and preserves idempotency.
// @Tags production-operations
// @Accept json
// @Produce json
// @Param id path string true "Alert ID"
// @Param body body dto.AlertRetryRequest true "Scoped retry request"
// @Success 202 {object} dto.AlertRetryResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/alerts/{id}/retry [post]
func (h *coreHandlers) alertRetry(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.AlertRetryRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.RetryAlertForSession(bearerToken(r), domain.AlertRetryRequest{AlertID: r.PathValue("id"), IdempotencyKey: request.IdempotencyKey})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.AlertRetryFromDomain(result))
}
// pluginLifecycles godoc
// @Summary List server-bound plugin lifecycle state
// @Description Lists durable plugin installation, desired/current state, compatibility, dependency, job, alert, and audit metadata.
// @Tags production-operations
// @Produce json
// @Success 200 {object} dto.PluginLifecycleListResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/plugin-lifecycles [get]
func (h *coreHandlers) pluginLifecycles(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
items, err := h.core.ListPluginLifecyclesForSession(bearerToken(r), domain.PluginLifecycleFilter{PluginID: r.URL.Query().Get("pluginId"), ServerInstanceID: r.URL.Query().Get("serverInstanceId"), CurrentState: domain.PluginLifecycleState(r.URL.Query().Get("currentState"))})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.PluginLifecycleListFromDomain(items))
}
// pluginLifecycleAction godoc
// @Summary Dispatch a platform-mediated plugin lifecycle action
// @Description Runs compatibility and capacity gates before creating one durable bounded Run job.
// @Tags production-operations
// @Accept json
// @Produce json
// @Param pluginId path string true "Plugin ID"
// @Param body body dto.PluginLifecycleActionRequest true "Plugin lifecycle action"
// @Success 202 {object} dto.PluginLifecycleActionResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/plugin-lifecycles/{pluginId}/actions [post]
func (h *coreHandlers) pluginLifecycleAction(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.PluginLifecycleActionRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.RunPluginLifecycleForSession(bearerToken(r), request.ToDomain(r.PathValue("pluginId")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.PluginLifecycleResultFromDomain(result))
}
// aiConfigDiffs godoc
// @Summary List reviewable AI config diffs
// @Description Lists persisted AI recommendations visible to the current operator without provider credentials or transport configuration.
// @Tags production-operations
// @Produce json
// @Success 200 {object} dto.AIConfigDiffListResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/ai/config-diffs [get]
func (h *coreHandlers) aiConfigDiffs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
items, err := h.core.ListAIConfigDiffsForSession(bearerToken(r), domain.AIConfigDiffFilter{ServerInstanceID: r.URL.Query().Get("serverInstanceId"), PluginID: r.URL.Query().Get("pluginId"), State: domain.AIConfigDiffState(r.URL.Query().Get("state"))})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AIConfigDiffListFromDomain(items))
}
// aiConfigDiffApprove godoc
// @Summary Approve one reviewable AI config diff
// @Description Revalidates actor/server/config revision fences before dispatching one bounded config write job.
// @Tags production-operations
// @Accept json
// @Produce json
// @Param id path string true "AI config diff ID"
// @Param body body dto.AIConfigDiffApprovalRequest true "AI config diff approval"
// @Success 202 {object} dto.AIConfigDiffApprovalResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/ai/config-diffs/{id}/approve [post]
func (h *coreHandlers) aiConfigDiffApprove(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.AIConfigDiffApprovalRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.ApproveAIConfigDiffForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.AIConfigDiffApprovalFromDomain(result))
}
// authRegister godoc
// @Summary Register a platform account
// @Description Creates a pending low-privilege platform account without granting platform administrator rights.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body dto.RegisterRequest true "Registration request"
// @Success 200 {object} dto.AuthSessionResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 409 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/auth/register [post]
func (h *coreHandlers) authRegister(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RegisterRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
session, err := h.core.RegisterUser(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
h.writeAuthSession(w, r, session)
}
// authLogin godoc
// @Summary Login to the platform
// @Description Authenticates an active platform user and returns a bearer session token.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body dto.LoginRequest true "Login request"
// @Success 200 {object} dto.AuthSessionResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/auth/login [post]
func (h *coreHandlers) authLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.LoginRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
session, err := h.core.LoginUser(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
h.writeAuthSession(w, r, session)
}
// authLogout godoc
// @Summary Logout of the platform
// @Description Invalidates the active bearer session token.
// @Tags auth
// @Produce json
// @Success 204
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/auth/logout [post]
func (h *coreHandlers) authLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
if err := h.core.LogoutUser(bearerToken(r)); err != nil {
writeServiceError(w, err)
return
}
h.clearSessionCookie(w, r)
w.WriteHeader(http.StatusNoContent)
}
// authRotate godoc
// @Summary Rotate the active platform session
// @Description Revokes the current bearer token and returns a new bounded session token.
// @Tags auth
// @Produce json
// @Success 200 {object} dto.AuthSessionResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/auth/rotate [post]
func (h *coreHandlers) authRotate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
session, err := h.core.RotateUserSession(bearerToken(r))
if err != nil {
writeServiceError(w, err)
return
}
h.writeAuthSession(w, r, session)
}
// currentUser godoc
// @Summary Get current platform user
// @Description Returns the authenticated current user's bounded identity, roles, profile, and theme preference.
// @Tags users
// @Produce json
// @Success 200 {object} dto.CurrentUserResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/users/current [get]
func (h *coreHandlers) currentUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
user, err := h.core.GetCurrentUser(bearerToken(r))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.CurrentUserFromDomain(user))
}
// currentUserProfile godoc
// @Summary Update current platform user profile
// @Description Updates bounded profile fields for the authenticated current user.
// @Tags users
// @Accept json
// @Produce json
// @Param body body dto.UserProfileBody true "Profile update request"
// @Success 200 {object} dto.CurrentUserResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/users/current/profile [put]
func (h *coreHandlers) currentUserProfile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
writeMethodNotAllowed(w, http.MethodPut)
return
}
request, err := decodeJSON[dto.UserProfileBody](r)
if err != nil {
writeDecodeError(w, err)
return
}
user, err := h.core.UpdateCurrentUserProfile(bearerToken(r), request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
if request.DisplayName != "" {
user.DisplayName = request.DisplayName
user, err = h.core.UpdateUser(user.ID, user)
if err != nil {
writeServiceError(w, err)
return
}
}
writeJSON(w, http.StatusOK, dto.CurrentUserFromDomain(user))
}
// currentUserTheme godoc
// @Summary Update current platform user theme
// @Description Persists the authenticated user's console theme preference.
// @Tags users
// @Accept json
// @Produce json
// @Param body body dto.UserThemePreferenceRequest true "Theme preference request"
// @Success 200 {object} dto.UserThemePreferenceResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/users/current/theme [put]
func (h *coreHandlers) currentUserTheme(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
writeMethodNotAllowed(w, http.MethodPut)
return
}
request, err := decodeJSON[dto.UserThemePreferenceRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
preference, err := h.core.UpdateCurrentUserTheme(bearerToken(r), request.ToDomain(""))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.UserThemePreferenceFromDomain(preference))
}
func bearerToken(r *http.Request) string {
const prefix = "Bearer "
header := r.Header.Get("Authorization")
if len(header) >= len(prefix) && header[:len(prefix)] == prefix {
return header[len(prefix):]
}
cookie, err := r.Cookie(platformSessionCookieName)
if err != nil {
return ""
}
return cookie.Value
}
// pluginBridgeAuthorize godoc
// @Summary Authorize plugin page bridge action
// @Description Evaluates one plugin bridge action against installed plugin manifest permissions without executing privileged work.
// @Tags plugin-bridge
// @Accept json
// @Produce json
// @Param body body dto.PluginBridgeAuthorizeRequest true "Plugin bridge authorization request"
// @Success 200 {object} dto.PluginBridgeAuthorizeResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/plugin-bridge/authorize [post]
func (h *coreHandlers) pluginBridgeAuthorize(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.PluginBridgeAuthorizeRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
var result domain.PluginBridgeAuthorization
if h.enforceAuthorization {
result, err = h.core.AuthorizePluginBridgeActionForSession(bearerToken(r), request.ToDomain())
} else {
result, err = h.core.AuthorizePluginBridgeAction(request.ToDomain())
}
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.PluginBridgeAuthorizeFromDomain(result))
}
// pluginBridgeExecute godoc
// @Summary Execute plugin page bridge action
// @Description Authorizes and executes one platform-mediated plugin page bridge action without exposing platform auth, run sockets, host paths, or provider credentials.
// @Tags plugin-bridge
// @Accept json
// @Produce json
// @Param body body dto.PluginBridgeExecuteRequest true "Plugin bridge execution request"
// @Success 200 {object} dto.PluginBridgeExecuteResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/plugin-bridge/execute [post]
func (h *coreHandlers) pluginBridgeExecute(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.PluginBridgeExecuteRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.ExecutePluginBridgeAction(bearerToken(r), request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.PluginBridgeExecuteFromDomain(result))
}
// users godoc
// @Summary Create or list users
// @Description Creates a platform user or lists platform users.
// @Tags users
// @Accept json
// @Produce json
// @Param body body dto.UserCreateRequest false "User create request"
// @Success 200 {object} dto.UserListResponse
// @Success 201 {object} dto.UserResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/users [get]
// @Router /api/v1/users [post]
func (h *coreHandlers) users(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requirePlatformAdmin(w, r); !ok {
return
}
switch r.Method {
case http.MethodGet:
users, err := h.core.ListUsers(domain.UserFilter{Status: domain.UserStatus(r.URL.Query().Get("status"))})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.UserListFromDomain(users))
case http.MethodPost:
request, err := decodeJSON[dto.UserCreateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
user, err := h.core.CreateUser(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.UserFromDomain(user))
default:
writeMethodNotAllowed(w, "GET, POST")
}
}
// userDetail godoc
// @Summary Get or update user
// @Description Returns or updates one platform user by ID.
// @Tags users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Param body body dto.UserUpdateRequest false "User update request"
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/users/{id} [get]
// @Router /api/v1/users/{id} [put]
func (h *coreHandlers) userDetail(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requirePlatformAdmin(w, r); !ok {
return
}
switch r.Method {
case http.MethodGet:
user, err := h.core.GetUser(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.UserFromDomain(user))
case http.MethodPut:
existing, err := h.core.GetUser(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
request, err := decodeJSON[dto.UserUpdateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
user, err := h.core.UpdateUser(r.PathValue("id"), request.ApplyTo(existing))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.UserFromDomain(user))
default:
writeMethodNotAllowed(w, "GET, PUT")
return
}
}
func (h *coreHandlers) requirePlatformAdmin(w http.ResponseWriter, r *http.Request) (domain.User, bool) {
user, err := h.core.GetCurrentUser(bearerToken(r))
if err != nil {
writeServiceError(w, err)
return domain.User{}, false
}
for _, role := range user.Roles {
switch role {
case "platform-admin", "admin":
return user, true
}
}
writeServiceError(w, service.ErrForbidden)
return domain.User{}, false
}
// aiProviders godoc
// @Summary Create or list AI providers
// @Description Creates or lists platform-managed AI provider metadata without raw keys.
// @Tags ai-providers
// @Accept json
// @Produce json
// @Param body body dto.AIProviderCreateRequest false "AI provider create request"
// @Success 200 {object} dto.AIProviderListResponse
// @Success 201 {object} dto.AIProviderResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/ai-providers [get]
// @Router /api/v1/ai-providers [post]
func (h *coreHandlers) aiProviders(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requirePlatformAdmin(w, r); !ok {
return
}
switch r.Method {
case http.MethodGet:
providers, err := h.core.ListAIProviders(domain.AIProviderFilter{
Kind: domain.AIProviderKind(r.URL.Query().Get("kind")),
Status: domain.AIProviderStatus(r.URL.Query().Get("status")),
})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AIProviderListFromDomain(providers))
case http.MethodPost:
request, err := decodeJSON[dto.AIProviderCreateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
provider, err := h.core.CreateAIProvider(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.AIProviderFromDomain(provider))
default:
writeMethodNotAllowed(w, "GET, POST")
}
}
// aiProviderDetail godoc
// @Summary Get AI provider
// @Description Returns one platform-managed AI provider by ID without raw key or base URL material.
// @Tags ai-providers
// @Produce json
// @Param id path string true "AI provider ID"
// @Success 200 {object} dto.AIProviderResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/ai-providers/{id} [get]
// @Router /api/v1/ai-providers/{id} [put]
func (h *coreHandlers) aiProviderDetail(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requirePlatformAdmin(w, r); !ok {
return
}
switch r.Method {
case http.MethodGet:
provider, err := h.core.GetAIProvider(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AIProviderFromDomain(provider))
case http.MethodPut:
existing, err := h.core.GetAIProvider(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
request, err := decodeJSON[dto.AIProviderUpdateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
update := request.ToDomain(r.PathValue("id"), existing.Status)
if strings.TrimSpace(update.BaseURL) == "" {
update.BaseURL = existing.BaseURL
}
if strings.TrimSpace(update.APIKeyRef) == "" {
update.APIKeyRef = existing.APIKeyRef
}
provider, err := h.core.UpdateAIProvider(r.PathValue("id"), update)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AIProviderFromDomain(provider))
default:
writeMethodNotAllowed(w, "GET, PUT")
}
}
// aiProviderStatus godoc
// @Summary Set AI provider status
// @Description Enables or disables one AI provider without exposing raw key material.
// @Tags ai-providers
// @Accept json
// @Produce json
// @Param id path string true "AI provider ID"
// @Param body body dto.AIProviderStatusRequest true "AI provider status request"
// @Success 200 {object} dto.AIProviderResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/ai-providers/{id}/status [post]
func (h *coreHandlers) aiProviderStatus(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requirePlatformAdmin(w, r); !ok {
return
}
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.AIProviderStatusRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
provider, err := h.core.SetAIProviderStatus(r.PathValue("id"), request.Status)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AIProviderFromDomain(provider))
}
// aiProviderTest godoc
// @Summary Test AI provider metadata
// @Description Performs local metadata validation for one AI provider without external network calls.
// @Tags ai-providers
// @Produce json
// @Param id path string true "AI provider ID"
// @Success 200 {object} dto.AIProviderTestResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/ai-providers/{id}/test [post]
func (h *coreHandlers) aiProviderTest(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requirePlatformAdmin(w, r); !ok {
return
}
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
result, err := h.core.TestAIProvider(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AIProviderTestFromDomain(result))
}
// aiProviderModels godoc
// @Summary List AI provider configured models
// @Description Returns configured model names for one AI provider without exposing credentials.
// @Tags ai-providers
// @Produce json
// @Param id path string true "AI provider ID"
// @Success 200 {object} dto.AIProviderModelsResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/ai-providers/{id}/models [get]
func (h *coreHandlers) aiProviderModels(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requirePlatformAdmin(w, r); !ok {
return
}
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
models, err := h.core.ListAIProviderModels(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AIProviderModelsFromDomain(models))
}
// aiInvocation godoc
// @Summary Invoke platform-mediated AI
// @Description Invokes AI through platform-owned provider metadata and returns redacted recommendations without exposing provider credentials.
// @Tags ai
// @Accept json
// @Produce json
// @Param body body dto.AIInvocationRequest true "AI invocation request"
// @Success 200 {object} dto.AIInvocationResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/ai/invocations [post]
func (h *coreHandlers) aiInvocation(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.AIInvocationRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
response, err := h.core.InvokeAIForSession(bearerToken(r), request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AIInvocationFromDomain(response))
}
func (h *coreHandlers) aiConfigSuggestion(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.LlmConfigSuggestionRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
response, err := h.core.InvokeAIForSession(bearerToken(r), domain.AIInvocationRequest{
RequestID: "config-suggestion:" + request.ServerInstanceID,
ServerInstanceID: request.ServerInstanceID,
Purpose: "config.suggest",
Prompt: request.Prompt,
CurrentConfig: request.CurrentConfig,
ContextRefs: map[string]string{"server": "server://" + request.ServerInstanceID},
})
if err != nil {
writeServiceError(w, err)
return
}
if response.Status != "ok" {
recommendation := "AI suggestion unavailable"
if response.Error != nil {
recommendation = response.Error.Message
}
writeJSON(w, http.StatusOK, dto.LlmConfigSuggestionResponse{ServerInstanceID: request.ServerInstanceID, Recommendation: recommendation})
return
}
suggested := ""
if response.ConfigRecommendation != nil {
suggested = response.ConfigRecommendation.SuggestedConfig
}
writeJSON(w, http.StatusOK, dto.LlmConfigSuggestionResponse{ServerInstanceID: request.ServerInstanceID, Recommendation: response.Recommendation, SuggestedConfig: suggested})
}
// gamePlugins godoc
// @Summary Create or list game management plugins
// @Description Creates or lists installed game management plugin metadata.
// @Tags game-plugins
// @Accept json
// @Produce json
// @Param body body dto.GamePluginCreateRequest false "Game plugin create request"
// @Success 200 {object} dto.GamePluginListResponse
// @Success 201 {object} dto.GamePluginResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/game-plugins [get]
// @Router /api/v1/game-plugins [post]
func (h *coreHandlers) gamePlugins(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
plugins, err := h.core.ListGamePlugins(domain.GamePluginFilter{
ServerType: r.URL.Query().Get("serverType"),
Status: domain.GamePluginStatus(r.URL.Query().Get("status")),
})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.GamePluginListFromDomain(plugins))
case http.MethodPost:
request, err := decodeJSON[dto.GamePluginCreateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
plugin, err := h.core.CreateGamePlugin(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.GamePluginFromDomain(plugin))
default:
writeMethodNotAllowed(w, "GET, POST")
}
}
// gamePluginManifestRegistration godoc
// @Summary Register game management plugin manifest
// @Description Validates and registers one game management plugin manifest as installed registry metadata, refreshing an existing manifest with the same ID in place.
// @Tags game-plugins
// @Accept json
// @Produce json
// @Param body body dto.GamePluginManifestRegistrationRequest true "Game plugin manifest registration request"
// @Success 201 {object} dto.GamePluginResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/game-plugins/register-manifest [post]
func (h *coreHandlers) gamePluginManifestRegistration(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.GamePluginManifestRegistrationRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
plugin, err := h.core.RegisterGamePluginManifest(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.GamePluginFromDomain(plugin))
}
// gamePluginDetail godoc
// @Summary Get game management plugin
// @Description Returns one game management plugin by ID.
// @Tags game-plugins
// @Produce json
// @Param id path string true "Game plugin ID"
// @Success 200 {object} dto.GamePluginResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/game-plugins/{id} [get]
func (h *coreHandlers) gamePluginDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
plugin, err := h.core.GetGamePlugin(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.GamePluginFromDomain(plugin))
}
// marketplacePlugins godoc
// @Summary List plugin marketplace entries
// @Description Lists marketplace plugin metadata projected from the platform registry without commerce data, host paths, run sockets, or raw credentials.
// @Tags plugin-marketplace
// @Produce json
// @Param status query string false "Plugin status"
// @Param serverType query string false "Server type"
// @Param capability query string false "Run or bridge capability"
// @Param keyword query string false "Keyword search"
// @Success 200 {object} dto.MarketplacePluginListResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/plugin-marketplace/plugins [get]
func (h *coreHandlers) marketplacePlugins(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
plugins, err := h.core.ListMarketplacePlugins(domain.PluginMarketplaceFilter{
ServerType: r.URL.Query().Get("serverType"),
Status: domain.GamePluginStatus(r.URL.Query().Get("status")),
Capability: r.URL.Query().Get("capability"),
Keyword: r.URL.Query().Get("keyword"),
})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.MarketplacePluginListFromDomain(plugins))
}
// marketplacePluginDetail godoc
// @Summary Get plugin marketplace detail
// @Description Returns one plugin marketplace entry with manifest-backed metadata and redacted platform-safe fields.
// @Tags plugin-marketplace
// @Produce json
// @Param id path string true "Plugin ID"
// @Success 200 {object} dto.MarketplacePluginResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/plugin-marketplace/plugins/{id} [get]
func (h *coreHandlers) marketplacePluginDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
plugin, err := h.core.GetMarketplacePlugin(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.MarketplacePluginFromDomain(plugin))
}
// marketplacePluginState godoc
// @Summary Change plugin marketplace state
// @Description Applies metadata-only install, enable, or disable state changes without package download or run execution.
// @Tags plugin-marketplace
// @Accept json
// @Produce json
// @Param id path string true "Plugin ID"
// @Param body body dto.MarketplacePluginStateRequest true "Marketplace state action"
// @Success 200 {object} dto.MarketplacePluginResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/plugin-marketplace/plugins/{id}/state [post]
func (h *coreHandlers) marketplacePluginState(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.MarketplacePluginStateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
plugin, err := h.core.SetMarketplacePluginState(r.PathValue("id"), request.Action)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.MarketplacePluginFromDomain(plugin))
}
// serverInstances godoc
// @Summary Create or list server instances
// @Description Creates or lists server instances linked to installed game plugins and run endpoints.
// @Tags server-instances
// @Accept json
// @Produce json
// @Param body body dto.ServerInstanceCreateRequest false "Server instance create request"
// @Success 200 {object} dto.ServerInstanceListResponse
// @Success 201 {object} dto.ServerInstanceResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances [get]
// @Router /api/v1/server-instances [post]
func (h *coreHandlers) serverInstances(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
instances, err := h.core.ListServerInstancesForSession(bearerToken(r), domain.ServerInstanceFilter{
PluginID: r.URL.Query().Get("pluginId"),
RunEndpointID: r.URL.Query().Get("runEndpointId"),
State: domain.ServerInstanceState(r.URL.Query().Get("state")),
})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerInstanceListFromDomain(instances))
case http.MethodPost:
request, err := decodeJSON[dto.ServerInstanceCreateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
instance, err := h.core.CreateServerInstanceForSession(bearerToken(r), request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.ServerInstanceFromDomain(instance))
default:
writeMethodNotAllowed(w, "GET, POST")
}
}
// serverInstanceDetail godoc
// @Summary Get, update, or delete server instance
// @Description Returns one server instance by ID, updates safe metadata, or deletes it by marking the instance deleted after safety validation and password confirmation. Delete requests send a JSON body with the current password and may include explicit forced-delete confirmation for running or installing instances.
// @Tags server-instances
// @Accept json
// @Produce json
// @Param id path string true "Server instance ID"
// @Param body body dto.ServerDeletionRequest false "Server metadata update or deletion request"
// @Success 204
// @Success 200 {object} dto.ServerInstanceResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id} [get]
// @Router /api/v1/server-instances/{id} [put]
// @Router /api/v1/server-instances/{id} [delete]
func (h *coreHandlers) serverInstanceDetail(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
instance, err := h.core.GetServerInstanceForSession(bearerToken(r), r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerInstanceFromDomain(instance))
case http.MethodPut:
request, err := decodeJSON[dto.ServerInstanceUpdateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
instance, err := h.core.UpdateServerInstanceForSession(bearerToken(r), r.PathValue("id"), request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerInstanceFromDomain(instance))
case http.MethodDelete:
request, err := decodeJSON[dto.ServerDeletionRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
_, err = h.core.DeleteServerInstanceForSession(bearerToken(r), r.PathValue("id"), request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
default:
writeMethodNotAllowed(w, http.MethodGet+", "+http.MethodPut+", "+http.MethodDelete)
}
}
// platformMetrics godoc
// @Summary Get platform resource usage metrics
// @Description Returns bounded platform resource usage derived from platform metadata without host paths or secrets.
// @Tags metrics
// @Produce json
// @Success 200 {object} dto.PlatformResourceUsageResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/metrics/platform [get]
func (h *coreHandlers) platformMetrics(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
if _, ok := h.requirePlatformAdmin(w, r); !ok {
return
}
usage, err := h.core.GetPlatformResourceUsage()
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.PlatformResourceUsageFromDomain(usage))
}
// serverInstanceMetrics godoc
// @Summary List visible server metrics
// @Description Returns bounded metrics for server instances visible to the authenticated user.
// @Tags metrics
// @Produce json
// @Success 200 {object} dto.ServerMetricsListResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/metrics/server-instances [get]
func (h *coreHandlers) serverInstanceMetrics(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
metrics, err := h.core.ListServerMetricsForSession(bearerToken(r))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerMetricsListFromDomain(metrics))
}
// serverInstanceConfig godoc
// @Summary Read server configuration
// @Description Returns logical server configuration content for an authorized server instance without exposing run internals.
// @Tags server-instances
// @Produce json
// @Param id path string true "Server instance ID"
// @Success 200 {object} dto.ServerConfigResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/config [get]
func (h *coreHandlers) serverInstanceConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
config, err := h.core.GetServerConfigForSession(bearerToken(r), r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerConfigFromDomain(config))
}
// serverDeclaredFileReadSnapshot godoc
// @Summary Read the latest declared file snapshot
// @Description Returns a redacted bounded result only for an authorized plugin-declared logical file key.
// @Tags server-instances
// @Produce json
// @Param id path string true "Server instance ID"
// @Param key query string true "Plugin-declared logical file key"
// @Success 200 {object} dto.DeclaredFileReadSnapshotResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/files/read-snapshot [get]
func (h *coreHandlers) serverDeclaredFileReadSnapshot(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
snapshot, err := h.core.GetDeclaredFileReadSnapshotForSession(bearerToken(r), r.PathValue("id"), r.URL.Query().Get("key"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.DeclaredFileReadSnapshotFromDomain(snapshot))
}
// serverInstanceConfigDiff godoc
// @Summary Preview server config diff
// @Description Compares current logical server config with proposed content without dispatching a write job.
// @Tags server-instances
// @Accept json
// @Produce json
// @Param id path string true "Server instance ID"
// @Param body body dto.ServerConfigDiffPreviewRequest true "Config diff preview request"
// @Success 200 {object} dto.ServerConfigDiffPreviewResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/config/diff [post]
func (h *coreHandlers) serverInstanceConfigDiff(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ServerConfigDiffPreviewRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
preview, err := h.core.PreviewServerConfigWriteForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerConfigDiffPreviewFromDomain(preview))
}
// serverInstanceConfigApprove godoc
// @Summary Approve server config write
// @Description Validates a reviewed config diff and queues a scoped config.write run job without exposing host paths or raw credentials.
// @Tags server-instances
// @Accept json
// @Produce json
// @Param id path string true "Server instance ID"
// @Param body body dto.ServerConfigWriteApprovalRequest true "Config write approval request"
// @Success 202 {object} dto.ServerConfigWriteDispatchResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/config/approve [post]
func (h *coreHandlers) serverInstanceConfigApprove(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ServerConfigWriteApprovalRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
dispatch, err := h.core.ApproveServerConfigWriteForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.ServerConfigWriteDispatchFromDomain(dispatch))
}
// fileOperationDispatch godoc
// @Summary Dispatch scoped file operation
// @Description Queues a scoped files.read or files.write job using logical file keys or refs, never raw host paths.
// @Tags file-operations
// @Accept json
// @Produce json
// @Param body body dto.FileOperationDispatchRequest true "File operation dispatch request"
// @Success 202 {object} dto.FileOperationDispatchResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/file-operations/dispatch [post]
func (h *coreHandlers) fileOperationDispatch(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.FileOperationDispatchRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.DispatchFileOperationForSession(bearerToken(r), request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.FileOperationDispatchFromDomain(result))
}
// serverAdministratorCandidates godoc
// @Summary List server administrator invite candidates
// @Description Returns active non-platform-admin users that the authenticated server owner can invite for one server.
// @Tags server-instances
// @Produce json
// @Param id path string true "Server instance ID"
// @Success 200 {object} dto.ServerMemberListResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/administrators/candidates [get]
func (h *coreHandlers) serverAdministratorCandidates(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
users, err := h.core.ListServerAdministratorCandidates(bearerToken(r), r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerMemberListFromDomain(users))
}
// serverAdministrators godoc
// @Summary Add server administrator
// @Description Adds an active non-platform-admin user as a server-scoped administrator when requested by the server owner.
// @Tags server-instances
// @Accept json
// @Produce json
// @Param id path string true "Server instance ID"
// @Param body body dto.ServerMemberRequest true "Server administrator request"
// @Success 200 {object} dto.ServerInstanceResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/administrators [post]
func (h *coreHandlers) serverAdministrators(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ServerMemberRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
instance, err := h.core.AddServerAdministrator(bearerToken(r), r.PathValue("id"), request.UserID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerInstanceFromDomain(instance))
}
// serverAdministratorDetail godoc
// @Summary Remove server administrator
// @Description Removes a server-scoped administrator from a server when requested by the server owner.
// @Tags server-instances
// @Produce json
// @Param id path string true "Server instance ID"
// @Param userId path string true "User ID"
// @Success 200 {object} dto.ServerInstanceResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/administrators/{userId} [delete]
func (h *coreHandlers) serverAdministratorDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
writeMethodNotAllowed(w, http.MethodDelete)
return
}
instance, err := h.core.RemoveServerAdministrator(bearerToken(r), r.PathValue("id"), r.PathValue("userId"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerInstanceFromDomain(instance))
}
// runControlHello godoc
// @Summary Register run control session
// @Description Accepts lightweight run hello metadata, creates or updates run endpoint metadata, and returns a platform-issued session token.
// @Tags run
// @Accept json
// @Produce json
// @Param body body dto.RunControlHelloRequest true "Run control hello request"
// @Success 200 {object} dto.RunControlHelloResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/control/hello [post]
func (h *coreHandlers) runControlHello(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunControlHelloRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.RegisterRunHello(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunControlHelloFromDomain(result))
}
// runControlHeartbeat godoc
// @Summary Accept run control heartbeat
// @Description Accepts lightweight run heartbeat metadata when the active platform-issued session token matches.
// @Tags run
// @Accept json
// @Produce json
// @Param body body dto.RunControlHeartbeatRequest true "Run control heartbeat request"
// @Success 200 {object} dto.RunControlHeartbeatResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/control/heartbeat [post]
func (h *coreHandlers) runControlHeartbeat(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunControlHeartbeatRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.AcceptRunHeartbeat(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunControlHeartbeatFromDomain(result))
}
// runJobClaim godoc
// @Summary Claim one run job
// @Description Lets a registered run endpoint claim one queued job assigned to it using the active session token.
// @Tags run-jobs
// @Accept json
// @Produce json
// @Param body body dto.RunJobClaimRequest true "Run job claim request"
// @Success 200 {object} dto.RunJobClaimResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/jobs/claim [post]
func (h *coreHandlers) runJobClaim(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunJobClaimRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.ClaimRunJob(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunJobClaimFromDomain(result))
}
// runJobAck godoc
// @Summary Acknowledge one run job
// @Description Lets a registered run endpoint acknowledge an active job lease before execution.
// @Tags run-jobs
// @Accept json
// @Produce json
// @Param body body dto.RunJobAckRequest true "Run job ack request"
// @Success 200 {object} dto.RunJobAckResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/jobs/ack [post]
func (h *coreHandlers) runJobAck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunJobAckRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.AckRunJob(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunJobAckFromDomain(result))
}
// runJobProgress godoc
// @Summary Update run job progress
// @Description Lets a registered run endpoint report bounded progress for an active job lease.
// @Tags run-jobs
// @Accept json
// @Produce json
// @Param body body dto.RunJobProgressRequest true "Run job progress request"
// @Success 200 {object} dto.RunJobProgressResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/jobs/progress [post]
func (h *coreHandlers) runJobProgress(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunJobProgressRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.UpdateRunJobProgress(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunJobProgressFromDomain(result))
}
// runJobResult godoc
// @Summary Complete run job
// @Description Lets a registered run endpoint submit a bounded terminal result for an active job lease.
// @Tags run-jobs
// @Accept json
// @Produce json
// @Param body body dto.RunJobResultRequest true "Run job result request"
// @Success 200 {object} dto.RunJobResultResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/jobs/result [post]
func (h *coreHandlers) runJobResult(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunJobResultRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.CompleteRunJob(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunJobResultFromDomain(result))
}
// runJobBuildInput returns secret-bearing build input only to the active leased run worker.
func (h *coreHandlers) runJobBuildInput(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.DistributionBuildInputRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.GetDistributionBuildInput(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.DistributionBuildInputFromDomain(result))
}
// runJobDependencyInput returns declared and resolved dependency input only to the active fenced Run attempt.
func (h *coreHandlers) runJobDependencyInput(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.DependencyExecutionInputRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.GetDependencyExecutionInput(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.DependencyExecutionInputFromDomain(result))
}
// runJobUpdateInput returns update metadata only to the active fenced Run attempt.
func (h *coreHandlers) runJobUpdateInput(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunUpdateInputRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.GetRunUpdateInput(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunUpdateInputFromDomain(result))
}
// runJobUpdateChunk serves one bounded update range only to the active fenced Run attempt.
func (h *coreHandlers) runJobUpdateChunk(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunUpdateChunkRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.ReadRunUpdateChunk(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunUpdateChunkFromDomain(result))
}
// runJobUpdateHealth godoc
// @Summary Confirm a reconciled Run self-update outcome
// @Description Accepts a signed current-session health or rollback report fenced to the terminal update job attempt.
// @Tags run-jobs
// @Accept json
// @Produce json
// @Param body body dto.RunUpdateHealthRequest true "Run update health report"
// @Success 200 {object} dto.RunUpdateHealthResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/jobs/update-health [post]
func (h *coreHandlers) runJobUpdateHealth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunUpdateHealthRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.ReportRunUpdateHealth(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunUpdateHealthFromDomain(result))
}
// runJobCancelPoll godoc
// @Summary Poll run job cancellation
// @Description Lets a registered run endpoint poll for cancellation requests on active leased jobs.
// @Tags run-jobs
// @Accept json
// @Produce json
// @Param body body dto.RunJobCancelPollRequest true "Run job cancel poll request"
// @Success 200 {object} dto.RunJobCancelPollResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/jobs/cancel [post]
func (h *coreHandlers) runJobCancelPoll(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunJobCancelPollRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.PollRunJobCancel(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunJobCancelPollFromDomain(result))
}
// runJobReconcile godoc
// @Summary Reconcile run jobs
// @Description Lets a registered run endpoint reconcile active platform jobs after restart or reconnect.
// @Tags run-jobs
// @Accept json
// @Produce json
// @Param body body dto.RunJobReconcileRequest true "Run job reconcile request"
// @Success 200 {object} dto.RunJobReconcileResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/jobs/reconcile [post]
func (h *coreHandlers) runJobReconcile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunJobReconcileRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.ReconcileRunJobs(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunJobReconcileFromDomain(result))
}
// runLogBatchIngest godoc
// @Summary Ingest run log batch
// @Description Accepts one bounded durable log batch from a registered run endpoint and returns an acknowledgement range.
// @Tags run-logs
// @Accept json
// @Produce json
// @Param body body dto.LogBatchIngestRequest true "Log batch ingest request"
// @Success 200 {object} dto.LogBatchIngestResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/logs/batches [post]
func (h *coreHandlers) runLogBatchIngest(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.LogBatchIngestRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.IngestLogBatch(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.LogBatchIngestFromDomain(result))
}
// runArtifactOpen godoc
// @Summary Open run artifact upload transfer
// @Description Lets a registered run endpoint open a resumable upload transfer for a scoped artifact owner.
// @Tags run-artifacts
// @Accept json
// @Produce json
// @Param body body dto.ArtifactTransferOpenRequest true "Artifact transfer open request"
// @Success 200 {object} dto.ArtifactTransferOpenResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/artifacts/open [post]
func (h *coreHandlers) runArtifactOpen(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ArtifactTransferOpenRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.OpenArtifactTransfer(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ArtifactTransferOpenFromDomain(result))
}
// runArtifactChunkUpload godoc
// @Summary Upload run artifact chunk
// @Description Accepts one bounded artifact chunk from a registered run endpoint and returns resumable acknowledgement state.
// @Tags run-artifacts
// @Accept json
// @Produce json
// @Param body body dto.ArtifactChunkUploadRequest true "Artifact chunk upload request"
// @Success 200 {object} dto.ArtifactChunkUploadResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/artifacts/chunks [post]
func (h *coreHandlers) runArtifactChunkUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ArtifactChunkUploadRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.UploadArtifactChunk(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ArtifactChunkUploadFromDomain(result))
}
// runArtifactStatus godoc
// @Summary Query run artifact upload status
// @Description Returns resumable chunk acknowledgement state for one artifact transfer.
// @Tags run-artifacts
// @Accept json
// @Produce json
// @Param body body dto.ArtifactTransferStatusRequest true "Artifact transfer status request"
// @Success 200 {object} dto.ArtifactTransferStatusResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/artifacts/status [post]
func (h *coreHandlers) runArtifactStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ArtifactTransferStatusRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.QueryArtifactTransferStatus(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ArtifactTransferStatusFromDomain(result))
}
// runArtifactComplete godoc
// @Summary Complete run artifact upload transfer
// @Description Marks an artifact available only after every chunk is present and final checksum validation passes.
// @Tags run-artifacts
// @Accept json
// @Produce json
// @Param body body dto.ArtifactTransferCompleteRequest true "Artifact transfer complete request"
// @Success 200 {object} dto.ArtifactTransferCompleteResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/artifacts/complete [post]
func (h *coreHandlers) runArtifactComplete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ArtifactTransferCompleteRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.CompleteArtifactTransfer(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ArtifactTransferCompleteFromDomain(result))
}
// runEndpoints godoc
// @Summary Create or list run endpoints
// @Description Creates or lists run endpoint metadata used by platform-mediated jobs.
// @Tags run
// @Accept json
// @Produce json
// @Param body body dto.RunEndpointCreateRequest false "Run endpoint create request"
// @Success 200 {object} dto.RunEndpointListResponse
// @Success 201 {object} dto.RunEndpointResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/endpoints [get]
// @Router /api/v1/run/endpoints [post]
func (h *coreHandlers) runEndpoints(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
endpoints, err := h.core.ListRunEndpoints(domain.RunEndpointFilter{Status: domain.RunEndpointStatus(r.URL.Query().Get("status"))})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunEndpointListFromDomain(endpoints))
case http.MethodPost:
request, err := decodeJSON[dto.RunEndpointCreateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
endpoint, err := h.core.CreateRunEndpoint(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.RunEndpointFromDomain(endpoint))
default:
writeMethodNotAllowed(w, "GET, POST")
}
}
// runEndpointDetail godoc
// @Summary Get run endpoint
// @Description Returns one run endpoint by ID.
// @Tags run
// @Produce json
// @Param id path string true "Run endpoint ID"
// @Success 200 {object} dto.RunEndpointResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/run/endpoints/{id} [get]
func (h *coreHandlers) runEndpointDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
endpoint, err := h.core.GetRunEndpoint(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunEndpointFromDomain(endpoint))
}
// jobs godoc
// @Summary Create or list jobs
// @Description Creates or lists platform job metadata.
// @Tags jobs
// @Accept json
// @Produce json
// @Param body body dto.JobCreateRequest false "Job create request"
// @Success 200 {object} dto.JobListResponse
// @Success 201 {object} dto.JobResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/jobs [get]
// @Router /api/v1/jobs [post]
func (h *coreHandlers) jobs(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
filter := domain.JobFilter{
ServerInstanceID: r.URL.Query().Get("serverInstanceId"),
RunEndpointID: r.URL.Query().Get("runEndpointId"),
State: domain.JobState(r.URL.Query().Get("state")),
}
var jobs []domain.Job
var err error
if h.enforceAuthorization {
jobs, err = h.core.ListJobsForSession(bearerToken(r), filter)
} else {
jobs, err = h.core.ListJobs(filter)
}
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.JobListFromDomain(jobs))
case http.MethodPost:
request, err := decodeJSON[dto.JobCreateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
job, err := h.core.CreateJob(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.JobFromDomain(job))
default:
writeMethodNotAllowed(w, "GET, POST")
}
}
// jobCancel godoc
// @Summary Request job cancellation
// @Description Records a cancellation request for an accepted or running job; run observes it through the job cancel poll route.
// @Tags jobs
// @Accept json
// @Produce json
// @Param id path string true "Job ID"
// @Param body body dto.RunJobCancelRequestBody true "Job cancel request"
// @Success 200 {object} dto.RunJobCancelRequestResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/jobs/{id}/cancel [post]
func (h *coreHandlers) jobCancel(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunJobCancelRequestBody](r)
if err != nil {
writeDecodeError(w, err)
return
}
request.JobID = r.PathValue("id")
var result domain.RunJobCancelRequestResult
if h.enforceAuthorization {
result, err = h.core.RequestRunJobCancelForSession(bearerToken(r), request.ToDomain())
} else {
result, err = h.core.RequestRunJobCancel(request.ToDomain())
}
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunJobCancelRequestFromDomain(result))
}
// jobDetail godoc
// @Summary Get job
// @Description Returns one platform job by ID.
// @Tags jobs
// @Produce json
// @Param id path string true "Job ID"
// @Success 200 {object} dto.JobResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/jobs/{id} [get]
func (h *coreHandlers) jobDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
var job domain.Job
var err error
if h.enforceAuthorization {
job, err = h.core.GetJobForSession(bearerToken(r), r.PathValue("id"))
} else {
job, err = h.core.GetJob(r.PathValue("id"))
}
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.JobFromDomain(job))
}
// artifacts godoc
// @Summary Create or list artifact metadata
// @Description Creates or lists artifact metadata; run chunk upload and browser content download use dedicated artifact transfer routes.
// @Tags artifacts
// @Accept json
// @Produce json
// @Param body body dto.ArtifactCreateRequest false "Artifact create request"
// @Success 200 {object} dto.ArtifactListResponse
// @Success 201 {object} dto.ArtifactResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/artifacts [get]
// @Router /api/v1/artifacts [post]
func (h *coreHandlers) artifacts(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
filter := domain.ArtifactFilter{
OwnerKind: domain.ArtifactOwnerKind(r.URL.Query().Get("ownerKind")),
OwnerID: r.URL.Query().Get("ownerId"),
State: domain.ArtifactState(r.URL.Query().Get("state")),
}
var artifacts []domain.Artifact
var err error
if h.enforceAuthorization {
artifacts, err = h.core.ListArtifactsForSession(bearerToken(r), filter)
} else {
artifacts, err = h.core.ListArtifacts(filter)
}
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ArtifactListFromDomain(artifacts))
case http.MethodPost:
request, err := decodeJSON[dto.ArtifactCreateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
artifact, err := h.core.CreateArtifact(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.ArtifactFromDomain(artifact))
default:
writeMethodNotAllowed(w, "GET, POST")
}
}
// artifactDetail godoc
// @Summary Get artifact metadata
// @Description Returns one artifact metadata record by ID.
// @Tags artifacts
// @Produce json
// @Param id path string true "Artifact ID"
// @Success 200 {object} dto.ArtifactResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/artifacts/{id} [get]
func (h *coreHandlers) artifactDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
artifact, err := h.core.GetArtifactForSession(bearerToken(r), r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ArtifactFromDomain(artifact))
}
// artifactDownload godoc
// @Summary Open a browser-safe artifact download reference
// @Description Returns a platform-owned artifact download reference without exposing storage paths, direct run sockets, or credentials.
// @Tags artifacts
// @Produce json
// @Param id path string true "Artifact ID"
// @Success 200 {object} dto.ArtifactDownloadReferenceResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/artifacts/{id}/download [post]
func (h *coreHandlers) artifactDownload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
reference, err := h.core.OpenArtifactDownloadForSession(bearerToken(r), domain.ArtifactDownloadReferenceRequest{ArtifactID: r.PathValue("id")})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ArtifactDownloadReferenceFromDomain(reference))
}
// artifactContent godoc
// @Summary Read bounded artifact content
// @Description Streams a bounded artifact byte range through platform authorization with safe integrity headers.
// @Tags artifacts
// @Produce octet-stream
// @Param id path string true "Artifact ID"
// @Param offset query int false "Byte offset"
// @Param limit query int false "Maximum bytes"
// @Success 200 {file} binary
// @Success 206 {file} binary
// @Failure 400 {object} dto.ErrorResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/artifacts/{id}/content [get]
func (h *coreHandlers) artifactContent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
offset, limit, err := artifactRangeRequest(r)
if err != nil {
writeServiceError(w, err)
return
}
content, err := h.core.ReadArtifactContentForSession(bearerToken(r), domain.ArtifactContentRequest{ArtifactID: r.PathValue("id"), Offset: offset, Limit: limit})
if err != nil {
writeServiceError(w, err)
return
}
w.Header().Set("Content-Type", content.ContentType)
w.Header().Set("Content-Disposition", "attachment; filename=\""+strings.ReplaceAll(content.Filename, "\"", "")+"\"")
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Length", strconv.FormatInt(content.SizeBytes, 10))
w.Header().Set("X-Artifact-Id", content.ArtifactID)
w.Header().Set("X-Artifact-Checksum", content.Checksum)
w.Header().Set("X-Artifact-Content-Checksum", content.ContentChecksum)
w.Header().Set("X-Artifact-Storage", content.StorageBehavior)
if content.Partial {
end := content.Offset + content.SizeBytes - 1
w.Header().Set("Content-Range", "bytes "+strconv.FormatInt(content.Offset, 10)+"-"+strconv.FormatInt(end, 10)+"/"+strconv.FormatInt(content.TotalSizeBytes, 10))
w.WriteHeader(http.StatusPartialContent)
} else {
w.WriteHeader(http.StatusOK)
}
_, _ = w.Write(content.Payload)
}
func artifactRangeRequest(r *http.Request) (int64, int, error) {
query := r.URL.Query()
offset, err := parseOptionalInt64(query.Get("offset"))
if err != nil {
return 0, 0, validator.ValidationError{Violations: []string{"offset must be a number"}}
}
limit64, err := parseOptionalInt64(query.Get("limit"))
if err != nil {
return 0, 0, validator.ValidationError{Violations: []string{"limit must be a number"}}
}
limit := int(limit64)
if rangeHeader := strings.TrimSpace(r.Header.Get("Range")); rangeHeader != "" {
rangeOffset, rangeLimit, ok := parseByteRange(rangeHeader)
if !ok {
return 0, 0, validator.ValidationError{Violations: []string{"range header is invalid"}}
}
offset = rangeOffset
limit = rangeLimit
}
return offset, limit, nil
}
func parseOptionalInt64(value string) (int64, error) {
if strings.TrimSpace(value) == "" {
return 0, nil
}
return strconv.ParseInt(value, 10, 64)
}
func parseByteRange(header string) (int64, int, bool) {
if !strings.HasPrefix(header, "bytes=") {
return 0, 0, false
}
parts := strings.Split(strings.TrimPrefix(header, "bytes="), "-")
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
return 0, 0, false
}
start, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return 0, 0, false
}
end, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil || end < start {
return 0, 0, false
}
length := end - start + 1
if length > int64(validator.MaxArtifactDownloadBytes) {
return 0, 0, false
}
return start, int(length), true
}
// logStreams godoc
// @Summary Create or list log stream metadata
// @Description Creates or lists log stream metadata; durable ingest and cursor query use dedicated log routes while browser tail transport remains future work.
// @Tags logs
// @Accept json
// @Produce json
// @Param body body dto.LogStreamCreateRequest false "Log stream create request"
// @Success 200 {object} dto.LogStreamListResponse
// @Success 201 {object} dto.LogStreamResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/log-streams [get]
// @Router /api/v1/log-streams [post]
func (h *coreHandlers) logStreams(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
filter := domain.LogStreamFilter{
ServerInstanceID: r.URL.Query().Get("serverInstanceId"),
StreamKey: r.URL.Query().Get("streamKey"),
}
var streams []domain.LogStream
var err error
if h.enforceAuthorization {
streams, err = h.core.ListLogStreamsForSession(bearerToken(r), filter)
} else {
streams, err = h.core.ListLogStreams(filter)
}
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.LogStreamListFromDomain(streams))
case http.MethodPost:
request, err := decodeJSON[dto.LogStreamCreateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
stream, err := h.core.CreateLogStream(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.LogStreamFromDomain(stream))
default:
writeMethodNotAllowed(w, "GET, POST")
}
}
// logStreamQuery godoc
// @Summary Query log stream entries
// @Description Returns bounded stored log entries after a stream sequence cursor.
// @Tags logs
// @Accept json
// @Produce json
// @Param body body dto.LogStreamCursorRequest true "Log stream cursor request"
// @Success 200 {object} dto.LogStreamCursorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/log-streams/query [post]
func (h *coreHandlers) logStreamQuery(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.LogStreamCursorRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
var result domain.LogStreamCursorResult
if h.enforceAuthorization {
result, err = h.core.QueryLogStreamForSession(bearerToken(r), request.ToDomain())
} else {
result, err = h.core.QueryLogStream(request.ToDomain())
}
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.LogStreamCursorFromDomain(result))
}
// logStreamDetail godoc
// @Summary Get log stream metadata
// @Description Returns one log stream metadata record by ID.
// @Tags logs
// @Produce json
// @Param id path string true "Log stream ID"
// @Success 200 {object} dto.LogStreamResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/log-streams/{id} [get]
func (h *coreHandlers) logStreamDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
var stream domain.LogStream
var err error
if h.enforceAuthorization {
stream, err = h.core.GetLogStreamForSession(bearerToken(r), r.PathValue("id"))
} else {
stream, err = h.core.GetLogStream(r.PathValue("id"))
}
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.LogStreamFromDomain(stream))
}
// auditEvents godoc
// @Summary Create or list audit events
// @Description Creates or lists audit event metadata.
// @Tags audit-events
// @Accept json
// @Produce json
// @Param body body dto.AuditEventCreateRequest false "Audit event create request"
// @Success 200 {object} dto.AuditEventListResponse
// @Success 201 {object} dto.AuditEventResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/audit-events [get]
// @Router /api/v1/audit-events [post]
func (h *coreHandlers) auditEvents(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
events, err := h.core.ListAuditEvents(domain.AuditEventFilter{
ActorID: r.URL.Query().Get("actorId"),
ResourceKind: r.URL.Query().Get("resourceKind"),
ResourceID: r.URL.Query().Get("resourceId"),
Result: domain.AuditResult(r.URL.Query().Get("result")),
})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AuditEventListFromDomain(events))
case http.MethodPost:
request, err := decodeJSON[dto.AuditEventCreateRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
event, err := h.core.CreateAuditEvent(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, dto.AuditEventFromDomain(event))
default:
writeMethodNotAllowed(w, "GET, POST")
}
}
// auditEventDetail godoc
// @Summary Get audit event
// @Description Returns one audit event by ID.
// @Tags audit-events
// @Produce json
// @Param id path string true "Audit event ID"
// @Success 200 {object} dto.AuditEventResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/audit-events/{id} [get]
func (h *coreHandlers) auditEventDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
event, err := h.core.GetAuditEvent(r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.AuditEventFromDomain(event))
}