package api import ( "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" "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/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}/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}/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}/plugin-data/{collection}", h.serverPluginDataCollection) mux.HandleFunc("/api/v1/server-instances/{id}/plugin-data/{collection}/transaction", h.serverPluginDataTransaction) 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/events", h.serverLogEvents) mux.HandleFunc("/api/v1/server-instances/{id}/files/workspace", h.serverFilesWorkspace) mux.HandleFunc("/api/v1/server-instances/{id}/files/list", h.serverFilesList) mux.HandleFunc("/api/v1/server-instances/{id}/files/browse", h.serverFilesBrowse) mux.HandleFunc("/api/v1/server-instances/{id}/files/refresh", h.serverFilesRefresh) mux.HandleFunc("/api/v1/server-instances/{id}/files/read-snapshot", h.serverFilesReadSnapshot) mux.HandleFunc("/api/v1/server-instances/{id}/files/read", h.serverFilesRead) mux.HandleFunc("/api/v1/server-instances/{id}/files/write", h.serverFilesWrite) mux.HandleFunc("/api/v1/server-instances/{id}/files/upload", h.serverFilesUpload) mux.HandleFunc("/api/v1/server-instances/{id}/files/download", h.serverFilesDownload) mux.HandleFunc("/api/v1/server-instances/{id}/rcon/commands", h.sourceRCONCommands) 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/control/events", h.requireRunSignature(h.runControlEvents)) mux.HandleFunc("/api/v1/run/lifecycle/report", h.requireRunSignature(h.runLifecycleReport)) 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/update-input", h.requireRunSignature(h.runJobUpdateInput)) mux.HandleFunc("/api/v1/run/jobs/update-chunk", h.requireRunSignature(h.runJobUpdateChunk)) mux.HandleFunc("/api/v1/run/files/input-chunk", h.requireRunSignature(h.runFileInputChunk)) mux.HandleFunc("/api/v1/run/jobs/update-health", h.requireRunSignature(h.runJobUpdateHealth)) 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/logs/progress", h.requireRunSignature(h.runLogStreamProgress)) 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) } // pluginLifecycles godoc // @Summary List server-bound plugin lifecycle state // @Description Lists durable plugin installation, desired/current state, compatibility, dependency, and job metadata. // @Tags plugin-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 Validates manifest compatibility and creates one durable bounded Run job. // @Tags plugin-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 plugin-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 plugin-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 } if violations := request.RuntimeProfiles.UnsupportedLegacyProfileViolations("runtimeProfiles"); len(violations) > 0 { writeServiceError(w, validator.ValidationError{Violations: violations}) 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 } if violations := request.Manifest.RuntimeProfiles.UnsupportedLegacyProfileViolations("manifest.runtimeProfiles"); len(violations) > 0 { writeServiceError(w, validator.ValidationError{Violations: violations}) 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 is kept as legacy service plumbing but is not registered as a browser product route. 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 is kept as legacy service plumbing but is not registered as a browser product route. 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 is kept as legacy service plumbing but is not registered as a browser product route. 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 is kept as legacy service plumbing but is not registered as a browser product route. 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)) } // serverFilesWorkspace godoc // @Summary Read server file workspace // @Description Returns the generic server file manager root and transfer policy without exposing host paths. // @Tags server-files // @Produce json // @Param id path string true "Server instance ID" // @Success 200 {object} dto.ServerFileWorkspaceResponse // @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/workspace [get] func (h *coreHandlers) serverFilesWorkspace(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeMethodNotAllowed(w, http.MethodGet) return } workspace, err := h.core.GetServerFileWorkspaceForSession(bearerToken(r), r.PathValue("id")) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, dto.ServerFileWorkspaceFromDomain(workspace)) } func (h *coreHandlers) serverFilesList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeMethodNotAllowed(w, http.MethodGet) return } directoryKey, err := h.serverFileDirectoryKey(r, r.URL.Query().Get("directoryKey")) if err != nil { writeServiceError(w, err) return } result, err := h.core.ListServerFilesForSession(bearerToken(r), domain.ServerFileListRequest{ServerInstanceID: r.PathValue("id"), DirectoryKey: directoryKey, Path: r.URL.Query().Get("path"), Query: r.URL.Query().Get("query"), Recursive: r.URL.Query().Get("recursive") == "true"}) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, dto.ServerFileListFromDomain(result)) } // serverFilesBrowse godoc // @Summary Browse one live server directory // @Description Dispatches a fresh Run files.list job for the requested logical directory and briefly waits for the matching result without exposing host paths or stale cached listings. // @Tags server-files // @Accept json // @Produce json // @Param id path string true "Server instance ID" // @Param body body dto.ServerFileListRequest true "Server file browse request" // @Success 202 {object} dto.ServerFileListResponse // @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/browse [post] func (h *coreHandlers) serverFilesBrowse(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } request, err := decodeJSON[dto.ServerFileListRequest](r) if err != nil { writeDecodeError(w, err) return } request.DirectoryKey, err = h.serverFileDirectoryKey(r, request.DirectoryKey) if err != nil { writeServiceError(w, err) return } result, err := h.core.BrowseServerFilesForSession(r.Context(), bearerToken(r), request.ToDomain(r.PathValue("id"))) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusAccepted, dto.ServerFileListFromDomain(result)) } func (h *coreHandlers) serverFilesRefresh(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } request, err := decodeJSON[dto.ServerFileListRequest](r) if err != nil { writeDecodeError(w, err) return } request.DirectoryKey, err = h.serverFileDirectoryKey(r, request.DirectoryKey) if err != nil { writeServiceError(w, err) return } result, err := h.core.RefreshServerFileListForSession(bearerToken(r), request.ToDomain(r.PathValue("id"))) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusAccepted, dto.ServerFileListFromDomain(result)) } func (h *coreHandlers) serverFilesReadSnapshot(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)) } func (h *coreHandlers) serverFilesRead(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } request, err := decodeJSON[dto.ServerFileReadRequest](r) if err != nil { writeDecodeError(w, err) return } result, err := h.core.ReadServerFileForSession(bearerToken(r), request.ToDomain(r.PathValue("id"))) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusAccepted, dto.FileOperationDispatchFromDomain(result)) } func (h *coreHandlers) serverFilesWrite(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } request, err := decodeJSON[dto.ServerFileWriteRequest](r) if err != nil { writeDecodeError(w, err) return } result, err := h.core.WriteServerFileForSession(bearerToken(r), request.ToDomain(r.PathValue("id"))) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusAccepted, dto.FileOperationDispatchFromDomain(result)) } func (h *coreHandlers) serverFilesUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } upload, err := h.decodeServerFileUpload(r) if err != nil { writeServiceError(w, err) return } result, err := h.core.UploadServerFileForSession(bearerToken(r), upload) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusAccepted, dto.ServerFileUploadFromDomain(result)) } func (h *coreHandlers) serverFilesDownload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } request, err := decodeJSON[dto.ServerFileDownloadRequest](r) if err != nil { writeDecodeError(w, err) return } result, err := h.core.PrepareServerFileDownloadForSession(bearerToken(r), request.ToDomain(r.PathValue("id"))) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusAccepted, dto.ServerFileDownloadFromDomain(result)) } func (h *coreHandlers) serverFileDirectoryKey(r *http.Request, requested string) (string, error) { requested = strings.TrimSpace(requested) if requested != "" { return requested, nil } workspace, err := h.core.GetServerFileWorkspaceForSession(bearerToken(r), r.PathValue("id")) if err != nil { return "", err } return workspace.DefaultDirectoryKey, nil } func (h *coreHandlers) decodeServerFileUpload(r *http.Request) (domain.ServerFileUploadRequest, error) { if err := r.ParseMultipartForm(8 * 1024 * 1024); err != nil { return domain.ServerFileUploadRequest{}, validator.ValidationError{Violations: []string{"multipart upload is invalid"}} } file, header, err := r.FormFile("file") if err != nil { return domain.ServerFileUploadRequest{}, validator.ValidationError{Violations: []string{"file is required"}} } defer file.Close() payload, err := io.ReadAll(io.LimitReader(file, validator.MaxArtifactBytes+1)) if err != nil { return domain.ServerFileUploadRequest{}, err } if int64(len(payload)) > validator.MaxArtifactBytes { return domain.ServerFileUploadRequest{}, validator.ValidationError{Violations: []string{"payload is too large"}} } checksum := strings.TrimSpace(r.FormValue("checksum")) if checksum == "" { checksum = validator.BytesChecksum(payload) } filename := strings.TrimSpace(r.FormValue("filename")) if filename == "" && header != nil { filename = header.Filename } return domain.ServerFileUploadRequest{ServerInstanceID: r.PathValue("id"), DirectoryKey: r.FormValue("directoryKey"), RelativePath: r.FormValue("relativePath"), Filename: filename, Payload: payload, Checksum: checksum, IdempotencyKey: r.FormValue("idempotencyKey")}, nil } // 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)) } // runControlEvents godoc // @Summary Stream lightweight Run control events // @Description Opens a signed Run-only event stream for wakeups such as queued job changes. Events never carry job payloads, logs, artifacts, host paths, or credentials. // @Tags run // @Accept json // @Produce text/event-stream // @Param body body dto.RunControlStreamRequest true "Run control stream request" // @Success 200 {object} dto.RunControlEventResponse // @Failure 400 {object} dto.ErrorResponse // @Failure 401 {object} dto.ErrorResponse // @Failure 405 {object} dto.ErrorResponse // @Router /api/v1/run/control/events [post] func (h *coreHandlers) runControlEvents(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } request, err := decodeJSON[dto.RunControlStreamRequest](r) if err != nil { writeDecodeError(w, err) return } subscription, err := h.core.SubscribeRunControlEvents(request.ToDomain()) if err != nil { writeServiceError(w, err) return } defer subscription.Cancel() flusher, ok := w.(http.Flusher) if !ok { writeServiceError(w, validator.ValidationError{Violations: []string{"streaming response is unavailable"}}) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") lastSeq := request.LastEventSeq ready := dto.RunControlEventResponse{RunEndpointID: request.RunEndpointID, Sequence: lastSeq, Type: domain.RunControlEventTypeReady, ServerTime: time.Now().UTC(), RetrySeconds: 5} if err := writeRunControlSSE(w, flusher, ready); err != nil { return } if subscription.Initial != nil { event := dto.RunControlEventFromDomain(*subscription.Initial) if event.Sequence > lastSeq { lastSeq = event.Sequence } if err := writeRunControlSSE(w, flusher, event); err != nil { return } } heartbeatTicker := time.NewTicker(15 * time.Second) defer heartbeatTicker.Stop() for { select { case <-r.Context().Done(): return case event, ok := <-subscription.Events: if !ok { return } response := dto.RunControlEventFromDomain(event) if response.Sequence > lastSeq { lastSeq = response.Sequence } if err := writeRunControlSSE(w, flusher, response); err != nil { return } case <-heartbeatTicker.C: heartbeat := dto.RunControlEventResponse{RunEndpointID: request.RunEndpointID, Sequence: lastSeq, Type: domain.RunControlEventTypeHeartbeat, ServerTime: time.Now().UTC(), RetrySeconds: 5} if err := writeRunControlSSE(w, flusher, heartbeat); err != nil { return } } } } func writeRunControlSSE(w http.ResponseWriter, flusher http.Flusher, event dto.RunControlEventResponse) error { data, err := json.Marshal(event) if err != nil { return err } if event.Type != "" { if _, err := fmt.Fprintf(w, "event: %s\n", event.Type); err != nil { return err } } if _, err := fmt.Fprintf(w, "id: %d\n", event.Sequence); err != nil { return err } if event.RetrySeconds > 0 { if _, err := fmt.Fprintf(w, "retry: %d\n", event.RetrySeconds*1000); err != nil { return err } } if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil { return err } flusher.Flush() return nil } // runLifecycleReport godoc // @Summary Report autonomous run lifecycle result // @Description Lets a registered run endpoint report an observed lifecycle terminal result without a platform-assigned job lease. // @Tags run // @Accept json // @Produce json // @Param body body dto.RunLifecycleReportRequest true "Run lifecycle report request" // @Success 200 {object} dto.RunLifecycleReportResponse // @Failure 400 {object} dto.ErrorResponse // @Failure 405 {object} dto.ErrorResponse // @Router /api/v1/run/lifecycle/report [post] func (h *coreHandlers) runLifecycleReport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } request, err := decodeJSON[dto.RunLifecycleReportRequest](r) if err != nil { writeDecodeError(w, err) return } result, err := h.core.ReportRunLifecycle(request.ToDomain()) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, dto.RunLifecycleReportFromDomain(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.ClaimRunJobWithWait(r.Context(), 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)) } // runFileInputChunk serves one bounded browser-uploaded file input range only to the active fenced Run attempt. func (h *coreHandlers) runFileInputChunk(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } request, err := decodeJSON[dto.RunFileInputChunkRequest](r) if err != nil { writeDecodeError(w, err) return } result, err := h.core.ReadRunFileInputChunk(request.ToDomain()) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, dto.RunFileInputChunkFromDomain(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)) } func (h *coreHandlers) runLogStreamProgress(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeMethodNotAllowed(w, http.MethodPost) return } request, err := decodeJSON[dto.RunLogStreamProgressRequest](r) if err != nil { writeDecodeError(w, err) return } result, err := h.core.GetRunLogStreamProgress(request.ToDomain()) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, dto.RunLogStreamProgressFromDomain(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 octet-stream // @Produce json // @Param X-Artifact-Transfer-Id header string true "Artifact transfer ID" // @Param X-Artifact-Id header string true "Artifact ID" // @Param X-Artifact-Chunk-Index header int true "Chunk index" // @Param X-Artifact-Offset header int true "Byte offset" // @Param X-Artifact-Size header int true "Chunk size" // @Param X-Artifact-Checksum header string true "Chunk sha256 checksum" // @Param body body file true "Artifact chunk bytes" // @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 := decodeArtifactChunkUploadRequest(r) if err != nil { writeServiceError(w, err) return } result, err := h.core.UploadArtifactChunk(request.ToDomain()) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, dto.ArtifactChunkUploadFromDomain(result)) } const ( artifactTransferIDHeader = "X-Artifact-Transfer-Id" artifactIDHeader = "X-Artifact-Id" artifactChunkIndexHeader = "X-Artifact-Chunk-Index" artifactChunkOffsetHeader = "X-Artifact-Offset" artifactChunkSizeHeader = "X-Artifact-Size" artifactChunkHashHeader = "X-Artifact-Checksum" ) func decodeArtifactChunkUploadRequest(r *http.Request) (dto.ArtifactChunkUploadRequest, error) { if !isOctetStream(r.Header.Get("Content-Type")) { return dto.ArtifactChunkUploadRequest{}, validator.ValidationError{Violations: []string{"contentType must be application/octet-stream"}} } chunkIndex, err := parseRequiredIntHeader(r, artifactChunkIndexHeader) if err != nil { return dto.ArtifactChunkUploadRequest{}, err } offset, err := parseRequiredInt64Header(r, artifactChunkOffsetHeader) if err != nil { return dto.ArtifactChunkUploadRequest{}, err } sizeBytes, err := parseRequiredIntHeader(r, artifactChunkSizeHeader) if err != nil { return dto.ArtifactChunkUploadRequest{}, err } if sizeBytes <= 0 || sizeBytes > validator.MaxArtifactChunkBytes { return dto.ArtifactChunkUploadRequest{}, validator.ValidationError{Violations: []string{fmt.Sprintf("%s must be between 1 and %d", artifactChunkSizeHeader, validator.MaxArtifactChunkBytes)}} } payload, err := io.ReadAll(io.LimitReader(r.Body, int64(validator.MaxArtifactChunkBytes)+1)) if err != nil { return dto.ArtifactChunkUploadRequest{}, err } if len(payload) != sizeBytes { return dto.ArtifactChunkUploadRequest{}, validator.ValidationError{Violations: []string{"request body size must match artifact chunk size"}} } return dto.ArtifactChunkUploadRequest{ RunEndpointID: strings.TrimSpace(r.Header.Get(runEndpointHeader)), SessionToken: strings.TrimSpace(r.Header.Get(runSessionTokenHeader)), TransferID: strings.TrimSpace(r.Header.Get(artifactTransferIDHeader)), ArtifactID: strings.TrimSpace(r.Header.Get(artifactIDHeader)), ChunkIndex: chunkIndex, Offset: offset, SizeBytes: sizeBytes, Checksum: strings.TrimSpace(r.Header.Get(artifactChunkHashHeader)), Payload: payload, }, nil } func parseRequiredIntHeader(r *http.Request, name string) (int, error) { value := strings.TrimSpace(r.Header.Get(name)) if value == "" { return 0, validator.ValidationError{Violations: []string{name + " is required"}} } parsed, err := strconv.Atoi(value) if err != nil { return 0, validator.ValidationError{Violations: []string{name + " must be a number"}} } return parsed, nil } func parseRequiredInt64Header(r *http.Request, name string) (int64, error) { value := strings.TrimSpace(r.Header.Get(name)) if value == "" { return 0, validator.ValidationError{Violations: []string{name + " is required"}} } parsed, err := strconv.ParseInt(value, 10, 64) if err != nil { return 0, validator.ValidationError{Violations: []string{name + " must be a number"}} } return parsed, nil } // 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")), States: parseJobStates(r.URL.Query().Get("states")), } 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") } } func parseJobStates(raw string) []domain.JobState { if strings.TrimSpace(raw) == "" { return nil } states := []domain.JobState{} for _, part := range strings.Split(raw, ",") { state := domain.JobState(strings.TrimSpace(part)) if state != "" { states = append(states, state) } } return states } // 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 } stream, err := h.core.OpenArtifactContentStreamForSession(bearerToken(r), domain.ArtifactContentRequest{ArtifactID: r.PathValue("id"), Offset: offset, Limit: limit}) if err != nil { writeServiceError(w, err) return } defer stream.Body.Close() content := stream.Content 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) } _, _ = io.Copy(w, stream.Body) } 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)) }