2687 lines
97 KiB
Go
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))
|
|
}
|