Remove legacy client-manager platform path
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"browser.local/platform/dto"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/service"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
func TestAuthorizedRouterEnforcesAdminAndCrossOwnerBoundaries(t *testing.T) {
|
||||
@@ -165,6 +166,71 @@ func TestRunHTTPEnvelopeRequiresValidSignatureAndRejectsReplay(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedRunArtifactChunkUploadUsesHeaderEnvelope(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
core := service.NewCoreService(store)
|
||||
if _, err := core.CreateGamePlugin(validGamePluginRequest().ToDomain()); err != nil {
|
||||
t.Fatalf("create plugin: %v", err)
|
||||
}
|
||||
if _, err := core.CreateRunEndpoint(validRunEndpointRequest().ToDomain()); err != nil {
|
||||
t.Fatalf("create endpoint: %v", err)
|
||||
}
|
||||
if _, err := core.CreateServerInstance(domain.ServerInstance{ID: "server-signed-artifact", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Signed Artifact", State: domain.ServerInstanceStateReady, ConfigVersion: 1}); err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
if _, err := core.CreateJob(domain.Job{ID: "job-signed-artifact", ServerInstanceID: "server-signed-artifact", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "job-signed-artifact"}); err != nil {
|
||||
t.Fatalf("create job: %v", err)
|
||||
}
|
||||
token := "run-artifact-session-secret"
|
||||
stamp := time.Now().UTC()
|
||||
hash := sha256.Sum256([]byte(token))
|
||||
if err := store.RunControlSessions().Create(domain.RunControlSession{RunEndpointID: "run-local", SessionTokenHash: hex.EncodeToString(hash[:]), Status: domain.AuthSessionStatusActive, Generation: 1, CapabilityFingerprint: "cap-v1", HeartbeatIntervalSeconds: 15, CreatedAt: stamp, UpdatedAt: stamp, ExpiresAt: stamp.Add(time.Hour), RequireSignedRequests: true}); err != nil {
|
||||
t.Fatalf("create Run session: %v", err)
|
||||
}
|
||||
payload := []byte("signed raw artifact chunk")
|
||||
opened, err := core.OpenArtifactTransfer(domain.ArtifactTransferOpen{RunEndpointID: "run-local", SessionToken: token, ArtifactID: "artifact-signed-raw", Direction: domain.ArtifactTransferDirectionUpload, OwnerKind: domain.ArtifactOwnerKindJob, OwnerID: "job-signed-artifact", SizeBytes: int64(len(payload)), ChunkSizeBytes: len(payload), Checksum: validator.BytesChecksum(payload), IdempotencyKey: "signed-raw-artifact"})
|
||||
if err != nil {
|
||||
t.Fatalf("open transfer: %v", err)
|
||||
}
|
||||
router := NewTestRouterWithCore(core)
|
||||
unsigned := rawArtifactChunkRequest(t, router, token, opened.TransferID, payload, "nonce-unsigned-raw", stamp, false)
|
||||
assertErrorResponse(t, unsigned, http.StatusUnauthorized, errorCodeUnauthorized)
|
||||
signed := rawArtifactChunkRequest(t, router, token, opened.TransferID, payload, "nonce-signed-raw", stamp, true)
|
||||
assertStatus(t, signed, http.StatusOK)
|
||||
response := decodeBody[dto.ArtifactChunkUploadResponse](t, signed)
|
||||
if !response.Accepted || response.TransferID != opened.TransferID || response.NextMissingChunkIndex != 1 {
|
||||
t.Fatalf("unexpected signed chunk response: %+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func rawArtifactChunkRequest(t *testing.T, router http.Handler, token string, transferID string, payload []byte, nonce string, stamp time.Time, signed bool) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
path := "/api/v1/run/artifacts/chunks"
|
||||
timestamp := strconv.FormatInt(stamp.Unix(), 10)
|
||||
req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("X-Run-Endpoint", "run-local")
|
||||
req.Header.Set("X-Run-Session-Token", token)
|
||||
req.Header.Set("X-Run-Timestamp", timestamp)
|
||||
req.Header.Set("X-Run-Nonce", nonce)
|
||||
req.Header.Set("X-Artifact-Transfer-Id", transferID)
|
||||
req.Header.Set("X-Artifact-Id", "artifact-signed-raw")
|
||||
req.Header.Set("X-Artifact-Chunk-Index", "0")
|
||||
req.Header.Set("X-Artifact-Offset", "0")
|
||||
req.Header.Set("X-Artifact-Size", strconv.Itoa(len(payload)))
|
||||
req.Header.Set("X-Artifact-Checksum", validator.BytesChecksum(payload))
|
||||
if signed {
|
||||
bodyHash := sha256.Sum256(payload)
|
||||
canonical := strings.Join([]string{http.MethodPost, path, timestamp, nonce, hex.EncodeToString(bodyHash[:])}, "\n")
|
||||
mac := hmac.New(sha256.New, []byte(token))
|
||||
_, _ = mac.Write([]byte(canonical))
|
||||
req.Header.Set("X-Run-Signature", hex.EncodeToString(mac.Sum(nil)))
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, req)
|
||||
return recorder
|
||||
}
|
||||
|
||||
func TestAuthorizedRouterAllowsRunLifecycleReportWithoutBearer(t *testing.T) {
|
||||
store := repo.NewMemoryStore()
|
||||
core := service.NewCoreService(store)
|
||||
|
||||
@@ -17,7 +17,6 @@ type componentLogServerContextKey struct{}
|
||||
|
||||
const (
|
||||
logEventHeartbeatInterval = 15 * time.Second
|
||||
liveLogSourceClockSkew = 90 * time.Second
|
||||
managedLogSessionIDPrefix = "log-session:"
|
||||
)
|
||||
|
||||
@@ -27,7 +26,6 @@ func (h *coreHandlers) serverLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
liveBoundary := time.Now().UTC().Add(-liveLogSourceClockSkew)
|
||||
instance, streams, liveEligible, subscription, err := h.openLogEventSubscription(r)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
@@ -150,12 +148,6 @@ func (h *coreHandlers) serverLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !sourceLogEntryIsLive(event.Entry, liveBoundary) {
|
||||
if event.Entry.Seq > emittedThrough[event.Stream.ID] {
|
||||
emittedThrough[event.Stream.ID] = event.Entry.Seq
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !active.hasStream(event.Stream.ID) {
|
||||
active.streams = append(active.streams, event.Stream)
|
||||
if err := writeSSEJSON(w, "stream", "", dto.LogStreamFromDomain(event.Stream)); err != nil {
|
||||
@@ -179,10 +171,6 @@ func (h *coreHandlers) serverLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func sourceLogEntryIsLive(entry domain.LogEntry, liveBoundary time.Time) bool {
|
||||
return !entry.Timestamp.IsZero() && !entry.Timestamp.Before(liveBoundary)
|
||||
}
|
||||
|
||||
type supervisedLogSession struct {
|
||||
sessionID string
|
||||
startedAt time.Time
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestLogEventsSSELiveOnlyStartsAfterSnapshotTail(t *testing.T) {
|
||||
assertSSEEvent(t, reader, "log", `"seq":2`)
|
||||
}
|
||||
|
||||
func TestLogEventsSSESkipsBufferedBackfillAfterOpen(t *testing.T) {
|
||||
func TestLogEventsSSEStreamsBufferedAppendAfterOpen(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
hello := createLogIngestAPIFixtures(t, router)
|
||||
assertStatus(t, performJSON(t, router, http.MethodPost, "/api/v1/run/logs/batches", validLogBatchRequest(t, hello.SessionToken, 1, 1)), http.StatusOK)
|
||||
@@ -180,11 +180,12 @@ func TestLogEventsSSESkipsBufferedBackfillAfterOpen(t *testing.T) {
|
||||
assertSSEEvent(t, reader, "stream", `"id":"log-1"`)
|
||||
assertSSEEvent(t, reader, "ready", `"streamCount":1`)
|
||||
|
||||
stale := validLogBatchRequest(t, hello.SessionToken, 2, 2)
|
||||
assertStatus(t, performJSON(t, router, http.MethodPost, "/api/v1/run/logs/batches", stale), http.StatusOK)
|
||||
buffered := validLogBatchRequest(t, hello.SessionToken, 2, 2)
|
||||
assertStatus(t, performJSON(t, router, http.MethodPost, "/api/v1/run/logs/batches", buffered), http.StatusOK)
|
||||
fresh := validLogBatchRequest(t, hello.SessionToken, 3, 3)
|
||||
retimestampLogBatchRequest(t, &fresh, time.Now().UTC().Add(time.Second))
|
||||
assertStatus(t, performJSON(t, router, http.MethodPost, "/api/v1/run/logs/batches", fresh), http.StatusOK)
|
||||
assertSSEEvent(t, reader, "log", `"seq":2`)
|
||||
assertSSEEvent(t, reader, "log", `"seq":3`)
|
||||
}
|
||||
|
||||
|
||||
@@ -944,6 +944,10 @@ func (h *coreHandlers) gamePluginManifestRegistration(w http.ResponseWriter, r *
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
if violations := request.Manifest.RuntimeProfiles.UnsupportedLegacyClientManagerViolations("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)
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type ClientManagerLifecycleStatus string
|
||||
|
||||
const (
|
||||
ClientManagerLifecycleRequested ClientManagerLifecycleStatus = "requested"
|
||||
ClientManagerLifecycleBuilding ClientManagerLifecycleStatus = "building"
|
||||
ClientManagerLifecycleAvailable ClientManagerLifecycleStatus = "available"
|
||||
ClientManagerLifecycleDeploying ClientManagerLifecycleStatus = "deploying"
|
||||
ClientManagerLifecycleInstalled ClientManagerLifecycleStatus = "installed"
|
||||
ClientManagerLifecycleRegistering ClientManagerLifecycleStatus = "registering"
|
||||
ClientManagerLifecycleOnline ClientManagerLifecycleStatus = "online"
|
||||
ClientManagerLifecycleDegraded ClientManagerLifecycleStatus = "degraded"
|
||||
ClientManagerLifecycleOffline ClientManagerLifecycleStatus = "offline"
|
||||
ClientManagerLifecycleUpdating ClientManagerLifecycleStatus = "updating"
|
||||
ClientManagerLifecycleRollingBack ClientManagerLifecycleStatus = "rolling_back"
|
||||
ClientManagerLifecycleStopping ClientManagerLifecycleStatus = "stopping"
|
||||
ClientManagerLifecycleUninstalled ClientManagerLifecycleStatus = "uninstalled"
|
||||
ClientManagerLifecycleFailed ClientManagerLifecycleStatus = "failed"
|
||||
)
|
||||
|
||||
type ClientManagerHealthStatus string
|
||||
|
||||
const (
|
||||
ClientManagerHealthUnknown ClientManagerHealthStatus = "unknown"
|
||||
ClientManagerHealthHealthy ClientManagerHealthStatus = "healthy"
|
||||
ClientManagerHealthDegraded ClientManagerHealthStatus = "degraded"
|
||||
ClientManagerHealthUnhealthy ClientManagerHealthStatus = "unhealthy"
|
||||
ClientManagerHealthOffline ClientManagerHealthStatus = "offline"
|
||||
)
|
||||
|
||||
type ClientManagerLifecycleOperation string
|
||||
|
||||
const (
|
||||
ClientManagerOperationDeploy ClientManagerLifecycleOperation = "deploy"
|
||||
ClientManagerOperationStart ClientManagerLifecycleOperation = "start"
|
||||
ClientManagerOperationStop ClientManagerLifecycleOperation = "stop"
|
||||
ClientManagerOperationRestart ClientManagerLifecycleOperation = "restart"
|
||||
ClientManagerOperationStatus ClientManagerLifecycleOperation = "status"
|
||||
ClientManagerOperationUpdate ClientManagerLifecycleOperation = "update"
|
||||
ClientManagerOperationRollback ClientManagerLifecycleOperation = "rollback"
|
||||
ClientManagerOperationUninstall ClientManagerLifecycleOperation = "uninstall"
|
||||
)
|
||||
|
||||
const (
|
||||
JobCapabilityClientManagerDeploy = "client-manager.deploy"
|
||||
JobCapabilityClientManagerControl = "client-manager.control"
|
||||
JobCapabilityClientManagerUpdate = "client-manager.update"
|
||||
JobCapabilityClientManagerRollback = "client-manager.rollback"
|
||||
JobCapabilityClientManagerUninstall = "client-manager.uninstall"
|
||||
)
|
||||
|
||||
const ClientManagerCompanionConfigSchemaVersion = 1
|
||||
|
||||
type ClientManagerSessionStatus string
|
||||
|
||||
const (
|
||||
ClientManagerSessionActive ClientManagerSessionStatus = "active"
|
||||
ClientManagerSessionRevoked ClientManagerSessionStatus = "revoked"
|
||||
ClientManagerSessionExpired ClientManagerSessionStatus = "expired"
|
||||
)
|
||||
|
||||
type RuntimeClientManagerDeployment struct {
|
||||
Mode string
|
||||
ExecutableRef string
|
||||
Arguments []string
|
||||
AutoStart bool
|
||||
RequiredRunCapabilities []string
|
||||
}
|
||||
|
||||
type RuntimeClientManagerLifecycle struct {
|
||||
Actions []string
|
||||
StartupTimeoutSeconds int
|
||||
StopTimeoutSeconds int
|
||||
}
|
||||
|
||||
type RuntimeClientManagerHealth struct {
|
||||
Mode string
|
||||
IntervalSeconds int
|
||||
DegradedAfterSeconds int
|
||||
OfflineAfterSeconds int
|
||||
RequiredCapabilities []string
|
||||
}
|
||||
|
||||
type RuntimeClientManagerCompatibility struct {
|
||||
MinimumVersion string
|
||||
MaximumVersion string
|
||||
AllowDowngrade bool
|
||||
}
|
||||
|
||||
type RuntimeClientManagerUpdatePolicy struct {
|
||||
Strategy string
|
||||
RequireApproval bool
|
||||
HealthConfirmationSeconds int
|
||||
RetainPrevious bool
|
||||
}
|
||||
|
||||
type ClientManagerInstallation struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
ProfileKey string
|
||||
RunEndpointID string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
Status ClientManagerLifecycleStatus
|
||||
Phase string
|
||||
DesiredVersion string
|
||||
ActiveVersion string
|
||||
PreviousVersion string
|
||||
DesiredRevision string
|
||||
ActiveRevision string
|
||||
PreviousRevision string
|
||||
DesiredArtifactID string
|
||||
ActiveArtifactID string
|
||||
PreviousArtifactID string
|
||||
Checksum string
|
||||
KeyGeneration int
|
||||
DeploymentGeneration int
|
||||
CurrentJobID string
|
||||
LastSuccessfulJobID string
|
||||
LastOperation ClientManagerLifecycleOperation
|
||||
Health ClientManagerHealthStatus
|
||||
HealthReason string
|
||||
LastSeenAt time.Time
|
||||
LastHeartbeatSequence uint64
|
||||
Retryable bool
|
||||
RequiresRedeploy bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
InstalledAt time.Time
|
||||
UninstalledAt time.Time
|
||||
}
|
||||
|
||||
type ClientManagerSession struct {
|
||||
ID string
|
||||
InstallationID string
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
RunEndpointID string
|
||||
ArtifactID string
|
||||
KeyGeneration int
|
||||
DeploymentGeneration int
|
||||
TokenHash string
|
||||
Capabilities []string
|
||||
Status ClientManagerSessionStatus
|
||||
LastHeartbeatSequence uint64
|
||||
LastSeenAt time.Time
|
||||
ExpiresAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
RevokedAt time.Time
|
||||
}
|
||||
|
||||
type ClientManagerRegistrationNonce struct {
|
||||
ID string
|
||||
InstallationID string
|
||||
ExpiresAt time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ClientManagerLifecycleActionAvailability struct {
|
||||
Operation ClientManagerLifecycleOperation
|
||||
Available bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
type ClientManagerLifecycleView struct {
|
||||
Installation ClientManagerInstallation
|
||||
Distribution ClientManagerDistribution
|
||||
Job Job
|
||||
Actions []ClientManagerLifecycleActionAvailability
|
||||
}
|
||||
|
||||
type ClientManagerDeployRequest struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
DistributionID string
|
||||
ExpectedDeploymentGeneration int
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type ClientManagerControlRequest struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
Operation ClientManagerLifecycleOperation
|
||||
ExpectedDeploymentGeneration int
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type ClientManagerUpdateRequest struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
DistributionID string
|
||||
ExpectedDeploymentGeneration int
|
||||
Approved bool
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type ClientManagerUninstallRequest struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
ExpectedDeploymentGeneration int
|
||||
Confirmed bool
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type ClientManagerRetryRequest struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
ExpectedDeploymentGeneration int
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type ClientManagerRevokeSessionRequest struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
Reason string
|
||||
}
|
||||
|
||||
type ClientManagerLifecycleInputRequest struct {
|
||||
RunEndpointID string
|
||||
SessionToken string
|
||||
JobID string
|
||||
LeaseToken string
|
||||
Attempt int
|
||||
}
|
||||
|
||||
type ClientManagerLifecycleInput struct {
|
||||
InstallationID string
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
Operation ClientManagerLifecycleOperation
|
||||
ArtifactID string
|
||||
Checksum string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
Version string
|
||||
SourceRevision string
|
||||
KeyGeneration int
|
||||
DeploymentGeneration int
|
||||
ExecutableRef string
|
||||
Arguments []string
|
||||
AutoStart bool
|
||||
StartupTimeoutSeconds int
|
||||
StopTimeoutSeconds int
|
||||
HealthConfirmationSeconds int
|
||||
IdempotencyKey string
|
||||
CompanionConfig *ClientManagerCompanionConfigInput
|
||||
}
|
||||
|
||||
type ClientManagerCompanionConfigInput struct {
|
||||
SchemaVersion int
|
||||
ConfigTemplateKey string
|
||||
ConfigTemplateRef string
|
||||
ConfigOutputRef string
|
||||
ConfigSchemaRef string
|
||||
ConfigFormat string
|
||||
PlatformBaseURLSource string
|
||||
InstallationID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
ProfileKey string
|
||||
ArtifactID string
|
||||
Version string
|
||||
SourceRevision string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
KeyGeneration int
|
||||
DeploymentGeneration int
|
||||
Capabilities []string
|
||||
RegistrationProof string
|
||||
ProofMaterialSource string
|
||||
ProofMaterialEnv string
|
||||
SessionMode string
|
||||
TLSPolicy string
|
||||
HeartbeatIntervalSeconds int
|
||||
CommandPollIntervalSeconds int
|
||||
RequestTimeoutSeconds int
|
||||
}
|
||||
|
||||
type ClientManagerRegisterRequest struct {
|
||||
InstallationID string
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
ArtifactID string
|
||||
Version string
|
||||
SourceRevision string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
KeyGeneration int
|
||||
DeploymentGeneration int
|
||||
Capabilities []string
|
||||
Timestamp time.Time
|
||||
Nonce string
|
||||
Signature string
|
||||
}
|
||||
|
||||
type ClientManagerRegisterResult struct {
|
||||
Accepted bool
|
||||
InstallationID string
|
||||
SessionToken string
|
||||
ExpiresAt time.Time
|
||||
HeartbeatEvery int
|
||||
ServerTime time.Time
|
||||
}
|
||||
|
||||
type ClientManagerHeartbeat struct {
|
||||
InstallationID string
|
||||
SessionToken string
|
||||
Sequence uint64
|
||||
Health ClientManagerHealthStatus
|
||||
HealthReason string
|
||||
Capabilities []string
|
||||
SentAt time.Time
|
||||
}
|
||||
|
||||
type ClientManagerHeartbeatResult struct {
|
||||
Accepted bool
|
||||
InstallationID string
|
||||
Status ClientManagerLifecycleStatus
|
||||
Health ClientManagerHealthStatus
|
||||
NextHeartbeat int
|
||||
SessionExpiresAt time.Time
|
||||
ServerTime time.Time
|
||||
}
|
||||
|
||||
type ClientManagerInstallationFilter struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
RunEndpointID string
|
||||
Status ClientManagerLifecycleStatus
|
||||
}
|
||||
|
||||
type ClientManagerSessionFilter struct {
|
||||
InstallationID string
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
Status ClientManagerSessionStatus
|
||||
}
|
||||
|
||||
type ClientManagerNonceFilter struct {
|
||||
InstallationID string
|
||||
ExpiresBefore time.Time
|
||||
}
|
||||
|
||||
func CopyClientManagerInstallation(value ClientManagerInstallation) ClientManagerInstallation {
|
||||
return value
|
||||
}
|
||||
|
||||
func CopyClientManagerSession(value ClientManagerSession) ClientManagerSession {
|
||||
value.Capabilities = CopyStringSlice(value.Capabilities)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopyClientManagerRegistrationNonce(value ClientManagerRegistrationNonce) ClientManagerRegistrationNonce {
|
||||
return value
|
||||
}
|
||||
|
||||
func CopyClientManagerLifecycleView(value ClientManagerLifecycleView) ClientManagerLifecycleView {
|
||||
value.Installation = CopyClientManagerInstallation(value.Installation)
|
||||
value.Distribution = CopyClientManagerDistribution(value.Distribution)
|
||||
value.Job = CopyJob(value.Job)
|
||||
value.Actions = append([]ClientManagerLifecycleActionAvailability(nil), value.Actions...)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopyClientManagerLifecycleInput(value ClientManagerLifecycleInput) ClientManagerLifecycleInput {
|
||||
value.Arguments = CopyStringSlice(value.Arguments)
|
||||
if value.CompanionConfig != nil {
|
||||
companion := CopyClientManagerCompanionConfigInput(*value.CompanionConfig)
|
||||
value.CompanionConfig = &companion
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func CopyClientManagerCompanionConfigInput(value ClientManagerCompanionConfigInput) ClientManagerCompanionConfigInput {
|
||||
value.Capabilities = CopyStringSlice(value.Capabilities)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopyClientManagerRegisterRequest(value ClientManagerRegisterRequest) ClientManagerRegisterRequest {
|
||||
value.Capabilities = CopyStringSlice(value.Capabilities)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopyClientManagerHeartbeat(value ClientManagerHeartbeat) ClientManagerHeartbeat {
|
||||
value.Capabilities = CopyStringSlice(value.Capabilities)
|
||||
return value
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCopyClientManagerLifecycleInputDeepCopiesCompanionCapabilities(t *testing.T) {
|
||||
original := ClientManagerLifecycleInput{
|
||||
Arguments: []string{"--foreground"},
|
||||
CompanionConfig: &ClientManagerCompanionConfigInput{
|
||||
Capabilities: []string{"component.register", "game-client.bridge"},
|
||||
},
|
||||
}
|
||||
cloned := CopyClientManagerLifecycleInput(original)
|
||||
cloned.Arguments[0] = "--changed"
|
||||
cloned.CompanionConfig.Capabilities[0] = "changed"
|
||||
|
||||
if original.Arguments[0] != "--foreground" {
|
||||
t.Fatalf("arguments were not deeply copied: %+v", original.Arguments)
|
||||
}
|
||||
if original.CompanionConfig.Capabilities[0] != "component.register" {
|
||||
t.Fatalf("companion capabilities were not deeply copied: %+v", original.CompanionConfig.Capabilities)
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,6 @@ type DistributionComponentKind string
|
||||
|
||||
const (
|
||||
DistributionComponentRun DistributionComponentKind = "run"
|
||||
DistributionComponentClientManager DistributionComponentKind = "client-manager"
|
||||
)
|
||||
|
||||
type ComponentKeyStatus string
|
||||
@@ -402,7 +401,6 @@ type RuntimeLifecycleProfile struct {
|
||||
Capabilities []string
|
||||
ActionRefs PluginLifecycleActions
|
||||
TransportKeys []string
|
||||
ClientManagerRef string
|
||||
DLLExtensionRefs []string
|
||||
Platforms []string
|
||||
}
|
||||
@@ -463,28 +461,6 @@ type RuntimeDataTarget struct {
|
||||
Platforms []string
|
||||
}
|
||||
|
||||
type RuntimeClientManagerProfile struct {
|
||||
Key string
|
||||
DisplayName string
|
||||
Version string
|
||||
RepositoryURL string
|
||||
RevisionPolicy string
|
||||
Branch string
|
||||
Tag string
|
||||
Revision string
|
||||
SupportedTargets []RuntimeTarget
|
||||
BuildSystem string
|
||||
WorkspaceRef string
|
||||
EntryRef string
|
||||
ConfigTemplates []RuntimeConfigTemplate
|
||||
OutputArtifacts []string
|
||||
Deployment RuntimeClientManagerDeployment
|
||||
Lifecycle RuntimeClientManagerLifecycle
|
||||
Health RuntimeClientManagerHealth
|
||||
Compatibility RuntimeClientManagerCompatibility
|
||||
UpdatePolicy RuntimeClientManagerUpdatePolicy
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionProfile struct {
|
||||
Key string
|
||||
DisplayName string
|
||||
@@ -593,7 +569,6 @@ type GamePluginRuntimeProfiles struct {
|
||||
LogSources []RuntimeLogSource
|
||||
TransportProfiles []RuntimeTransportProfile
|
||||
DataTargets []RuntimeDataTarget
|
||||
ClientManagers []RuntimeClientManagerProfile
|
||||
DLLExtensions []RuntimeDLLExtensionProfile
|
||||
}
|
||||
|
||||
@@ -1432,26 +1407,6 @@ type RunDistribution struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ClientManagerDistribution struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
ProfileKey string
|
||||
Version string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
RepositoryURL string
|
||||
SourceRevision string
|
||||
BuildJobID string
|
||||
ArtifactID string
|
||||
Checksum string
|
||||
KeyGeneration int
|
||||
SecretRef string
|
||||
Status DistributionStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type DependencyStatus struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
@@ -1471,25 +1426,6 @@ type DependencyStatus struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type ClientManagerBuildJob struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
PluginID string
|
||||
ProfileKey string
|
||||
Version string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
RepositoryURL string
|
||||
SourceRevision string
|
||||
ArtifactID string
|
||||
Checksum string
|
||||
KeyGeneration int
|
||||
LogsRef string
|
||||
Status DistributionJobStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type RunUpdateJob struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
@@ -1517,16 +1453,6 @@ type RunDistributionGenerateRequest struct {
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type ClientManagerBuildRequest struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
RepositoryURL string
|
||||
SourceRevision string
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
type ComponentKeyResetRequest struct {
|
||||
ServerInstanceID string
|
||||
ComponentKind DistributionComponentKind
|
||||
@@ -1716,26 +1642,12 @@ type RunDistributionFilter struct {
|
||||
Status DistributionStatus
|
||||
}
|
||||
|
||||
type ClientManagerDistributionFilter struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
Status DistributionStatus
|
||||
}
|
||||
|
||||
type DependencyStatusFilter struct {
|
||||
ServerInstanceID string
|
||||
ProbeKey string
|
||||
State DependencyState
|
||||
}
|
||||
|
||||
type ClientManagerBuildJobFilter struct {
|
||||
ServerInstanceID string
|
||||
ProfileKey string
|
||||
Status DistributionJobStatus
|
||||
}
|
||||
|
||||
type RunUpdateJobFilter struct {
|
||||
ServerInstanceID string
|
||||
Status DistributionJobStatus
|
||||
@@ -1927,16 +1839,6 @@ func CopyGamePluginRuntimeProfiles(profiles GamePluginRuntimeProfiles) GamePlugi
|
||||
for i := range profiles.DataTargets {
|
||||
profiles.DataTargets[i].Platforms = CopyStringSlice(profiles.DataTargets[i].Platforms)
|
||||
}
|
||||
profiles.ClientManagers = append([]RuntimeClientManagerProfile(nil), profiles.ClientManagers...)
|
||||
for i := range profiles.ClientManagers {
|
||||
profiles.ClientManagers[i].SupportedTargets = append([]RuntimeTarget(nil), profiles.ClientManagers[i].SupportedTargets...)
|
||||
profiles.ClientManagers[i].ConfigTemplates = append([]RuntimeConfigTemplate(nil), profiles.ClientManagers[i].ConfigTemplates...)
|
||||
profiles.ClientManagers[i].OutputArtifacts = CopyStringSlice(profiles.ClientManagers[i].OutputArtifacts)
|
||||
profiles.ClientManagers[i].Deployment.Arguments = CopyStringSlice(profiles.ClientManagers[i].Deployment.Arguments)
|
||||
profiles.ClientManagers[i].Deployment.RequiredRunCapabilities = CopyStringSlice(profiles.ClientManagers[i].Deployment.RequiredRunCapabilities)
|
||||
profiles.ClientManagers[i].Lifecycle.Actions = CopyStringSlice(profiles.ClientManagers[i].Lifecycle.Actions)
|
||||
profiles.ClientManagers[i].Health.RequiredCapabilities = CopyStringSlice(profiles.ClientManagers[i].Health.RequiredCapabilities)
|
||||
}
|
||||
profiles.DLLExtensions = append([]RuntimeDLLExtensionProfile(nil), profiles.DLLExtensions...)
|
||||
for i := range profiles.DLLExtensions {
|
||||
profiles.DLLExtensions[i].SupportedTargets = append([]RuntimeTarget(nil), profiles.DLLExtensions[i].SupportedTargets...)
|
||||
@@ -2242,10 +2144,6 @@ func CopyRunDistribution(distribution RunDistribution) RunDistribution {
|
||||
return distribution
|
||||
}
|
||||
|
||||
func CopyClientManagerDistribution(distribution ClientManagerDistribution) ClientManagerDistribution {
|
||||
return distribution
|
||||
}
|
||||
|
||||
func CopyDependencyStatus(status DependencyStatus) DependencyStatus {
|
||||
return status
|
||||
}
|
||||
@@ -2259,10 +2157,6 @@ func CopyDependencyCatalog(catalog DependencyCatalog) DependencyCatalog {
|
||||
return catalog
|
||||
}
|
||||
|
||||
func CopyClientManagerBuildJob(job ClientManagerBuildJob) ClientManagerBuildJob {
|
||||
return job
|
||||
}
|
||||
|
||||
func CopyRunUpdateJob(job RunUpdateJob) RunUpdateJob {
|
||||
return job
|
||||
}
|
||||
@@ -2271,10 +2165,6 @@ func CopyRunDistributionGenerateRequest(request RunDistributionGenerateRequest)
|
||||
return request
|
||||
}
|
||||
|
||||
func CopyClientManagerBuildRequest(request ClientManagerBuildRequest) ClientManagerBuildRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
func CopyComponentKeyResetRequest(request ComponentKeyResetRequest) ComponentKeyResetRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ Run sessions persist only a token hash, generation, status, expiry, capability f
|
||||
- `serverInstanceId`: server instance using the binding.
|
||||
- `pluginId` and `pluginVersion`: installed plugin contract that declared the logical runtime profile.
|
||||
- `profileKey`: declared lifecycle/runtime profile key.
|
||||
- `mode`: runtime mode such as `local-process`, `hosted-ftp-rcon`, `ftp-only`, or `custom-client`.
|
||||
- `mode`: runtime mode such as `local-process`, `hosted-ftp-rcon`, or `ftp-only`.
|
||||
- `bindings`: logical binding keys to operator-provided settings.
|
||||
- `missingKeys`: logical keys that must be completed before dependent actions are available.
|
||||
- `status`: `complete` or `incomplete`.
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type ClientManagerDeployRequest struct {
|
||||
ProfileKey string `json:"profileKey"`
|
||||
DistributionID string `json:"distributionId"`
|
||||
ExpectedDeploymentGeneration int `json:"expectedDeploymentGeneration,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
type ClientManagerControlRequest struct {
|
||||
ProfileKey string `json:"profileKey"`
|
||||
Operation string `json:"operation"`
|
||||
ExpectedDeploymentGeneration int `json:"expectedDeploymentGeneration"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
type ClientManagerUpdateRequest struct {
|
||||
ProfileKey string `json:"profileKey"`
|
||||
DistributionID string `json:"distributionId"`
|
||||
ExpectedDeploymentGeneration int `json:"expectedDeploymentGeneration"`
|
||||
Approved bool `json:"approved"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
type ClientManagerUninstallRequest struct {
|
||||
ProfileKey string `json:"profileKey"`
|
||||
ExpectedDeploymentGeneration int `json:"expectedDeploymentGeneration"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
type ClientManagerRetryRequest struct {
|
||||
ProfileKey string `json:"profileKey"`
|
||||
ExpectedDeploymentGeneration int `json:"expectedDeploymentGeneration"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
}
|
||||
|
||||
type ClientManagerRevokeSessionRequest struct {
|
||||
ProfileKey string `json:"profileKey"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type ClientManagerLifecycleActionResponse struct {
|
||||
Operation string `json:"operation"`
|
||||
Available bool `json:"available"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type ClientManagerLifecycleJobResponse struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
State domain.JobState `json:"state,omitempty"`
|
||||
Progress JobProgressBody `json:"progress,omitempty"`
|
||||
Attempt int `json:"attempt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type ClientManagerInstallationResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
Status string `json:"status"`
|
||||
Phase string `json:"phase,omitempty"`
|
||||
DesiredVersion string `json:"desiredVersion,omitempty"`
|
||||
ActiveVersion string `json:"activeVersion,omitempty"`
|
||||
PreviousVersion string `json:"previousVersion,omitempty"`
|
||||
DesiredRevision string `json:"desiredRevision,omitempty"`
|
||||
ActiveRevision string `json:"activeRevision,omitempty"`
|
||||
PreviousRevision string `json:"previousRevision,omitempty"`
|
||||
DesiredArtifactID string `json:"desiredArtifactId,omitempty"`
|
||||
ActiveArtifactID string `json:"activeArtifactId,omitempty"`
|
||||
PreviousArtifactID string `json:"previousArtifactId,omitempty"`
|
||||
KeyGeneration int `json:"keyGeneration"`
|
||||
DeploymentGeneration int `json:"deploymentGeneration"`
|
||||
CurrentJobID string `json:"currentJobId,omitempty"`
|
||||
LastSuccessfulJobID string `json:"lastSuccessfulJobId,omitempty"`
|
||||
LastOperation string `json:"lastOperation,omitempty"`
|
||||
Health string `json:"health"`
|
||||
HealthReason string `json:"healthReason,omitempty"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt,omitempty"`
|
||||
Retryable bool `json:"retryable"`
|
||||
RequiresRedeploy bool `json:"requiresRedeploy"`
|
||||
InstalledAt time.Time `json:"installedAt,omitempty"`
|
||||
UninstalledAt time.Time `json:"uninstalledAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Distribution *ClientManagerDistributionSummaryResponse `json:"distribution,omitempty"`
|
||||
Job *ClientManagerLifecycleJobResponse `json:"job,omitempty"`
|
||||
Actions []ClientManagerLifecycleActionResponse `json:"actions"`
|
||||
}
|
||||
|
||||
type ClientManagerDistributionSummaryResponse struct {
|
||||
ID string `json:"id"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
SourceRevision string `json:"sourceRevision"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
Checksum string `json:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type ClientManagerInstallationListResponse struct {
|
||||
Items []ClientManagerInstallationResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type ClientManagerLifecycleInputRequest struct {
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
SessionToken string `json:"sessionToken"`
|
||||
JobID string `json:"jobId"`
|
||||
LeaseToken string `json:"leaseToken"`
|
||||
Attempt int `json:"attempt"`
|
||||
}
|
||||
|
||||
type ClientManagerLifecycleInputResponse struct {
|
||||
InstallationID string `json:"installationId"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
Operation string `json:"operation"`
|
||||
ArtifactID string `json:"artifactId,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
Version string `json:"version,omitempty"`
|
||||
SourceRevision string `json:"sourceRevision,omitempty"`
|
||||
KeyGeneration int `json:"keyGeneration"`
|
||||
DeploymentGeneration int `json:"deploymentGeneration"`
|
||||
ExecutableRef string `json:"executableRef"`
|
||||
Arguments []string `json:"arguments"`
|
||||
AutoStart bool `json:"autoStart"`
|
||||
StartupTimeoutSeconds int `json:"startupTimeoutSeconds"`
|
||||
StopTimeoutSeconds int `json:"stopTimeoutSeconds"`
|
||||
HealthConfirmationSeconds int `json:"healthConfirmationSeconds"`
|
||||
IdempotencyKey string `json:"idempotencyKey"`
|
||||
CompanionConfig *ClientManagerCompanionConfigInputResponse `json:"companionConfig,omitempty"`
|
||||
}
|
||||
|
||||
type ClientManagerCompanionConfigInputResponse struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
ConfigTemplateKey string `json:"configTemplateKey"`
|
||||
ConfigTemplateRef string `json:"configTemplateRef"`
|
||||
ConfigOutputRef string `json:"configOutputRef"`
|
||||
ConfigSchemaRef string `json:"configSchemaRef"`
|
||||
ConfigFormat string `json:"configFormat"`
|
||||
PlatformBaseURLSource string `json:"platformBaseUrlSource"`
|
||||
InstallationID string `json:"installationId"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
Version string `json:"version"`
|
||||
SourceRevision string `json:"sourceRevision"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
KeyGeneration int `json:"keyGeneration"`
|
||||
DeploymentGeneration int `json:"deploymentGeneration"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
RegistrationProof string `json:"registrationProof"`
|
||||
ProofMaterialSource string `json:"proofMaterialSource"`
|
||||
ProofMaterialEnv string `json:"proofMaterialEnv"`
|
||||
SessionMode string `json:"sessionMode"`
|
||||
TLSPolicy string `json:"tlsPolicy"`
|
||||
HeartbeatIntervalSeconds int `json:"heartbeatIntervalSeconds"`
|
||||
CommandPollIntervalSeconds int `json:"commandPollIntervalSeconds"`
|
||||
RequestTimeoutSeconds int `json:"requestTimeoutSeconds"`
|
||||
}
|
||||
|
||||
type ClientManagerRegisterRequest struct {
|
||||
InstallationID string `json:"installationId"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
Version string `json:"version"`
|
||||
SourceRevision string `json:"sourceRevision"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
KeyGeneration int `json:"keyGeneration"`
|
||||
DeploymentGeneration int `json:"deploymentGeneration"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Nonce string `json:"nonce"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type ClientManagerRegisterResponse struct {
|
||||
Accepted bool `json:"accepted"`
|
||||
InstallationID string `json:"installationId"`
|
||||
SessionToken string `json:"sessionToken"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
HeartbeatEvery int `json:"heartbeatEverySeconds"`
|
||||
ServerTime time.Time `json:"serverTime"`
|
||||
}
|
||||
|
||||
type ClientManagerHeartbeatRequest struct {
|
||||
InstallationID string `json:"installationId"`
|
||||
SessionToken string `json:"sessionToken"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
Health string `json:"health"`
|
||||
HealthReason string `json:"healthReason,omitempty"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
SentAt time.Time `json:"sentAt"`
|
||||
}
|
||||
|
||||
type ClientManagerHeartbeatResponse struct {
|
||||
Accepted bool `json:"accepted"`
|
||||
InstallationID string `json:"installationId"`
|
||||
Status string `json:"status"`
|
||||
Health string `json:"health"`
|
||||
NextHeartbeat int `json:"nextHeartbeatSeconds"`
|
||||
SessionExpiresAt time.Time `json:"sessionExpiresAt"`
|
||||
ServerTime time.Time `json:"serverTime"`
|
||||
}
|
||||
|
||||
func (request ClientManagerDeployRequest) ToDomain(serverID string) domain.ClientManagerDeployRequest {
|
||||
return domain.ClientManagerDeployRequest{ServerInstanceID: serverID, ProfileKey: request.ProfileKey, DistributionID: request.DistributionID, ExpectedDeploymentGeneration: request.ExpectedDeploymentGeneration, IdempotencyKey: request.IdempotencyKey}
|
||||
}
|
||||
|
||||
func (request ClientManagerControlRequest) ToDomain(serverID string) domain.ClientManagerControlRequest {
|
||||
return domain.ClientManagerControlRequest{ServerInstanceID: serverID, ProfileKey: request.ProfileKey, Operation: domain.ClientManagerLifecycleOperation(request.Operation), ExpectedDeploymentGeneration: request.ExpectedDeploymentGeneration, IdempotencyKey: request.IdempotencyKey}
|
||||
}
|
||||
|
||||
func (request ClientManagerUpdateRequest) ToDomain(serverID string) domain.ClientManagerUpdateRequest {
|
||||
return domain.ClientManagerUpdateRequest{ServerInstanceID: serverID, ProfileKey: request.ProfileKey, DistributionID: request.DistributionID, ExpectedDeploymentGeneration: request.ExpectedDeploymentGeneration, Approved: request.Approved, IdempotencyKey: request.IdempotencyKey}
|
||||
}
|
||||
|
||||
func (request ClientManagerUninstallRequest) ToDomain(serverID string) domain.ClientManagerUninstallRequest {
|
||||
return domain.ClientManagerUninstallRequest{ServerInstanceID: serverID, ProfileKey: request.ProfileKey, ExpectedDeploymentGeneration: request.ExpectedDeploymentGeneration, Confirmed: request.Confirmed, IdempotencyKey: request.IdempotencyKey}
|
||||
}
|
||||
|
||||
func (request ClientManagerRetryRequest) ToDomain(serverID string) domain.ClientManagerRetryRequest {
|
||||
return domain.ClientManagerRetryRequest{ServerInstanceID: serverID, ProfileKey: request.ProfileKey, ExpectedDeploymentGeneration: request.ExpectedDeploymentGeneration, IdempotencyKey: request.IdempotencyKey}
|
||||
}
|
||||
|
||||
func (request ClientManagerRevokeSessionRequest) ToDomain(serverID string) domain.ClientManagerRevokeSessionRequest {
|
||||
return domain.ClientManagerRevokeSessionRequest{ServerInstanceID: serverID, ProfileKey: request.ProfileKey, Reason: request.Reason}
|
||||
}
|
||||
|
||||
func (request ClientManagerLifecycleInputRequest) ToDomain() domain.ClientManagerLifecycleInputRequest {
|
||||
return domain.ClientManagerLifecycleInputRequest{RunEndpointID: request.RunEndpointID, SessionToken: request.SessionToken, JobID: request.JobID, LeaseToken: request.LeaseToken, Attempt: request.Attempt}
|
||||
}
|
||||
|
||||
func ClientManagerLifecycleInputFromDomain(value domain.ClientManagerLifecycleInput) ClientManagerLifecycleInputResponse {
|
||||
value = domain.CopyClientManagerLifecycleInput(value)
|
||||
response := ClientManagerLifecycleInputResponse{InstallationID: value.InstallationID, ServerInstanceID: value.ServerInstanceID, ProfileKey: value.ProfileKey, Operation: string(value.Operation), ArtifactID: value.ArtifactID, Checksum: value.Checksum, TargetOS: value.TargetOS, TargetArch: value.TargetArch, Version: value.Version, SourceRevision: value.SourceRevision, KeyGeneration: value.KeyGeneration, DeploymentGeneration: value.DeploymentGeneration, ExecutableRef: value.ExecutableRef, Arguments: value.Arguments, AutoStart: value.AutoStart, StartupTimeoutSeconds: value.StartupTimeoutSeconds, StopTimeoutSeconds: value.StopTimeoutSeconds, HealthConfirmationSeconds: value.HealthConfirmationSeconds, IdempotencyKey: value.IdempotencyKey}
|
||||
if value.CompanionConfig != nil {
|
||||
companion := ClientManagerCompanionConfigInputFromDomain(*value.CompanionConfig)
|
||||
response.CompanionConfig = &companion
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func ClientManagerCompanionConfigInputFromDomain(value domain.ClientManagerCompanionConfigInput) ClientManagerCompanionConfigInputResponse {
|
||||
value = domain.CopyClientManagerCompanionConfigInput(value)
|
||||
return ClientManagerCompanionConfigInputResponse{SchemaVersion: value.SchemaVersion, ConfigTemplateKey: value.ConfigTemplateKey, ConfigTemplateRef: value.ConfigTemplateRef, ConfigOutputRef: value.ConfigOutputRef, ConfigSchemaRef: value.ConfigSchemaRef, ConfigFormat: value.ConfigFormat, PlatformBaseURLSource: value.PlatformBaseURLSource, InstallationID: value.InstallationID, ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, ProfileKey: value.ProfileKey, ArtifactID: value.ArtifactID, Version: value.Version, SourceRevision: value.SourceRevision, TargetOS: value.TargetOS, TargetArch: value.TargetArch, KeyGeneration: value.KeyGeneration, DeploymentGeneration: value.DeploymentGeneration, Capabilities: value.Capabilities, RegistrationProof: value.RegistrationProof, ProofMaterialSource: value.ProofMaterialSource, ProofMaterialEnv: value.ProofMaterialEnv, SessionMode: value.SessionMode, TLSPolicy: value.TLSPolicy, HeartbeatIntervalSeconds: value.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: value.CommandPollIntervalSeconds, RequestTimeoutSeconds: value.RequestTimeoutSeconds}
|
||||
}
|
||||
|
||||
func (request ClientManagerRegisterRequest) ToDomain() domain.ClientManagerRegisterRequest {
|
||||
return domain.ClientManagerRegisterRequest{InstallationID: request.InstallationID, ServerInstanceID: request.ServerInstanceID, ProfileKey: request.ProfileKey, ArtifactID: request.ArtifactID, Version: request.Version, SourceRevision: request.SourceRevision, TargetOS: request.TargetOS, TargetArch: request.TargetArch, KeyGeneration: request.KeyGeneration, DeploymentGeneration: request.DeploymentGeneration, Capabilities: domain.CopyStringSlice(request.Capabilities), Timestamp: request.Timestamp, Nonce: request.Nonce, Signature: request.Signature}
|
||||
}
|
||||
|
||||
func ClientManagerRegisterFromDomain(value domain.ClientManagerRegisterResult) ClientManagerRegisterResponse {
|
||||
return ClientManagerRegisterResponse{Accepted: value.Accepted, InstallationID: value.InstallationID, SessionToken: value.SessionToken, ExpiresAt: value.ExpiresAt, HeartbeatEvery: value.HeartbeatEvery, ServerTime: value.ServerTime}
|
||||
}
|
||||
|
||||
func (request ClientManagerHeartbeatRequest) ToDomain() domain.ClientManagerHeartbeat {
|
||||
return domain.ClientManagerHeartbeat{InstallationID: request.InstallationID, SessionToken: request.SessionToken, Sequence: request.Sequence, Health: domain.ClientManagerHealthStatus(request.Health), HealthReason: request.HealthReason, Capabilities: domain.CopyStringSlice(request.Capabilities), SentAt: request.SentAt}
|
||||
}
|
||||
|
||||
func ClientManagerHeartbeatFromDomain(value domain.ClientManagerHeartbeatResult) ClientManagerHeartbeatResponse {
|
||||
return ClientManagerHeartbeatResponse{Accepted: value.Accepted, InstallationID: value.InstallationID, Status: string(value.Status), Health: string(value.Health), NextHeartbeat: value.NextHeartbeat, SessionExpiresAt: value.SessionExpiresAt, ServerTime: value.ServerTime}
|
||||
}
|
||||
|
||||
func ClientManagerLifecycleViewFromDomain(value domain.ClientManagerLifecycleView) ClientManagerInstallationResponse {
|
||||
value = domain.CopyClientManagerLifecycleView(value)
|
||||
installation := value.Installation
|
||||
response := ClientManagerInstallationResponse{ID: installation.ID, ServerInstanceID: installation.ServerInstanceID, PluginID: installation.PluginID, ProfileKey: installation.ProfileKey, TargetOS: installation.TargetOS, TargetArch: installation.TargetArch, Status: string(installation.Status), Phase: installation.Phase, DesiredVersion: installation.DesiredVersion, ActiveVersion: installation.ActiveVersion, PreviousVersion: installation.PreviousVersion, DesiredRevision: installation.DesiredRevision, ActiveRevision: installation.ActiveRevision, PreviousRevision: installation.PreviousRevision, DesiredArtifactID: installation.DesiredArtifactID, ActiveArtifactID: installation.ActiveArtifactID, PreviousArtifactID: installation.PreviousArtifactID, KeyGeneration: installation.KeyGeneration, DeploymentGeneration: installation.DeploymentGeneration, CurrentJobID: installation.CurrentJobID, LastSuccessfulJobID: installation.LastSuccessfulJobID, LastOperation: string(installation.LastOperation), Health: string(installation.Health), HealthReason: installation.HealthReason, LastSeenAt: installation.LastSeenAt, Retryable: installation.Retryable, RequiresRedeploy: installation.RequiresRedeploy, InstalledAt: installation.InstalledAt, UninstalledAt: installation.UninstalledAt, UpdatedAt: installation.UpdatedAt}
|
||||
if value.Distribution.ID != "" {
|
||||
response.Distribution = &ClientManagerDistributionSummaryResponse{ID: value.Distribution.ID, ArtifactID: value.Distribution.ArtifactID, SourceRevision: value.Distribution.SourceRevision, TargetOS: value.Distribution.TargetOS, TargetArch: value.Distribution.TargetArch, Checksum: value.Distribution.Checksum, KeyGeneration: value.Distribution.KeyGeneration, Status: string(value.Distribution.Status)}
|
||||
}
|
||||
if value.Job.ID != "" {
|
||||
response.Job = &ClientManagerLifecycleJobResponse{ID: value.Job.ID, State: value.Job.State, Progress: JobProgressBody{Percent: value.Job.Progress.Percent, Message: value.Job.Progress.Message}, Attempt: value.Job.Attempt, CreatedAt: value.Job.CreatedAt, UpdatedAt: value.Job.UpdatedAt}
|
||||
}
|
||||
response.Actions = make([]ClientManagerLifecycleActionResponse, len(value.Actions))
|
||||
for i, action := range value.Actions {
|
||||
response.Actions[i] = ClientManagerLifecycleActionResponse{Operation: string(action.Operation), Available: action.Available, Reason: action.Reason}
|
||||
}
|
||||
if response.Actions == nil {
|
||||
response.Actions = []ClientManagerLifecycleActionResponse{}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func ClientManagerLifecycleViewsFromDomain(values []domain.ClientManagerLifecycleView) ClientManagerInstallationListResponse {
|
||||
items := make([]ClientManagerInstallationResponse, len(values))
|
||||
for i, value := range values {
|
||||
items[i] = ClientManagerLifecycleViewFromDomain(value)
|
||||
}
|
||||
return ClientManagerInstallationListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestClientManagerLifecycleInputProjectsRunOnlyCompanionConfigWithoutRawMaterial(t *testing.T) {
|
||||
response := ClientManagerLifecycleInputFromDomain(domain.ClientManagerLifecycleInput{
|
||||
InstallationID: "installation-1",
|
||||
ServerInstanceID: "server-1",
|
||||
ProfileKey: "scum-client-manager",
|
||||
Operation: domain.ClientManagerOperationDeploy,
|
||||
DeploymentGeneration: 2,
|
||||
CompanionConfig: &domain.ClientManagerCompanionConfigInput{
|
||||
SchemaVersion: 1,
|
||||
ConfigTemplateKey: "client-config",
|
||||
ConfigTemplateRef: "config.yaml.example",
|
||||
ConfigOutputRef: "config.yaml",
|
||||
ConfigSchemaRef: "schemas/companion/config.schema.json",
|
||||
ConfigFormat: "yaml",
|
||||
PlatformBaseURLSource: "run-control",
|
||||
InstallationID: "installation-1",
|
||||
ServerInstanceID: "server-1",
|
||||
PluginID: "game.scum",
|
||||
ProfileKey: "scum-client-manager",
|
||||
ArtifactID: "artifact-1",
|
||||
Version: "1.0.0",
|
||||
SourceRevision: "revision-1",
|
||||
TargetOS: "windows",
|
||||
TargetArch: "amd64",
|
||||
KeyGeneration: 3,
|
||||
DeploymentGeneration: 2,
|
||||
Capabilities: []string{"component.register", "component.heartbeat", "component.health", "game-client.bridge"},
|
||||
RegistrationProof: "hmac-sha256",
|
||||
ProofMaterialSource: "component-package",
|
||||
ProofMaterialEnv: "SCUM_COMPONENT_PROOF",
|
||||
SessionMode: "component-session",
|
||||
TLSPolicy: "verify-system-roots",
|
||||
HeartbeatIntervalSeconds: 30,
|
||||
CommandPollIntervalSeconds: 5,
|
||||
RequestTimeoutSeconds: 15,
|
||||
},
|
||||
})
|
||||
if response.CompanionConfig == nil || response.CompanionConfig.ProofMaterialEnv != "SCUM_COMPONENT_PROOF" {
|
||||
t.Fatalf("expected run-only companion projection, got %+v", response.CompanionConfig)
|
||||
}
|
||||
encoded, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal lifecycle input: %v", err)
|
||||
}
|
||||
serialized := string(encoded)
|
||||
for _, forbidden := range []string{`"proofMaterial":`, `"authKey":`, `"componentKey":`, `"sessionToken":`, `"secretRef":`, "/Users/", "run.socket"} {
|
||||
if strings.Contains(serialized, forbidden) {
|
||||
t.Fatalf("run lifecycle input leaked %q: %s", forbidden, serialized)
|
||||
}
|
||||
}
|
||||
|
||||
browserProjection, err := json.Marshal(ClientManagerLifecycleViewFromDomain(domain.ClientManagerLifecycleView{Installation: domain.ClientManagerInstallation{ID: "installation-1"}}))
|
||||
if err != nil {
|
||||
t.Fatalf("marshal browser lifecycle projection: %v", err)
|
||||
}
|
||||
if strings.Contains(string(browserProjection), "companionConfig") || strings.Contains(string(browserProjection), "proofMaterialEnv") {
|
||||
t.Fatalf("browser lifecycle projection exposed run-only config: %s", browserProjection)
|
||||
}
|
||||
}
|
||||
@@ -80,19 +80,6 @@ type LogBackfillRequest struct {
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
}
|
||||
|
||||
type ClientManagerDownloadRequest struct {
|
||||
ProfileKey string `json:"profileKey,omitempty"`
|
||||
}
|
||||
|
||||
type ClientManagerBuildRequest struct {
|
||||
ProfileKey string `json:"profileKey"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
RepositoryURL string `json:"repositoryUrl"`
|
||||
SourceRevision string `json:"sourceRevision,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
}
|
||||
|
||||
type ComponentKeyResetRequest struct {
|
||||
ComponentKind string `json:"componentKind"`
|
||||
ComponentKey string `json:"componentKey,omitempty"`
|
||||
@@ -145,26 +132,6 @@ type RunDistributionResponse struct {
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ClientManagerDistributionResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
Version string `json:"version,omitempty"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
RepositoryURL string `json:"repositoryUrl"`
|
||||
SourceRevision string `json:"sourceRevision"`
|
||||
BuildJobID string `json:"buildJobId"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
Checksum string `json:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration"`
|
||||
SecretRef string `json:"secretRef"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type DependencyStatusResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
@@ -225,25 +192,6 @@ type DependencyCatalogResponse struct {
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ClientManagerBuildJobResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
Version string `json:"version,omitempty"`
|
||||
TargetOS string `json:"targetOs"`
|
||||
TargetArch string `json:"targetArch"`
|
||||
RepositoryURL string `json:"repositoryUrl"`
|
||||
SourceRevision string `json:"sourceRevision"`
|
||||
ArtifactID string `json:"artifactId,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
KeyGeneration int `json:"keyGeneration,omitempty"`
|
||||
LogsRef string `json:"logsRef,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type RunUpdateJobResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
@@ -310,18 +258,6 @@ func (request LogBackfillRequest) ToDomain(serverInstanceID string) domain.LogBa
|
||||
}
|
||||
}
|
||||
|
||||
func (request ClientManagerBuildRequest) ToDomain(serverInstanceID string) domain.ClientManagerBuildRequest {
|
||||
return domain.ClientManagerBuildRequest{
|
||||
ServerInstanceID: serverInstanceID,
|
||||
ProfileKey: request.ProfileKey,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
RepositoryURL: request.RepositoryURL,
|
||||
SourceRevision: request.SourceRevision,
|
||||
IdempotencyKey: request.IdempotencyKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (request ComponentKeyResetRequest) ToDomain(serverInstanceID string) domain.ComponentKeyResetRequest {
|
||||
return domain.ComponentKeyResetRequest{
|
||||
ServerInstanceID: serverInstanceID,
|
||||
@@ -381,28 +317,6 @@ func RunDistributionFromDomain(distribution domain.RunDistribution) RunDistribut
|
||||
}
|
||||
}
|
||||
|
||||
func ClientManagerDistributionFromDomain(distribution domain.ClientManagerDistribution) ClientManagerDistributionResponse {
|
||||
return ClientManagerDistributionResponse{
|
||||
ID: distribution.ID,
|
||||
ServerInstanceID: distribution.ServerInstanceID,
|
||||
PluginID: distribution.PluginID,
|
||||
ProfileKey: distribution.ProfileKey,
|
||||
Version: distribution.Version,
|
||||
TargetOS: distribution.TargetOS,
|
||||
TargetArch: distribution.TargetArch,
|
||||
RepositoryURL: distribution.RepositoryURL,
|
||||
SourceRevision: distribution.SourceRevision,
|
||||
BuildJobID: distribution.BuildJobID,
|
||||
ArtifactID: distribution.ArtifactID,
|
||||
Checksum: distribution.Checksum,
|
||||
KeyGeneration: distribution.KeyGeneration,
|
||||
SecretRef: distribution.SecretRef,
|
||||
Status: string(distribution.Status),
|
||||
CreatedAt: distribution.CreatedAt,
|
||||
UpdatedAt: distribution.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func DependencyStatusFromDomain(status domain.DependencyStatus) DependencyStatusResponse {
|
||||
return DependencyStatusResponse{
|
||||
ID: status.ID,
|
||||
@@ -441,27 +355,6 @@ func DependencyCatalogFromDomain(catalog domain.DependencyCatalog) DependencyCat
|
||||
return DependencyCatalogResponse{ServerInstanceID: catalog.ServerInstanceID, PluginID: catalog.PluginID, PluginVersion: catalog.PluginVersion, ProfileKey: catalog.ProfileKey, TargetOS: catalog.TargetOS, TargetArch: catalog.TargetArch, Probes: probes, Plans: plans, UpdatedAt: catalog.UpdatedAt}
|
||||
}
|
||||
|
||||
func ClientManagerBuildJobFromDomain(job domain.ClientManagerBuildJob) ClientManagerBuildJobResponse {
|
||||
return ClientManagerBuildJobResponse{
|
||||
ID: job.ID,
|
||||
ServerInstanceID: job.ServerInstanceID,
|
||||
PluginID: job.PluginID,
|
||||
ProfileKey: job.ProfileKey,
|
||||
Version: job.Version,
|
||||
TargetOS: job.TargetOS,
|
||||
TargetArch: job.TargetArch,
|
||||
RepositoryURL: job.RepositoryURL,
|
||||
SourceRevision: job.SourceRevision,
|
||||
ArtifactID: job.ArtifactID,
|
||||
Checksum: job.Checksum,
|
||||
KeyGeneration: job.KeyGeneration,
|
||||
LogsRef: job.LogsRef,
|
||||
Status: string(job.Status),
|
||||
CreatedAt: job.CreatedAt,
|
||||
UpdatedAt: job.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func RunUpdateJobFromDomain(job domain.RunUpdateJob) RunUpdateJobResponse {
|
||||
return RunUpdateJobResponse{
|
||||
ID: job.ID,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
@@ -27,7 +29,7 @@ type RuntimeLifecycleProfileBody struct {
|
||||
Capabilities []string `json:"capabilities"`
|
||||
ActionRefs PluginLifecycleActionsBody `json:"actionRefs,omitempty"`
|
||||
TransportKeys []string `json:"transportKeys,omitempty"`
|
||||
ClientManagerRef string `json:"clientManagerRef,omitempty"`
|
||||
ClientManagerRef json.RawMessage `json:"clientManagerRef,omitempty"`
|
||||
DLLExtensionRefs []string `json:"dllExtensionRefs,omitempty"`
|
||||
Platforms []string `json:"platforms,omitempty"`
|
||||
}
|
||||
@@ -148,57 +150,6 @@ type RuntimeConfigTemplateBody struct {
|
||||
OutputRef string `json:"outputRef"`
|
||||
}
|
||||
|
||||
type RuntimeClientManagerDeploymentBody struct {
|
||||
Mode string `json:"mode"`
|
||||
ExecutableRef string `json:"executableRef"`
|
||||
Arguments []string `json:"arguments,omitempty"`
|
||||
AutoStart bool `json:"autoStart,omitempty"`
|
||||
RequiredRunCapabilities []string `json:"requiredRunCapabilities"`
|
||||
}
|
||||
|
||||
type RuntimeClientManagerLifecycleBody struct {
|
||||
Actions []string `json:"actions"`
|
||||
StartupTimeoutSeconds int `json:"startupTimeoutSeconds"`
|
||||
StopTimeoutSeconds int `json:"stopTimeoutSeconds"`
|
||||
}
|
||||
|
||||
type RuntimeClientManagerHealthBody struct {
|
||||
Mode string `json:"mode"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
DegradedAfterSeconds int `json:"degradedAfterSeconds"`
|
||||
OfflineAfterSeconds int `json:"offlineAfterSeconds"`
|
||||
RequiredCapabilities []string `json:"requiredCapabilities"`
|
||||
}
|
||||
|
||||
type RuntimeClientManagerCompatibilityBody struct {
|
||||
MinimumVersion string `json:"minimumVersion,omitempty"`
|
||||
MaximumVersion string `json:"maximumVersion,omitempty"`
|
||||
AllowDowngrade bool `json:"allowDowngrade"`
|
||||
}
|
||||
|
||||
type RuntimeClientManagerUpdatePolicyBody struct {
|
||||
Strategy string `json:"strategy"`
|
||||
RequireApproval bool `json:"requireApproval"`
|
||||
HealthConfirmationSeconds int `json:"healthConfirmationSeconds"`
|
||||
RetainPrevious bool `json:"retainPrevious"`
|
||||
}
|
||||
|
||||
type RuntimeClientManagerProfileBody struct {
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Repository RuntimeRepositoryBody `json:"repository"`
|
||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||
Build RuntimeBuildBody `json:"build"`
|
||||
ConfigTemplates []RuntimeConfigTemplateBody `json:"configTemplates,omitempty"`
|
||||
OutputArtifacts []string `json:"outputArtifacts"`
|
||||
Deployment RuntimeClientManagerDeploymentBody `json:"deployment,omitempty"`
|
||||
Lifecycle RuntimeClientManagerLifecycleBody `json:"lifecycle,omitempty"`
|
||||
Health RuntimeClientManagerHealthBody `json:"health,omitempty"`
|
||||
Compatibility RuntimeClientManagerCompatibilityBody `json:"compatibility,omitempty"`
|
||||
UpdatePolicy RuntimeClientManagerUpdatePolicyBody `json:"updatePolicy,omitempty"`
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionProfileBody struct {
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"displayName"`
|
||||
@@ -264,10 +215,23 @@ type GamePluginRuntimeProfilesBody struct {
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
||||
ClientManagers []RuntimeClientManagerProfileBody `json:"clientManagers,omitempty"`
|
||||
ClientManagers *[]json.RawMessage `json:"clientManagers,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
||||
}
|
||||
|
||||
func (body GamePluginRuntimeProfilesBody) UnsupportedLegacyClientManagerViolations(prefix string) []string {
|
||||
var violations []string
|
||||
if body.ClientManagers != nil {
|
||||
violations = append(violations, prefix+".clientManagers is no longer supported")
|
||||
}
|
||||
for i, profile := range body.LifecycleProfiles {
|
||||
if len(profile.ClientManagerRef) > 0 {
|
||||
violations = append(violations, fmt.Sprintf("%s.lifecycleProfiles[%d].clientManagerRef is no longer supported", prefix, i))
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
// GamePluginRuntimeProfilesResponseBody is intentionally distinct from the
|
||||
// manifest input shape so browser responses cannot accidentally include
|
||||
// machine-local DLL placement or RCON activation details.
|
||||
@@ -289,7 +253,7 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
||||
profiles.Discovery = append(profiles.Discovery, domain.RuntimeDiscoveryProbe{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Required: item.Required, Expected: item.Expected, Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||
}
|
||||
for _, item := range body.LifecycleProfiles {
|
||||
profiles.LifecycleProfiles = append(profiles.LifecycleProfiles, domain.RuntimeLifecycleProfile{Key: item.Key, Mode: item.Mode, Capabilities: domain.CopyStringSlice(item.Capabilities), ActionRefs: item.ActionRefs.ToDomain(), TransportKeys: domain.CopyStringSlice(item.TransportKeys), ClientManagerRef: item.ClientManagerRef, DLLExtensionRefs: domain.CopyStringSlice(item.DLLExtensionRefs), Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||
profiles.LifecycleProfiles = append(profiles.LifecycleProfiles, domain.RuntimeLifecycleProfile{Key: item.Key, Mode: item.Mode, Capabilities: domain.CopyStringSlice(item.Capabilities), ActionRefs: item.ActionRefs.ToDomain(), TransportKeys: domain.CopyStringSlice(item.TransportKeys), DLLExtensionRefs: domain.CopyStringSlice(item.DLLExtensionRefs), Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||
}
|
||||
for _, item := range body.DependencyProbes {
|
||||
profiles.DependencyProbes = append(profiles.DependencyProbes, domain.RuntimeDependencyProbe{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Required: item.Required, MinimumVersion: item.MinimumVersion, Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||
@@ -329,25 +293,6 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
||||
for _, item := range body.DataTargets {
|
||||
profiles.DataTargets = append(profiles.DataTargets, domain.RuntimeDataTarget{Key: item.Key, Kind: item.Kind, TransportKey: item.TransportKey, SourceRootKey: item.SourceRootKey, SourcePath: item.SourcePath, WorkspaceKey: item.WorkspaceKey, RefreshPolicy: item.RefreshPolicy, MaxBytes: item.MaxBytes, Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||
}
|
||||
for _, item := range body.ClientManagers {
|
||||
manager := domain.RuntimeClientManagerProfile{
|
||||
Key: item.Key, DisplayName: item.DisplayName, Version: item.Version,
|
||||
RepositoryURL: item.Repository.URL, RevisionPolicy: item.Repository.RevisionPolicy, Branch: item.Repository.Branch, Tag: item.Repository.Tag, Revision: item.Repository.Revision,
|
||||
BuildSystem: item.Build.System, WorkspaceRef: item.Build.WorkspaceRef, EntryRef: item.Build.EntryRef, OutputArtifacts: domain.CopyStringSlice(item.OutputArtifacts),
|
||||
Deployment: domain.RuntimeClientManagerDeployment{Mode: item.Deployment.Mode, ExecutableRef: item.Deployment.ExecutableRef, Arguments: domain.CopyStringSlice(item.Deployment.Arguments), AutoStart: item.Deployment.AutoStart, RequiredRunCapabilities: domain.CopyStringSlice(item.Deployment.RequiredRunCapabilities)},
|
||||
Lifecycle: domain.RuntimeClientManagerLifecycle{Actions: domain.CopyStringSlice(item.Lifecycle.Actions), StartupTimeoutSeconds: item.Lifecycle.StartupTimeoutSeconds, StopTimeoutSeconds: item.Lifecycle.StopTimeoutSeconds},
|
||||
Health: domain.RuntimeClientManagerHealth{Mode: item.Health.Mode, IntervalSeconds: item.Health.IntervalSeconds, DegradedAfterSeconds: item.Health.DegradedAfterSeconds, OfflineAfterSeconds: item.Health.OfflineAfterSeconds, RequiredCapabilities: domain.CopyStringSlice(item.Health.RequiredCapabilities)},
|
||||
Compatibility: domain.RuntimeClientManagerCompatibility{MinimumVersion: item.Compatibility.MinimumVersion, MaximumVersion: item.Compatibility.MaximumVersion, AllowDowngrade: item.Compatibility.AllowDowngrade},
|
||||
UpdatePolicy: domain.RuntimeClientManagerUpdatePolicy{Strategy: item.UpdatePolicy.Strategy, RequireApproval: item.UpdatePolicy.RequireApproval, HealthConfirmationSeconds: item.UpdatePolicy.HealthConfirmationSeconds, RetainPrevious: item.UpdatePolicy.RetainPrevious},
|
||||
}
|
||||
for _, target := range item.SupportedTargets {
|
||||
manager.SupportedTargets = append(manager.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
|
||||
}
|
||||
for _, config := range item.ConfigTemplates {
|
||||
manager.ConfigTemplates = append(manager.ConfigTemplates, domain.RuntimeConfigTemplate{Key: config.Key, TemplateRef: config.TemplateRef, OutputRef: config.OutputRef})
|
||||
}
|
||||
profiles.ClientManagers = append(profiles.ClientManagers, manager)
|
||||
}
|
||||
for _, item := range body.DLLExtensions {
|
||||
extension := domain.RuntimeDLLExtensionProfile{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, SCUMExecutableChecksum: item.SCUMExecutableChecksum, UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart, RCONPort: item.RCONPort}
|
||||
for _, target := range item.SupportedTargets {
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
// ClientManagerInstallation is the durable desired/observed lifecycle aggregate for one server/profile.
|
||||
type ClientManagerInstallation struct {
|
||||
// ID is the stable installation identifier.
|
||||
ID string `json:"id" db:"id"`
|
||||
// ServerInstanceID scopes the installation to one authorized server.
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
// PluginID identifies the installed game plugin declaration.
|
||||
PluginID string `json:"pluginId" db:"plugin_id"`
|
||||
// ProfileKey identifies the plugin client-manager profile.
|
||||
ProfileKey string `json:"profileKey" db:"profile_key"`
|
||||
// RunEndpointID is the fenced machine executor assignment.
|
||||
RunEndpointID string `json:"runEndpointId" db:"run_endpoint_id"`
|
||||
// TargetOS is the package operating-system target.
|
||||
TargetOS string `json:"targetOs" db:"target_os"`
|
||||
// TargetArch is the package architecture target.
|
||||
TargetArch string `json:"targetArch" db:"target_arch"`
|
||||
// Status is the durable lifecycle state.
|
||||
Status domain.ClientManagerLifecycleStatus `json:"status" db:"status"`
|
||||
// Phase is a bounded safe execution phase.
|
||||
Phase string `json:"phase" db:"phase"`
|
||||
DesiredVersion string `json:"desiredVersion" db:"desired_version"`
|
||||
ActiveVersion string `json:"activeVersion" db:"active_version"`
|
||||
PreviousVersion string `json:"previousVersion" db:"previous_version"`
|
||||
DesiredRevision string `json:"desiredRevision" db:"desired_revision"`
|
||||
ActiveRevision string `json:"activeRevision" db:"active_revision"`
|
||||
PreviousRevision string `json:"previousRevision" db:"previous_revision"`
|
||||
DesiredArtifactID string `json:"desiredArtifactId" db:"desired_artifact_id"`
|
||||
ActiveArtifactID string `json:"activeArtifactId" db:"active_artifact_id"`
|
||||
PreviousArtifactID string `json:"previousArtifactId" db:"previous_artifact_id"`
|
||||
Checksum string `json:"checksum" db:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration" db:"key_generation"`
|
||||
DeploymentGeneration int `json:"deploymentGeneration" db:"deployment_generation"`
|
||||
CurrentJobID string `json:"currentJobId" db:"current_job_id"`
|
||||
LastSuccessfulJobID string `json:"lastSuccessfulJobId" db:"last_successful_job_id"`
|
||||
LastOperation domain.ClientManagerLifecycleOperation `json:"lastOperation" db:"last_operation"`
|
||||
Health domain.ClientManagerHealthStatus `json:"health" db:"health"`
|
||||
HealthReason string `json:"healthReason" db:"health_reason"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt" db:"last_seen_at"`
|
||||
LastHeartbeatSequence uint64 `json:"lastHeartbeatSequence" db:"last_heartbeat_sequence"`
|
||||
Retryable bool `json:"retryable" db:"retryable"`
|
||||
RequiresRedeploy bool `json:"requiresRedeploy" db:"requires_redeploy"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
InstalledAt time.Time `json:"installedAt" db:"installed_at"`
|
||||
UninstalledAt time.Time `json:"uninstalledAt" db:"uninstalled_at"`
|
||||
}
|
||||
|
||||
func (ClientManagerInstallation) TableName() string { return "client_manager_installations" }
|
||||
|
||||
// ClientManagerSession stores only the hash and fences for a short-lived component session.
|
||||
type ClientManagerSession struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
InstallationID string `json:"installationId" db:"installation_id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
ProfileKey string `json:"profileKey" db:"profile_key"`
|
||||
RunEndpointID string `json:"runEndpointId" db:"run_endpoint_id"`
|
||||
ArtifactID string `json:"artifactId" db:"artifact_id"`
|
||||
KeyGeneration int `json:"keyGeneration" db:"key_generation"`
|
||||
DeploymentGeneration int `json:"deploymentGeneration" db:"deployment_generation"`
|
||||
TokenHash string `json:"tokenHash" db:"token_hash"`
|
||||
Capabilities []string `json:"capabilities" db:"capabilities"`
|
||||
Status domain.ClientManagerSessionStatus `json:"status" db:"status"`
|
||||
LastHeartbeatSequence uint64 `json:"lastHeartbeatSequence" db:"last_heartbeat_sequence"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt" db:"last_seen_at"`
|
||||
ExpiresAt time.Time `json:"expiresAt" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
RevokedAt time.Time `json:"revokedAt" db:"revoked_at"`
|
||||
}
|
||||
|
||||
func (ClientManagerSession) TableName() string { return "client_manager_sessions" }
|
||||
|
||||
// ClientManagerRegistrationNonce is a bounded replay fence for one signed registration request.
|
||||
type ClientManagerRegistrationNonce struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
InstallationID string `json:"installationId" db:"installation_id"`
|
||||
ExpiresAt time.Time `json:"expiresAt" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
}
|
||||
|
||||
func (ClientManagerRegistrationNonce) TableName() string { return "client_manager_registration_nonces" }
|
||||
|
||||
func ClientManagerInstallationFromDomain(value domain.ClientManagerInstallation) ClientManagerInstallation {
|
||||
return ClientManagerInstallation(value)
|
||||
}
|
||||
|
||||
func (value ClientManagerInstallation) ToDomain() domain.ClientManagerInstallation {
|
||||
return domain.ClientManagerInstallation(value)
|
||||
}
|
||||
|
||||
func ClientManagerSessionFromDomain(value domain.ClientManagerSession) ClientManagerSession {
|
||||
value = domain.CopyClientManagerSession(value)
|
||||
return ClientManagerSession(value)
|
||||
}
|
||||
|
||||
func (value ClientManagerSession) ToDomain() domain.ClientManagerSession {
|
||||
return domain.CopyClientManagerSession(domain.ClientManagerSession(value))
|
||||
}
|
||||
|
||||
func ClientManagerRegistrationNonceFromDomain(value domain.ClientManagerRegistrationNonce) ClientManagerRegistrationNonce {
|
||||
return ClientManagerRegistrationNonce(value)
|
||||
}
|
||||
|
||||
func (value ClientManagerRegistrationNonce) ToDomain() domain.ClientManagerRegistrationNonce {
|
||||
return domain.ClientManagerRegistrationNonce(value)
|
||||
}
|
||||
@@ -60,28 +60,6 @@ type RunDistribution struct {
|
||||
|
||||
func (RunDistribution) TableName() string { return "run_distributions" }
|
||||
|
||||
type ClientManagerDistribution struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
PluginID string `json:"pluginId" db:"plugin_id"`
|
||||
ProfileKey string `json:"profileKey" db:"profile_key"`
|
||||
Version string `json:"version" db:"version"`
|
||||
TargetOS string `json:"targetOs" db:"target_os"`
|
||||
TargetArch string `json:"targetArch" db:"target_arch"`
|
||||
RepositoryURL string `json:"repositoryUrl" db:"repository_url"`
|
||||
SourceRevision string `json:"sourceRevision" db:"source_revision"`
|
||||
BuildJobID string `json:"buildJobId" db:"build_job_id"`
|
||||
ArtifactID string `json:"artifactId" db:"artifact_id"`
|
||||
Checksum string `json:"checksum" db:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration" db:"key_generation"`
|
||||
SecretRef string `json:"secretRef" db:"secret_ref"`
|
||||
Status domain.DistributionStatus `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (ClientManagerDistribution) TableName() string { return "client_manager_distributions" }
|
||||
|
||||
type DependencyStatus struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
@@ -103,27 +81,6 @@ type DependencyStatus struct {
|
||||
|
||||
func (DependencyStatus) TableName() string { return "dependency_statuses" }
|
||||
|
||||
type ClientManagerBuildJob struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
PluginID string `json:"pluginId" db:"plugin_id"`
|
||||
ProfileKey string `json:"profileKey" db:"profile_key"`
|
||||
Version string `json:"version" db:"version"`
|
||||
TargetOS string `json:"targetOs" db:"target_os"`
|
||||
TargetArch string `json:"targetArch" db:"target_arch"`
|
||||
RepositoryURL string `json:"repositoryUrl" db:"repository_url"`
|
||||
SourceRevision string `json:"sourceRevision" db:"source_revision"`
|
||||
ArtifactID string `json:"artifactId" db:"artifact_id"`
|
||||
Checksum string `json:"checksum" db:"checksum"`
|
||||
KeyGeneration int `json:"keyGeneration" db:"key_generation"`
|
||||
LogsRef string `json:"logsRef,omitempty" db:"logs_ref"`
|
||||
Status domain.DistributionJobStatus `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (ClientManagerBuildJob) TableName() string { return "client_manager_build_jobs" }
|
||||
|
||||
type RunUpdateJob struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
@@ -195,14 +152,6 @@ func (distribution RunDistribution) ToDomain() domain.RunDistribution {
|
||||
return domain.RunDistribution(distribution)
|
||||
}
|
||||
|
||||
func ClientManagerDistributionFromDomain(distribution domain.ClientManagerDistribution) ClientManagerDistribution {
|
||||
return ClientManagerDistribution(distribution)
|
||||
}
|
||||
|
||||
func (distribution ClientManagerDistribution) ToDomain() domain.ClientManagerDistribution {
|
||||
return domain.ClientManagerDistribution(distribution)
|
||||
}
|
||||
|
||||
func DependencyStatusFromDomain(status domain.DependencyStatus) DependencyStatus {
|
||||
return DependencyStatus(status)
|
||||
}
|
||||
@@ -211,14 +160,6 @@ func (status DependencyStatus) ToDomain() domain.DependencyStatus {
|
||||
return domain.DependencyStatus(status)
|
||||
}
|
||||
|
||||
func ClientManagerBuildJobFromDomain(job domain.ClientManagerBuildJob) ClientManagerBuildJob {
|
||||
return ClientManagerBuildJob(job)
|
||||
}
|
||||
|
||||
func (job ClientManagerBuildJob) ToDomain() domain.ClientManagerBuildJob {
|
||||
return domain.ClientManagerBuildJob(job)
|
||||
}
|
||||
|
||||
func RunUpdateJobFromDomain(job domain.RunUpdateJob) RunUpdateJob {
|
||||
return RunUpdateJob(job)
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ func TestTableNames(t *testing.T) {
|
||||
LogStream{}.TableName(): "log_streams",
|
||||
PluginLifecycleInstallation{}.TableName(): "plugin_lifecycle_installations",
|
||||
AIConfigDiff{}.TableName(): "ai_config_diffs",
|
||||
ClientManagerInstallation{}.TableName(): "client_manager_installations",
|
||||
ClientManagerSession{}.TableName(): "client_manager_sessions",
|
||||
ClientManagerRegistrationNonce{}.TableName(): "client_manager_registration_nonces",
|
||||
}
|
||||
|
||||
for got, want := range tests {
|
||||
|
||||
@@ -16,4 +16,4 @@ Platform owns the reviewable dependency catalog, immutable plan digest, selected
|
||||
3. The terminal staged result moves the safe phase to `restart-requested`. The local journal persists the activation manifest before helper launch. The helper backs up/replaces atomically, starts the new binary with helper environment removed, waits for health, and rolls back on timeout or identity failure.
|
||||
4. The new Run reports success or rollback through signed `update-health` only after registration and job reconciliation. Platform then projects `succeeded` or `rolled-back`; a hello-only outcome is never treated as health confirmation.
|
||||
|
||||
Control heartbeat, job ack/result/cancel/reconcile, durable logs, and artifact upload use independent loops and deadlines. This contract does not include production code signing/KMS, rollout rings/fleet orchestration, client-manager lifecycle, plugin lifecycle, external mirrors/storage, or real AI-provider integration.
|
||||
Control heartbeat, job ack/result/cancel/reconcile, durable logs, and artifact upload use independent loops and deadlines. This contract does not include production code signing/KMS, rollout rings/fleet orchestration, plugin lifecycle, external mirrors/storage, or real AI-provider integration.
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestClientManagerLifecycleRepositoriesPersistAndFilter(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "metadata.json")
|
||||
store, err := NewFileStore(path)
|
||||
if err != nil {
|
||||
t.Fatalf("create file store: %v", err)
|
||||
}
|
||||
stamp := time.Date(2026, 7, 18, 3, 0, 0, 0, time.UTC)
|
||||
installation := domain.ClientManagerInstallation{ID: "cm-install-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client-manager", RunEndpointID: "run-1", TargetOS: "linux", TargetArch: "amd64", Status: domain.ClientManagerLifecycleOnline, Phase: "healthy", ActiveVersion: "1.0.0", ActiveArtifactID: "artifact-1", KeyGeneration: 2, DeploymentGeneration: 3, Health: domain.ClientManagerHealthHealthy, LastSeenAt: stamp, CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := store.ClientManagerInstallations().Create(installation); err != nil {
|
||||
t.Fatalf("persist installation: %v", err)
|
||||
}
|
||||
session := domain.ClientManagerSession{ID: "cm-session-1", InstallationID: installation.ID, ServerInstanceID: installation.ServerInstanceID, ProfileKey: installation.ProfileKey, RunEndpointID: installation.RunEndpointID, ArtifactID: installation.ActiveArtifactID, KeyGeneration: 2, DeploymentGeneration: 3, TokenHash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Capabilities: []string{"component.register", "component.heartbeat"}, Status: domain.ClientManagerSessionActive, LastSeenAt: stamp, ExpiresAt: stamp.Add(15 * time.Minute), CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := store.ClientManagerSessions().Create(session); err != nil {
|
||||
t.Fatalf("persist session: %v", err)
|
||||
}
|
||||
nonce := domain.ClientManagerRegistrationNonce{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", InstallationID: installation.ID, CreatedAt: stamp, ExpiresAt: stamp.Add(5 * time.Minute)}
|
||||
if err := store.ClientManagerNonces().Create(nonce); err != nil {
|
||||
t.Fatalf("persist nonce: %v", err)
|
||||
}
|
||||
|
||||
restarted, err := NewFileStore(path)
|
||||
if err != nil {
|
||||
t.Fatalf("reload file store: %v", err)
|
||||
}
|
||||
installations, err := restarted.ClientManagerInstallations().List(domain.ClientManagerInstallationFilter{ServerInstanceID: "server-1", ProfileKey: "scum-client-manager", Status: domain.ClientManagerLifecycleOnline})
|
||||
if err != nil || len(installations) != 1 || installations[0].DeploymentGeneration != 3 {
|
||||
t.Fatalf("unexpected persisted installations: items=%+v err=%v", installations, err)
|
||||
}
|
||||
sessions, err := restarted.ClientManagerSessions().List(domain.ClientManagerSessionFilter{InstallationID: installation.ID, Status: domain.ClientManagerSessionActive})
|
||||
if err != nil || len(sessions) != 1 {
|
||||
t.Fatalf("unexpected persisted sessions: items=%+v err=%v", sessions, err)
|
||||
}
|
||||
sessions[0].Capabilities[0] = "mutated"
|
||||
stored, _ := restarted.ClientManagerSessions().Get(session.ID)
|
||||
if stored.Capabilities[0] != "component.register" {
|
||||
t.Fatalf("repository returned shared capability storage: %+v", stored)
|
||||
}
|
||||
expired, err := restarted.ClientManagerNonces().List(domain.ClientManagerNonceFilter{ExpiresBefore: stamp.Add(6 * time.Minute)})
|
||||
if err != nil || len(expired) != 1 {
|
||||
t.Fatalf("unexpected nonce expiry filter: items=%+v err=%v", expired, err)
|
||||
}
|
||||
if err := restarted.ClientManagerNonces().Delete(nonce.ID); err != nil {
|
||||
t.Fatalf("delete expired nonce: %v", err)
|
||||
}
|
||||
if _, err := restarted.ClientManagerNonces().Get(nonce.ID); err != ErrNotFound {
|
||||
t.Fatalf("expected deleted nonce, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,7 @@ type StoreSnapshot struct {
|
||||
RuntimeBindings []domain.RuntimeBinding `json:"runtimeBindings"`
|
||||
EncryptedComponentKeys []domain.EncryptedComponentKey `json:"encryptedComponentKeys"`
|
||||
RunDistributions []domain.RunDistribution `json:"runDistributions"`
|
||||
ClientManagerDistributions []domain.ClientManagerDistribution `json:"clientManagerDistributions"`
|
||||
ClientManagerInstallations []domain.ClientManagerInstallation `json:"clientManagerInstallations"`
|
||||
ClientManagerSessions []domain.ClientManagerSession `json:"clientManagerSessions"`
|
||||
ClientManagerNonces []domain.ClientManagerRegistrationNonce `json:"clientManagerNonces"`
|
||||
DependencyStatuses []domain.DependencyStatus `json:"dependencyStatuses"`
|
||||
ClientManagerBuildJobs []domain.ClientManagerBuildJob `json:"clientManagerBuildJobs"`
|
||||
RunUpdateJobs []domain.RunUpdateJob `json:"runUpdateJobs"`
|
||||
LogStreams []domain.LogStream `json:"logStreams"`
|
||||
MetricSamples []domain.MetricSample `json:"metricSamples"`
|
||||
@@ -119,30 +114,10 @@ func (store *FileStore) RunDistributions() RunDistributionRepository {
|
||||
return &persistentRepository[domain.RunDistribution, domain.RunDistributionFilter]{repository: store.MemoryStore.runDists, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *FileStore) ClientManagerDistributions() ClientManagerDistributionRepository {
|
||||
return &persistentRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]{repository: store.MemoryStore.clientDists, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *FileStore) ClientManagerInstallations() ClientManagerInstallationRepository {
|
||||
return &persistentRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]{repository: store.MemoryStore.clientInstalls, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *FileStore) ClientManagerSessions() ClientManagerSessionRepository {
|
||||
return &persistentRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]{repository: store.MemoryStore.clientSessions, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *FileStore) ClientManagerNonces() ClientManagerNonceRepository {
|
||||
return &persistentRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]{repository: store.MemoryStore.clientNonces, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *FileStore) DependencyStatuses() DependencyStatusRepository {
|
||||
return &persistentRepository[domain.DependencyStatus, domain.DependencyStatusFilter]{repository: store.MemoryStore.dependencies, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *FileStore) ClientManagerBuildJobs() ClientManagerBuildJobRepository {
|
||||
return &persistentRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]{repository: store.MemoryStore.buildJobs, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *FileStore) RunUpdateJobs() RunUpdateJobRepository {
|
||||
return &persistentRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]{repository: store.MemoryStore.updateJobs, persist: store.persist}
|
||||
}
|
||||
@@ -239,12 +214,7 @@ func (store *FileStore) snapshot() StoreSnapshot {
|
||||
RuntimeBindings: snapshotRepository(store.MemoryStore.runtimeBindings),
|
||||
EncryptedComponentKeys: snapshotRepository(store.MemoryStore.componentKeys),
|
||||
RunDistributions: snapshotRepository(store.MemoryStore.runDists),
|
||||
ClientManagerDistributions: snapshotRepository(store.MemoryStore.clientDists),
|
||||
ClientManagerInstallations: snapshotRepository(store.MemoryStore.clientInstalls),
|
||||
ClientManagerSessions: snapshotRepository(store.MemoryStore.clientSessions),
|
||||
ClientManagerNonces: snapshotRepository(store.MemoryStore.clientNonces),
|
||||
DependencyStatuses: snapshotRepository(store.MemoryStore.dependencies),
|
||||
ClientManagerBuildJobs: snapshotRepository(store.MemoryStore.buildJobs),
|
||||
RunUpdateJobs: snapshotRepository(store.MemoryStore.updateJobs),
|
||||
LogStreams: snapshotRepository(store.MemoryStore.logStreams),
|
||||
MetricSamples: snapshotRepository(store.MemoryStore.metricSamples),
|
||||
@@ -271,12 +241,7 @@ func (store *FileStore) loadSnapshot(snapshot StoreSnapshot) {
|
||||
loadRepository(store.MemoryStore.runtimeBindings, snapshot.RuntimeBindings)
|
||||
loadRepository(store.MemoryStore.componentKeys, snapshot.EncryptedComponentKeys)
|
||||
loadRepository(store.MemoryStore.runDists, snapshot.RunDistributions)
|
||||
loadRepository(store.MemoryStore.clientDists, snapshot.ClientManagerDistributions)
|
||||
loadRepository(store.MemoryStore.clientInstalls, snapshot.ClientManagerInstallations)
|
||||
loadRepository(store.MemoryStore.clientSessions, snapshot.ClientManagerSessions)
|
||||
loadRepository(store.MemoryStore.clientNonces, snapshot.ClientManagerNonces)
|
||||
loadRepository(store.MemoryStore.dependencies, snapshot.DependencyStatuses)
|
||||
loadRepository(store.MemoryStore.buildJobs, snapshot.ClientManagerBuildJobs)
|
||||
loadRepository(store.MemoryStore.updateJobs, snapshot.RunUpdateJobs)
|
||||
loadRepository(store.MemoryStore.logStreams, snapshot.LogStreams)
|
||||
loadRepository(store.MemoryStore.metricSamples, snapshot.MetricSamples)
|
||||
|
||||
@@ -101,30 +101,10 @@ func (store *MySQLStore) RunDistributions() RunDistributionRepository {
|
||||
return &persistentRepository[domain.RunDistribution, domain.RunDistributionFilter]{repository: store.MemoryStore.runDists, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) ClientManagerDistributions() ClientManagerDistributionRepository {
|
||||
return &persistentRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]{repository: store.MemoryStore.clientDists, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) ClientManagerInstallations() ClientManagerInstallationRepository {
|
||||
return &persistentRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]{repository: store.MemoryStore.clientInstalls, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) ClientManagerSessions() ClientManagerSessionRepository {
|
||||
return &persistentRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]{repository: store.MemoryStore.clientSessions, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) ClientManagerNonces() ClientManagerNonceRepository {
|
||||
return &persistentRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]{repository: store.MemoryStore.clientNonces, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) DependencyStatuses() DependencyStatusRepository {
|
||||
return &persistentRepository[domain.DependencyStatus, domain.DependencyStatusFilter]{repository: store.MemoryStore.dependencies, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) ClientManagerBuildJobs() ClientManagerBuildJobRepository {
|
||||
return &persistentRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]{repository: store.MemoryStore.buildJobs, persist: store.persist}
|
||||
}
|
||||
|
||||
func (store *MySQLStore) RunUpdateJobs() RunUpdateJobRepository {
|
||||
return &persistentRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]{repository: store.MemoryStore.updateJobs, persist: store.persist}
|
||||
}
|
||||
@@ -238,12 +218,7 @@ func (store *MySQLStore) snapshot() StoreSnapshot {
|
||||
RuntimeBindings: snapshotRepository(store.MemoryStore.runtimeBindings),
|
||||
EncryptedComponentKeys: snapshotRepository(store.MemoryStore.componentKeys),
|
||||
RunDistributions: snapshotRepository(store.MemoryStore.runDists),
|
||||
ClientManagerDistributions: snapshotRepository(store.MemoryStore.clientDists),
|
||||
ClientManagerInstallations: snapshotRepository(store.MemoryStore.clientInstalls),
|
||||
ClientManagerSessions: snapshotRepository(store.MemoryStore.clientSessions),
|
||||
ClientManagerNonces: snapshotRepository(store.MemoryStore.clientNonces),
|
||||
DependencyStatuses: snapshotRepository(store.MemoryStore.dependencies),
|
||||
ClientManagerBuildJobs: snapshotRepository(store.MemoryStore.buildJobs),
|
||||
RunUpdateJobs: snapshotRepository(store.MemoryStore.updateJobs),
|
||||
LogStreams: snapshotRepository(store.MemoryStore.logStreams),
|
||||
MetricSamples: snapshotRepository(store.MemoryStore.metricSamples),
|
||||
@@ -270,12 +245,7 @@ func (store *MySQLStore) loadSnapshot(snapshot StoreSnapshot) {
|
||||
loadRepository(store.MemoryStore.runtimeBindings, snapshot.RuntimeBindings)
|
||||
loadRepository(store.MemoryStore.componentKeys, snapshot.EncryptedComponentKeys)
|
||||
loadRepository(store.MemoryStore.runDists, snapshot.RunDistributions)
|
||||
loadRepository(store.MemoryStore.clientDists, snapshot.ClientManagerDistributions)
|
||||
loadRepository(store.MemoryStore.clientInstalls, snapshot.ClientManagerInstallations)
|
||||
loadRepository(store.MemoryStore.clientSessions, snapshot.ClientManagerSessions)
|
||||
loadRepository(store.MemoryStore.clientNonces, snapshot.ClientManagerNonces)
|
||||
loadRepository(store.MemoryStore.dependencies, snapshot.DependencyStatuses)
|
||||
loadRepository(store.MemoryStore.buildJobs, snapshot.ClientManagerBuildJobs)
|
||||
loadRepository(store.MemoryStore.updateJobs, snapshot.RunUpdateJobs)
|
||||
loadRepository(store.MemoryStore.logStreams, snapshot.LogStreams)
|
||||
loadRepository(store.MemoryStore.metricSamples, snapshot.MetricSamples)
|
||||
|
||||
@@ -99,35 +99,6 @@ type RunDistributionRepository interface {
|
||||
Update(domain.RunDistribution) error
|
||||
}
|
||||
|
||||
type ClientManagerDistributionRepository interface {
|
||||
Create(domain.ClientManagerDistribution) error
|
||||
Get(id string) (domain.ClientManagerDistribution, error)
|
||||
List(domain.ClientManagerDistributionFilter) ([]domain.ClientManagerDistribution, error)
|
||||
Update(domain.ClientManagerDistribution) error
|
||||
}
|
||||
|
||||
type ClientManagerInstallationRepository interface {
|
||||
Create(domain.ClientManagerInstallation) error
|
||||
Get(id string) (domain.ClientManagerInstallation, error)
|
||||
List(domain.ClientManagerInstallationFilter) ([]domain.ClientManagerInstallation, error)
|
||||
Update(domain.ClientManagerInstallation) error
|
||||
}
|
||||
|
||||
type ClientManagerSessionRepository interface {
|
||||
Create(domain.ClientManagerSession) error
|
||||
Get(id string) (domain.ClientManagerSession, error)
|
||||
List(domain.ClientManagerSessionFilter) ([]domain.ClientManagerSession, error)
|
||||
Update(domain.ClientManagerSession) error
|
||||
}
|
||||
|
||||
type ClientManagerNonceRepository interface {
|
||||
Create(domain.ClientManagerRegistrationNonce) error
|
||||
Get(id string) (domain.ClientManagerRegistrationNonce, error)
|
||||
List(domain.ClientManagerNonceFilter) ([]domain.ClientManagerRegistrationNonce, error)
|
||||
Update(domain.ClientManagerRegistrationNonce) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type DependencyStatusRepository interface {
|
||||
Create(domain.DependencyStatus) error
|
||||
Get(id string) (domain.DependencyStatus, error)
|
||||
@@ -135,13 +106,6 @@ type DependencyStatusRepository interface {
|
||||
Update(domain.DependencyStatus) error
|
||||
}
|
||||
|
||||
type ClientManagerBuildJobRepository interface {
|
||||
Create(domain.ClientManagerBuildJob) error
|
||||
Get(id string) (domain.ClientManagerBuildJob, error)
|
||||
List(domain.ClientManagerBuildJobFilter) ([]domain.ClientManagerBuildJob, error)
|
||||
Update(domain.ClientManagerBuildJob) error
|
||||
}
|
||||
|
||||
type RunUpdateJobRepository interface {
|
||||
Create(domain.RunUpdateJob) error
|
||||
Get(id string) (domain.RunUpdateJob, error)
|
||||
@@ -233,12 +197,7 @@ type Store interface {
|
||||
RuntimeBindings() RuntimeBindingRepository
|
||||
EncryptedComponentKeys() EncryptedComponentKeyRepository
|
||||
RunDistributions() RunDistributionRepository
|
||||
ClientManagerDistributions() ClientManagerDistributionRepository
|
||||
ClientManagerInstallations() ClientManagerInstallationRepository
|
||||
ClientManagerSessions() ClientManagerSessionRepository
|
||||
ClientManagerNonces() ClientManagerNonceRepository
|
||||
DependencyStatuses() DependencyStatusRepository
|
||||
ClientManagerBuildJobs() ClientManagerBuildJobRepository
|
||||
RunUpdateJobs() RunUpdateJobRepository
|
||||
LogStreams() LogStreamRepository
|
||||
MetricSamples() MetricSampleRepository
|
||||
@@ -264,12 +223,7 @@ type MemoryStore struct {
|
||||
runtimeBindings *memoryRepository[domain.RuntimeBinding, domain.RuntimeBindingFilter]
|
||||
componentKeys *memoryRepository[domain.EncryptedComponentKey, domain.EncryptedComponentKeyFilter]
|
||||
runDists *memoryRepository[domain.RunDistribution, domain.RunDistributionFilter]
|
||||
clientDists *memoryRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]
|
||||
clientInstalls *memoryRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]
|
||||
clientSessions *memoryRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]
|
||||
clientNonces *memoryRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]
|
||||
dependencies *memoryRepository[domain.DependencyStatus, domain.DependencyStatusFilter]
|
||||
buildJobs *memoryRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]
|
||||
updateJobs *memoryRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]
|
||||
logStreams *memoryRepository[domain.LogStream, domain.LogStreamFilter]
|
||||
metricSamples *memoryRepository[domain.MetricSample, domain.MetricSampleFilter]
|
||||
@@ -340,36 +294,11 @@ func NewMemoryStore() *MemoryStore {
|
||||
domain.CopyRunDistribution,
|
||||
matchRunDistribution,
|
||||
),
|
||||
clientDists: newMemoryRepository(
|
||||
func(distribution domain.ClientManagerDistribution) string { return distribution.ID },
|
||||
domain.CopyClientManagerDistribution,
|
||||
matchClientManagerDistribution,
|
||||
),
|
||||
clientInstalls: newMemoryRepository(
|
||||
func(installation domain.ClientManagerInstallation) string { return installation.ID },
|
||||
domain.CopyClientManagerInstallation,
|
||||
matchClientManagerInstallation,
|
||||
),
|
||||
clientSessions: newMemoryRepository(
|
||||
func(session domain.ClientManagerSession) string { return session.ID },
|
||||
domain.CopyClientManagerSession,
|
||||
matchClientManagerSession,
|
||||
),
|
||||
clientNonces: newMemoryRepository(
|
||||
func(nonce domain.ClientManagerRegistrationNonce) string { return nonce.ID },
|
||||
domain.CopyClientManagerRegistrationNonce,
|
||||
matchClientManagerNonce,
|
||||
),
|
||||
dependencies: newMemoryRepository(
|
||||
func(status domain.DependencyStatus) string { return status.ID },
|
||||
domain.CopyDependencyStatus,
|
||||
matchDependencyStatus,
|
||||
),
|
||||
buildJobs: newMemoryRepository(
|
||||
func(job domain.ClientManagerBuildJob) string { return job.ID },
|
||||
domain.CopyClientManagerBuildJob,
|
||||
matchClientManagerBuildJob,
|
||||
),
|
||||
updateJobs: newMemoryRepository(
|
||||
func(job domain.RunUpdateJob) string { return job.ID },
|
||||
domain.CopyRunUpdateJob,
|
||||
@@ -425,22 +354,7 @@ func (store *MemoryStore) EncryptedComponentKeys() EncryptedComponentKeyReposito
|
||||
return store.componentKeys
|
||||
}
|
||||
func (store *MemoryStore) RunDistributions() RunDistributionRepository { return store.runDists }
|
||||
func (store *MemoryStore) ClientManagerDistributions() ClientManagerDistributionRepository {
|
||||
return store.clientDists
|
||||
}
|
||||
func (store *MemoryStore) ClientManagerInstallations() ClientManagerInstallationRepository {
|
||||
return store.clientInstalls
|
||||
}
|
||||
func (store *MemoryStore) ClientManagerSessions() ClientManagerSessionRepository {
|
||||
return store.clientSessions
|
||||
}
|
||||
func (store *MemoryStore) ClientManagerNonces() ClientManagerNonceRepository {
|
||||
return store.clientNonces
|
||||
}
|
||||
func (store *MemoryStore) DependencyStatuses() DependencyStatusRepository { return store.dependencies }
|
||||
func (store *MemoryStore) ClientManagerBuildJobs() ClientManagerBuildJobRepository {
|
||||
return store.buildJobs
|
||||
}
|
||||
func (store *MemoryStore) RunUpdateJobs() RunUpdateJobRepository { return store.updateJobs }
|
||||
func (store *MemoryStore) LogStreams() LogStreamRepository { return store.logStreams }
|
||||
func (store *MemoryStore) MetricSamples() MetricSampleRepository { return store.metricSamples }
|
||||
@@ -659,45 +573,12 @@ func matchRunDistribution(distribution domain.RunDistribution, filter domain.Run
|
||||
(filter.Status == "" || distribution.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchClientManagerDistribution(distribution domain.ClientManagerDistribution, filter domain.ClientManagerDistributionFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || distribution.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ProfileKey == "" || distribution.ProfileKey == filter.ProfileKey) &&
|
||||
(filter.TargetOS == "" || distribution.TargetOS == filter.TargetOS) &&
|
||||
(filter.TargetArch == "" || distribution.TargetArch == filter.TargetArch) &&
|
||||
(filter.Status == "" || distribution.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchClientManagerInstallation(installation domain.ClientManagerInstallation, filter domain.ClientManagerInstallationFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || installation.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ProfileKey == "" || installation.ProfileKey == filter.ProfileKey) &&
|
||||
(filter.RunEndpointID == "" || installation.RunEndpointID == filter.RunEndpointID) &&
|
||||
(filter.Status == "" || installation.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchClientManagerSession(session domain.ClientManagerSession, filter domain.ClientManagerSessionFilter) bool {
|
||||
return (filter.InstallationID == "" || session.InstallationID == filter.InstallationID) &&
|
||||
(filter.ServerInstanceID == "" || session.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ProfileKey == "" || session.ProfileKey == filter.ProfileKey) &&
|
||||
(filter.Status == "" || session.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchClientManagerNonce(nonce domain.ClientManagerRegistrationNonce, filter domain.ClientManagerNonceFilter) bool {
|
||||
return (filter.InstallationID == "" || nonce.InstallationID == filter.InstallationID) &&
|
||||
(filter.ExpiresBefore.IsZero() || nonce.ExpiresAt.Before(filter.ExpiresBefore) || nonce.ExpiresAt.Equal(filter.ExpiresBefore))
|
||||
}
|
||||
|
||||
func matchDependencyStatus(status domain.DependencyStatus, filter domain.DependencyStatusFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || status.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ProbeKey == "" || status.ProbeKey == filter.ProbeKey) &&
|
||||
(filter.State == "" || status.State == filter.State)
|
||||
}
|
||||
|
||||
func matchClientManagerBuildJob(job domain.ClientManagerBuildJob, filter domain.ClientManagerBuildJobFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || job.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.ProfileKey == "" || job.ProfileKey == filter.ProfileKey) &&
|
||||
(filter.Status == "" || job.Status == filter.Status)
|
||||
}
|
||||
|
||||
func matchRunUpdateJob(job domain.RunUpdateJob, filter domain.RunUpdateJobFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || job.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.Status == "" || job.Status == filter.Status)
|
||||
|
||||
@@ -282,15 +282,6 @@ func (svc *CoreService) distributionArtifactDownloadPresentation(artifactID stri
|
||||
}
|
||||
}
|
||||
}
|
||||
clients, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{})
|
||||
if err == nil {
|
||||
for _, distribution := range clients {
|
||||
if distribution.ArtifactID == artifactID {
|
||||
format := packageFormatForTarget(distribution.TargetOS)
|
||||
return distributionPackageFilename(distribution.ProfileKey, distribution.TargetOS, distribution.TargetArch, format), packageContentType(format), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -389,11 +389,6 @@ func TestCoreServiceGeneratedSCUMRunRegistrationDoesNotQueueGuidedStart(t *testi
|
||||
domain.JobCapabilityFilesRead,
|
||||
"files.patch",
|
||||
"logs.read",
|
||||
domain.JobCapabilityClientManagerDeploy,
|
||||
domain.JobCapabilityClientManagerControl,
|
||||
domain.JobCapabilityClientManagerUpdate,
|
||||
domain.JobCapabilityClientManagerRollback,
|
||||
domain.JobCapabilityClientManagerUninstall,
|
||||
"artifacts.read",
|
||||
"artifacts.write",
|
||||
"ai.invoke",
|
||||
|
||||
@@ -169,45 +169,6 @@ func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.D
|
||||
}, nil
|
||||
}
|
||||
|
||||
clientDistributions, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
||||
if err != nil {
|
||||
return domain.DistributionBuildInput{}, err
|
||||
}
|
||||
for _, distribution := range clientDistributions {
|
||||
if distribution.BuildJobID != job.ID {
|
||||
continue
|
||||
}
|
||||
key, err := svc.activeComponentKey(distribution.ServerInstanceID, domain.DistributionComponentClientManager, distribution.ProfileKey)
|
||||
if err != nil {
|
||||
return domain.DistributionBuildInput{}, err
|
||||
}
|
||||
if key.Generation != distribution.KeyGeneration {
|
||||
return domain.DistributionBuildInput{}, validationError("client-manager build key generation is no longer current")
|
||||
}
|
||||
plainKey, err := svc.decryptRuntimeKey(key.EncryptedKey)
|
||||
if err != nil {
|
||||
return domain.DistributionBuildInput{}, err
|
||||
}
|
||||
return domain.DistributionBuildInput{
|
||||
JobID: job.ID,
|
||||
ComponentKind: domain.DistributionComponentClientManager,
|
||||
ServerInstanceID: distribution.ServerInstanceID,
|
||||
PluginID: distribution.PluginID,
|
||||
RunEndpointID: job.RunEndpointID,
|
||||
ProfileKey: distribution.ProfileKey,
|
||||
TargetOS: distribution.TargetOS,
|
||||
TargetArch: distribution.TargetArch,
|
||||
PlatformURL: runReleasePlatformURL(),
|
||||
PackageFormat: packageFormatForTarget(distribution.TargetOS),
|
||||
RepositoryURL: distribution.RepositoryURL,
|
||||
SourceRevision: distribution.SourceRevision,
|
||||
ArtifactID: distribution.ArtifactID,
|
||||
OutputFilename: clientManagerOutputName(distribution.ProfileKey, distribution.TargetOS),
|
||||
SecretRef: distribution.SecretRef,
|
||||
KeyGeneration: distribution.KeyGeneration,
|
||||
AuthKey: plainKey,
|
||||
}, nil
|
||||
}
|
||||
return domain.DistributionBuildInput{}, repo.ErrNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -25,34 +25,6 @@ type progressDistributionBuilder struct {
|
||||
payload []byte
|
||||
}
|
||||
|
||||
type failingBuildProgressStore struct {
|
||||
repo.Store
|
||||
buildJobs repo.ClientManagerBuildJobRepository
|
||||
}
|
||||
|
||||
func (store failingBuildProgressStore) ClientManagerBuildJobs() repo.ClientManagerBuildJobRepository {
|
||||
return store.buildJobs
|
||||
}
|
||||
|
||||
type failWhenDistributionJobRunningRepository struct {
|
||||
repo.ClientManagerBuildJobRepository
|
||||
jobs repo.JobRepository
|
||||
err error
|
||||
}
|
||||
|
||||
func (repository failWhenDistributionJobRunningRepository) List(filter domain.ClientManagerBuildJobFilter) ([]domain.ClientManagerBuildJob, error) {
|
||||
jobs, err := repository.jobs.List(domain.JobFilter{ServerInstanceID: filter.ServerInstanceID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if job.Capability == domain.JobCapabilityDistributionBuild && job.State == domain.JobStateRunning {
|
||||
return nil, repository.err
|
||||
}
|
||||
}
|
||||
return repository.ClientManagerBuildJobRepository.List(filter)
|
||||
}
|
||||
|
||||
func (builder captureDistributionBuilder) Readiness() (bool, string) {
|
||||
return true, ""
|
||||
}
|
||||
@@ -421,49 +393,6 @@ func TestCoreServiceProjectsPlatformBuilderProgressBeforeCompletion(t *testing.T
|
||||
completeDistributionBuild(t, svc, distribution, nil)
|
||||
}
|
||||
|
||||
func TestCoreServiceTerminalizesPlatformBuildWhenRunningProjectionFails(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
const leakedDetail = "sensitive-builder-token /private/platform/build/input/auth-key"
|
||||
baseStore := svc.store
|
||||
svc.store = failingBuildProgressStore{
|
||||
Store: baseStore,
|
||||
buildJobs: failWhenDistributionJobRunningRepository{
|
||||
ClientManagerBuildJobRepository: baseStore.ClientManagerBuildJobs(),
|
||||
jobs: baseStore.Jobs(),
|
||||
err: errors.New(leakedDetail),
|
||||
},
|
||||
}
|
||||
|
||||
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "running-projection-failure-terminalizes",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("queue platform distribution: %v", err)
|
||||
}
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
job, jobErr := svc.GetJob(distribution.BuildJobID)
|
||||
updated, distributionErr := svc.store.RunDistributions().Get(distribution.ID)
|
||||
if jobErr != nil || distributionErr != nil {
|
||||
t.Fatalf("read failed build state: jobErr=%v distributionErr=%v", jobErr, distributionErr)
|
||||
}
|
||||
if isTerminalJobState(job.State) {
|
||||
if job.State != domain.JobStateFailed || updated.Status != domain.DistributionStatusFailed {
|
||||
t.Fatalf("projection failure did not end in failed state: job=%+v distribution=%+v", job, updated)
|
||||
}
|
||||
if strings.Contains(job.Progress.Message, "sensitive-builder-token") || strings.Contains(job.Progress.Message, "/private/") || strings.Contains(job.TerminalFingerprint, "sensitive-builder-token") {
|
||||
t.Fatalf("terminal build state leaked internal failure details: %+v", job)
|
||||
}
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
t.Fatal("platform build remained non-terminal after running projection failure")
|
||||
}
|
||||
|
||||
func TestCoreServiceDiscardsBuildCompletedAfterKeyReset(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
inputs := make(chan domain.DistributionBuildInput, 1)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -77,44 +76,6 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
|
||||
}, nil
|
||||
}
|
||||
|
||||
clientDistributions, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
||||
if err != nil {
|
||||
return domain.DistributionBuildInput{}, err
|
||||
}
|
||||
for _, distribution := range clientDistributions {
|
||||
if distribution.BuildJobID != job.ID {
|
||||
continue
|
||||
}
|
||||
key, err := svc.activeComponentKey(distribution.ServerInstanceID, domain.DistributionComponentClientManager, distribution.ProfileKey)
|
||||
if err != nil {
|
||||
return domain.DistributionBuildInput{}, err
|
||||
}
|
||||
if key.Generation != distribution.KeyGeneration {
|
||||
return domain.DistributionBuildInput{}, validationError("client-manager build key generation is no longer current")
|
||||
}
|
||||
plainKey, err := svc.decryptRuntimeKey(key.EncryptedKey)
|
||||
if err != nil {
|
||||
return domain.DistributionBuildInput{}, err
|
||||
}
|
||||
return domain.DistributionBuildInput{
|
||||
JobID: job.ID,
|
||||
ComponentKind: domain.DistributionComponentClientManager,
|
||||
ServerInstanceID: distribution.ServerInstanceID,
|
||||
PluginID: distribution.PluginID,
|
||||
RunEndpointID: job.RunEndpointID,
|
||||
ProfileKey: distribution.ProfileKey,
|
||||
TargetOS: distribution.TargetOS,
|
||||
TargetArch: distribution.TargetArch,
|
||||
PackageFormat: packageFormatForTarget(distribution.TargetOS),
|
||||
RepositoryURL: distribution.RepositoryURL,
|
||||
SourceRevision: distribution.SourceRevision,
|
||||
ArtifactID: distribution.ArtifactID,
|
||||
OutputFilename: clientManagerOutputName(distribution.ProfileKey, distribution.TargetOS),
|
||||
SecretRef: distribution.SecretRef,
|
||||
KeyGeneration: distribution.KeyGeneration,
|
||||
AuthKey: plainKey,
|
||||
}, nil
|
||||
}
|
||||
return domain.DistributionBuildInput{}, repo.ErrNotFound
|
||||
}
|
||||
|
||||
@@ -122,21 +83,6 @@ func (svc *CoreService) projectDistributionBuildProgress(job domain.Job, stamp t
|
||||
if job.Capability != domain.JobCapabilityDistributionBuild {
|
||||
return nil
|
||||
}
|
||||
builds, err := svc.store.ClientManagerBuildJobs().List(domain.ClientManagerBuildJobFilter{ServerInstanceID: job.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, build := range builds {
|
||||
if build.ID != job.ID || build.Status != domain.DistributionJobStatusQueued {
|
||||
continue
|
||||
}
|
||||
build.Status = domain.DistributionJobStatusRunning
|
||||
build.UpdatedAt = stamp
|
||||
if err := validator.ValidateClientManagerBuildJob(build); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.store.ClientManagerBuildJobs().Update(build)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -148,7 +94,6 @@ func (svc *CoreService) projectDistributionBuildResult(job domain.Job, stamp tim
|
||||
return err
|
||||
}
|
||||
status := domain.DistributionStatusFailed
|
||||
buildStatus := domain.DistributionJobStatusFailed
|
||||
var artifact domain.Artifact
|
||||
if job.State == domain.JobStateSucceeded {
|
||||
artifactID := strings.TrimPrefix(job.ResultRef, "artifact://")
|
||||
@@ -164,7 +109,6 @@ func (svc *CoreService) projectDistributionBuildResult(job domain.Job, stamp tim
|
||||
return validationError("distribution build artifact is unavailable or outside the job scope")
|
||||
}
|
||||
status = domain.DistributionStatusAvailable
|
||||
buildStatus = domain.DistributionJobStatusSucceeded
|
||||
}
|
||||
|
||||
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
||||
@@ -195,57 +139,6 @@ func (svc *CoreService) projectDistributionBuildResult(job domain.Job, stamp tim
|
||||
return svc.store.RunDistributions().Update(distribution)
|
||||
}
|
||||
|
||||
clientDistributions, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, distribution := range clientDistributions {
|
||||
if distribution.BuildJobID != job.ID {
|
||||
continue
|
||||
}
|
||||
if distribution.Status == domain.DistributionStatusRevoked {
|
||||
if artifact.ID != "" {
|
||||
return svc.expireDistributionArtifact(artifact.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if job.State == domain.JobStateSucceeded && artifact.ID != distribution.ArtifactID {
|
||||
return validationError("client-manager build returned an unexpected artifact")
|
||||
}
|
||||
distribution.Status = status
|
||||
if artifact.ID != "" {
|
||||
distribution.Checksum = artifact.Checksum
|
||||
}
|
||||
distribution.UpdatedAt = stamp
|
||||
if err := validator.ValidateClientManagerDistribution(distribution); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.store.ClientManagerDistributions().Update(distribution); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.ProjectClientManagerDistribution(distribution); err != nil {
|
||||
return err
|
||||
}
|
||||
build, err := svc.store.ClientManagerBuildJobs().Get(job.ID)
|
||||
if err != nil && !errors.Is(err, repo.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
build.Status = buildStatus
|
||||
if artifact.ID != "" {
|
||||
build.ArtifactID = artifact.ID
|
||||
build.Checksum = artifact.Checksum
|
||||
}
|
||||
build.UpdatedAt = stamp
|
||||
if err := validator.ValidateClientManagerBuildJob(build); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.store.ClientManagerBuildJobs().Update(build); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return repo.ErrNotFound
|
||||
}
|
||||
|
||||
@@ -265,18 +158,6 @@ func (svc *CoreService) validateDistributionBuildResult(job domain.Job) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
if expectedArtifactID == "" {
|
||||
clientDistributions, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, distribution := range clientDistributions {
|
||||
if distribution.BuildJobID == job.ID {
|
||||
expectedArtifactID = distribution.ArtifactID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if expectedArtifactID == "" {
|
||||
return repo.ErrNotFound
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
@@ -307,15 +305,8 @@ func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.Distrib
|
||||
if len(binary) == 0 {
|
||||
return nil, validationError("platform builder produced an empty distribution executable")
|
||||
}
|
||||
if input.ComponentKind == domain.DistributionComponentRun {
|
||||
return binary, nil
|
||||
}
|
||||
configPayload, err := os.ReadFile(filepath.Join(outputDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, validationError("platform builder did not produce client-manager configuration")
|
||||
}
|
||||
return packageClientManagerDistribution(input.PackageFormat, outputName, binary, configPayload)
|
||||
}
|
||||
|
||||
// prepareRunSource implements the source phase of a Jenkins-style build. A
|
||||
// configured checkout is created when absent, fetched on every build, and
|
||||
@@ -519,36 +510,8 @@ EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$COMPONENT_KIND" != "client-manager" ]; then
|
||||
printf 'unsupported component kind\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$REPOSITORY_URL" in
|
||||
https://*) ;;
|
||||
*) printf 'client-manager repository must use https\n' >&2; exit 2 ;;
|
||||
esac
|
||||
cd /workspace/build
|
||||
progress 24 'git_sync: fetching client-manager source'
|
||||
git init --quiet
|
||||
git remote add origin "$REPOSITORY_URL"
|
||||
git fetch --quiet --depth 1 origin "$SOURCE_REVISION"
|
||||
git checkout --quiet --detach FETCH_HEAD
|
||||
progress 36 'env_check: writing client-manager configuration'
|
||||
{
|
||||
printf 'server_url: "%s"\n' "$PLATFORM_URL"
|
||||
printf 'server_instance_id: "%s"\n' "$SERVER_INSTANCE_ID"
|
||||
printf 'scum_client_credential: "%s"\n' "$auth_key"
|
||||
printf 'scum_client_name: "%s"\n' "$PROFILE_KEY"
|
||||
printf 'scum_client_version: "platform-build"\n'
|
||||
printf 'scum_client_machine_label: "managed-client"\n'
|
||||
printf 'ftp_provider: 3\n'
|
||||
} > config.yaml
|
||||
progress 52 'deps_download: downloading Go modules'
|
||||
go mod download
|
||||
progress 76 'build_compile: compiling client-manager executable'
|
||||
go build -trimpath -ldflags '-s -w' -o "/workspace/output/$OUTPUT_FILENAME" .
|
||||
cp config.yaml /workspace/output/config.yaml
|
||||
progress 88 'package_finalize: client-manager package inputs written'
|
||||
`
|
||||
|
||||
func (builder *DockerDistributionBuilder) containerArgs(input domain.DistributionBuildInput, sourceDir string, inputDir string, buildDir string, outputDir string, goBuildCacheDir string, goModCacheDir string, outputName string) []string {
|
||||
@@ -588,71 +551,6 @@ func (builder *DockerDistributionBuilder) containerArgs(input domain.Distributio
|
||||
}
|
||||
}
|
||||
|
||||
func packageClientManagerDistribution(packageFormat string, outputName string, binary []byte, configPayload []byte) ([]byte, error) {
|
||||
switch packageFormat {
|
||||
case "zip":
|
||||
return zipDistributionFiles(outputName, binary, configPayload)
|
||||
case "tar.gz":
|
||||
return tarGzipDistributionFiles(outputName, binary, configPayload)
|
||||
default:
|
||||
return nil, validationError("platform builder received an unsupported client-manager package format")
|
||||
}
|
||||
}
|
||||
|
||||
func zipDistributionFiles(outputName string, binary []byte, configPayload []byte) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
writer := zip.NewWriter(&buffer)
|
||||
files := []struct {
|
||||
name string
|
||||
mode os.FileMode
|
||||
payload []byte
|
||||
}{{outputName, 0o755, binary}, {"config.yaml", 0o600, configPayload}}
|
||||
for _, file := range files {
|
||||
header := &zip.FileHeader{Name: file.name, Method: zip.Deflate}
|
||||
header.SetMode(file.mode)
|
||||
header.SetModTime(time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
entry, err := writer.CreateHeader(header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := entry.Write(file.payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func tarGzipDistributionFiles(outputName string, binary []byte, configPayload []byte) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
gzipWriter := gzip.NewWriter(&buffer)
|
||||
gzipWriter.Header.ModTime = time.Unix(0, 0).UTC()
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
files := []struct {
|
||||
name string
|
||||
mode int64
|
||||
payload []byte
|
||||
}{{outputName, 0o755, binary}, {"config.yaml", 0o600, configPayload}}
|
||||
for _, file := range files {
|
||||
header := &tar.Header{Name: file.name, Mode: file.mode, Size: int64(len(file.payload)), ModTime: time.Unix(0, 0).UTC()}
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tarWriter.Write(file.payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := gzipWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// safeBuilderFailure keeps host paths and secret values out of reported build
|
||||
// failures.
|
||||
func safeBuilderFailure(output []byte, sensitiveValues ...string) string {
|
||||
|
||||
@@ -2,12 +2,9 @@ package service
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -384,29 +381,6 @@ func TestBuilderJobFailureMessageRedactsSensitiveValuesAndHostPaths(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageClientManagerDistributionProducesProtectedArchives(t *testing.T) {
|
||||
for _, packageFormat := range []string{"zip", "tar.gz"} {
|
||||
t.Run(packageFormat, func(t *testing.T) {
|
||||
payload, err := packageClientManagerDistribution(packageFormat, "client-manager.exe", []byte("binary"), []byte("credential: protected"))
|
||||
if err != nil {
|
||||
t.Fatalf("package client manager: %v", err)
|
||||
}
|
||||
files := readDistributionArchive(t, packageFormat, payload)
|
||||
if string(files["client-manager.exe"].payload) != "binary" || files["client-manager.exe"].mode.Perm() != 0o755 {
|
||||
t.Fatalf("unexpected executable archive entry: %+v", files["client-manager.exe"])
|
||||
}
|
||||
if string(files["config.yaml"].payload) != "credential: protected" || files["config.yaml"].mode.Perm() != 0o600 {
|
||||
t.Fatalf("unexpected config archive entry: %+v", files["config.yaml"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type distributionArchiveFile struct {
|
||||
mode os.FileMode
|
||||
payload []byte
|
||||
}
|
||||
|
||||
func createBuilderSource(t *testing.T) string {
|
||||
t.Helper()
|
||||
sourceDir := t.TempDir()
|
||||
@@ -443,55 +417,3 @@ func builderMountHostPath(t *testing.T, args []string, containerSuffix string) s
|
||||
t.Fatalf("Docker arguments do not mount %s: %+v", containerSuffix, args)
|
||||
return ""
|
||||
}
|
||||
|
||||
func readDistributionArchive(t *testing.T, packageFormat string, payload []byte) map[string]distributionArchiveFile {
|
||||
t.Helper()
|
||||
files := map[string]distributionArchiveFile{}
|
||||
switch packageFormat {
|
||||
case "zip":
|
||||
reader, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
|
||||
if err != nil {
|
||||
t.Fatalf("open zip: %v", err)
|
||||
}
|
||||
for _, entry := range reader.File {
|
||||
body, err := entry.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("open zip entry %s: %v", entry.Name, err)
|
||||
}
|
||||
content, err := io.ReadAll(body)
|
||||
if closeErr := body.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("read zip entry %s: %v", entry.Name, err)
|
||||
}
|
||||
files[entry.Name] = distributionArchiveFile{mode: entry.Mode(), payload: content}
|
||||
}
|
||||
case "tar.gz":
|
||||
gzipReader, err := gzip.NewReader(bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
t.Fatalf("open gzip: %v", err)
|
||||
}
|
||||
tarReader := tar.NewReader(gzipReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("read tar header: %v", err)
|
||||
}
|
||||
content, err := io.ReadAll(tarReader)
|
||||
if err != nil {
|
||||
t.Fatalf("read tar entry %s: %v", header.Name, err)
|
||||
}
|
||||
files[header.Name] = distributionArchiveFile{mode: os.FileMode(header.Mode), payload: content}
|
||||
}
|
||||
if err := gzipReader.Close(); err != nil {
|
||||
t.Fatalf("close gzip: %v", err)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unsupported archive format %q", packageFormat)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
@@ -115,158 +115,6 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
||||
return domain.CopyRunDistribution(distribution), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID string, request domain.ClientManagerBuildRequest) (domain.ClientManagerDistribution, error) {
|
||||
return domain.ClientManagerDistribution{}, legacyClientManagerUnsupported()
|
||||
|
||||
request = domain.CopyClientManagerBuildRequest(request)
|
||||
if strings.TrimSpace(request.IdempotencyKey) == "" {
|
||||
request.IdempotencyKey = "client-manager-" + request.ServerInstanceID + "-" + request.ProfileKey + "-" + request.TargetOS + "-" + request.TargetArch
|
||||
}
|
||||
if err := validator.ValidateClientManagerBuildRequest(request); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
user, err := svc.GetCurrentUser(sessionID)
|
||||
if err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := svc.validateDistributionPluginPermission(user.ID, plugin, instance.ID, "server.client-manager.manage", "client-manager.build.denied"); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := validatePluginTarget(plugin, request.TargetOS); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
profile, err := findRuntimeClientManagerProfile(plugin, request.ProfileKey)
|
||||
if err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if strings.TrimSpace(request.SourceRevision) == "" {
|
||||
request.SourceRevision = clientManagerProfileRevision(profile)
|
||||
}
|
||||
if !clientManagerProfileSupportsTarget(profile, request.TargetOS, request.TargetArch) || request.RepositoryURL != profile.RepositoryURL || !clientManagerProfileAllowsRevision(profile, request.SourceRevision) {
|
||||
return domain.ClientManagerDistribution{}, validationError("client-manager build must match the declared profile repository, revision, and target")
|
||||
}
|
||||
if err := svc.requireCompleteRuntimeBindings(user.ID, instance.ID, "client-manager.build.denied"); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if ready, reason := svc.distributionBuilderReadiness(); !ready {
|
||||
return domain.ClientManagerDistribution{}, validationError(reason)
|
||||
}
|
||||
|
||||
key, _, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentClientManager, request.ProfileKey)
|
||||
if err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
clientDistributionID := distributionID("client-manager-dist", instance.ID, request.ProfileKey, request.TargetOS, request.TargetArch, key.Generation, request.IdempotencyKey)
|
||||
if existing, err := svc.store.ClientManagerDistributions().Get(clientDistributionID); err == nil {
|
||||
return domain.CopyClientManagerDistribution(existing), nil
|
||||
} else if !errors.Is(err, repo.ErrNotFound) {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
|
||||
artifactID := artifactIDForDistribution(clientDistributionID + "-binary")
|
||||
stamp := svc.now()
|
||||
buildJobID := jobIDFromParts("job-distribution-build", instance.ID, clientDistributionID)
|
||||
buildJob := domain.ClientManagerBuildJob{
|
||||
ID: buildJobID,
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
ProfileKey: request.ProfileKey,
|
||||
Version: profile.Version,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
RepositoryURL: request.RepositoryURL,
|
||||
SourceRevision: request.SourceRevision,
|
||||
ArtifactID: artifactID,
|
||||
KeyGeneration: key.Generation,
|
||||
Status: domain.DistributionJobStatusQueued,
|
||||
CreatedAt: stamp,
|
||||
UpdatedAt: stamp,
|
||||
}
|
||||
if err := validator.ValidateClientManagerBuildJob(buildJob); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := svc.store.ClientManagerBuildJobs().Create(buildJob); err != nil {
|
||||
if !errors.Is(err, repo.ErrDuplicate) {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
existing, getErr := svc.store.ClientManagerBuildJobs().Get(buildJob.ID)
|
||||
if getErr != nil {
|
||||
return domain.ClientManagerDistribution{}, getErr
|
||||
}
|
||||
if !sameClientManagerBuildJobArtifacts(existing, buildJob) {
|
||||
return domain.ClientManagerDistribution{}, validationError("client-manager build job already exists with different artifacts")
|
||||
}
|
||||
buildJob = existing
|
||||
}
|
||||
distribution := domain.ClientManagerDistribution{
|
||||
ID: clientDistributionID,
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
ProfileKey: request.ProfileKey,
|
||||
Version: profile.Version,
|
||||
TargetOS: request.TargetOS,
|
||||
TargetArch: request.TargetArch,
|
||||
RepositoryURL: request.RepositoryURL,
|
||||
SourceRevision: request.SourceRevision,
|
||||
BuildJobID: buildJob.ID,
|
||||
ArtifactID: artifactID,
|
||||
KeyGeneration: key.Generation,
|
||||
SecretRef: key.SecretRef,
|
||||
Status: domain.DistributionStatusBuilding,
|
||||
CreatedAt: stamp,
|
||||
UpdatedAt: stamp,
|
||||
}
|
||||
if err := validator.ValidateClientManagerDistribution(distribution); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := svc.store.ClientManagerDistributions().Create(distribution); err != nil {
|
||||
if errors.Is(err, repo.ErrDuplicate) {
|
||||
existing, getErr := svc.store.ClientManagerDistributions().Get(distribution.ID)
|
||||
if getErr != nil {
|
||||
return domain.ClientManagerDistribution{}, getErr
|
||||
}
|
||||
return domain.CopyClientManagerDistribution(existing), nil
|
||||
}
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if err := svc.ProjectClientManagerDistribution(distribution); err != nil {
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: buildJobID,
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: platformDistributionBuilderEndpointID,
|
||||
Capability: domain.JobCapabilityDistributionBuild,
|
||||
TargetKey: "distribution/client-manager/" + request.ProfileKey,
|
||||
InputRef: "input://distribution-build/" + distribution.ID,
|
||||
IdempotencyKey: "distribution-build:" + distribution.ID,
|
||||
Progress: domain.JobProgress{Percent: 0, Message: "build queued"},
|
||||
})
|
||||
if err != nil {
|
||||
buildJob.Status = domain.DistributionJobStatusFailed
|
||||
buildJob.UpdatedAt = svc.now()
|
||||
distribution.Status = domain.DistributionStatusFailed
|
||||
distribution.UpdatedAt = buildJob.UpdatedAt
|
||||
_ = svc.store.ClientManagerBuildJobs().Update(buildJob)
|
||||
_ = svc.store.ClientManagerDistributions().Update(distribution)
|
||||
_ = svc.ProjectClientManagerDistribution(distribution)
|
||||
return domain.ClientManagerDistribution{}, err
|
||||
}
|
||||
if job.ID != buildJobID || job.Capability != domain.JobCapabilityDistributionBuild {
|
||||
return domain.ClientManagerDistribution{}, validationError("distribution build idempotency key conflicts with another job")
|
||||
}
|
||||
svc.enqueueDistributionBuild(job)
|
||||
return domain.CopyClientManagerDistribution(distribution), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) OpenLatestRunDistributionDownloadForSession(sessionID string, serverInstanceID string) (domain.ArtifactDownloadReference, error) {
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
||||
if err != nil {
|
||||
@@ -288,33 +136,6 @@ func (svc *CoreService) OpenLatestRunDistributionDownloadForSession(sessionID st
|
||||
return svc.OpenArtifactDownloadForSession(sessionID, domain.ArtifactDownloadReferenceRequest{ArtifactID: latest.ArtifactID})
|
||||
}
|
||||
|
||||
func (svc *CoreService) OpenLatestClientManagerDistributionDownloadForSession(sessionID string, serverInstanceID string, profileKey string) (domain.ArtifactDownloadReference, error) {
|
||||
return domain.ArtifactDownloadReference{}, legacyClientManagerUnsupported()
|
||||
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
||||
if err != nil {
|
||||
return domain.ArtifactDownloadReference{}, err
|
||||
}
|
||||
filter := domain.ClientManagerDistributionFilter{ServerInstanceID: instance.ID, Status: domain.DistributionStatusAvailable}
|
||||
if strings.TrimSpace(profileKey) != "" {
|
||||
filter.ProfileKey = strings.TrimSpace(profileKey)
|
||||
}
|
||||
distributions, err := svc.store.ClientManagerDistributions().List(filter)
|
||||
if err != nil {
|
||||
return domain.ArtifactDownloadReference{}, err
|
||||
}
|
||||
var latest domain.ClientManagerDistribution
|
||||
for _, distribution := range distributions {
|
||||
if latest.ID == "" || distribution.KeyGeneration > latest.KeyGeneration || (distribution.KeyGeneration == latest.KeyGeneration && distribution.UpdatedAt.After(latest.UpdatedAt)) {
|
||||
latest = distribution
|
||||
}
|
||||
}
|
||||
if latest.ID == "" {
|
||||
return domain.ArtifactDownloadReference{}, repo.ErrNotFound
|
||||
}
|
||||
return svc.OpenArtifactDownloadForSession(sessionID, domain.ArtifactDownloadReferenceRequest{ArtifactID: latest.ArtifactID})
|
||||
}
|
||||
|
||||
func (svc *CoreService) ResetComponentKeyForSession(sessionID string, request domain.ComponentKeyResetRequest) (domain.EncryptedComponentKey, error) {
|
||||
request = domain.CopyComponentKeyResetRequest(request)
|
||||
if err := validator.ValidateComponentKeyResetRequest(request); err != nil {
|
||||
@@ -810,27 +631,6 @@ func (svc *CoreService) revokeComponentDistributions(serverInstanceID string, ki
|
||||
return err
|
||||
}
|
||||
}
|
||||
case domain.DistributionComponentClientManager:
|
||||
distributions, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{ServerInstanceID: serverInstanceID, ProfileKey: componentKey})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, distribution := range distributions {
|
||||
if distribution.KeyGeneration >= latestGeneration || distribution.Status == domain.DistributionStatusRevoked {
|
||||
continue
|
||||
}
|
||||
distribution.Status = domain.DistributionStatusRevoked
|
||||
distribution.UpdatedAt = svc.now()
|
||||
if err := validator.ValidateClientManagerDistribution(distribution); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.store.ClientManagerDistributions().Update(distribution); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.expireDistributionArtifact(distribution.ArtifactID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -929,22 +729,6 @@ func (svc *CoreService) ensureArtifactPayload(artifactID string, payload []byte,
|
||||
return nil
|
||||
}
|
||||
|
||||
func sameClientManagerBuildJobArtifacts(existing domain.ClientManagerBuildJob, expected domain.ClientManagerBuildJob) bool {
|
||||
return existing.ServerInstanceID == expected.ServerInstanceID &&
|
||||
existing.PluginID == expected.PluginID &&
|
||||
existing.ProfileKey == expected.ProfileKey &&
|
||||
existing.Version == expected.Version &&
|
||||
existing.TargetOS == expected.TargetOS &&
|
||||
existing.TargetArch == expected.TargetArch &&
|
||||
existing.RepositoryURL == expected.RepositoryURL &&
|
||||
existing.SourceRevision == expected.SourceRevision &&
|
||||
existing.ArtifactID == expected.ArtifactID &&
|
||||
existing.Checksum == expected.Checksum &&
|
||||
existing.KeyGeneration == expected.KeyGeneration &&
|
||||
existing.LogsRef == expected.LogsRef &&
|
||||
existing.Status == expected.Status
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertDependencyStatus(instance domain.ServerInstance, request domain.DependencyJobRequest, jobID string, required bool, state domain.DependencyState, message string) error {
|
||||
statusID := distributionID("dependency-status", instance.ID, request.ProbeKey)
|
||||
stamp := svc.now()
|
||||
@@ -1070,14 +854,6 @@ func fingerprintForString(value string) string {
|
||||
return hex.EncodeToString(sum[:])[:12]
|
||||
}
|
||||
|
||||
func clientManagerOutputName(profileKey string, targetOS string) string {
|
||||
name := sanitizeIDPart(profileKey)
|
||||
if targetOS == "windows" {
|
||||
return name + ".exe"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func minInt(a int, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
|
||||
@@ -511,30 +511,18 @@ func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRejectsClientManagerDistributionRequests(t *testing.T) {
|
||||
func TestCoreServiceRejectsClientManagerComponentRequests(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
_, err := svc.GenerateClientManagerDistributionForSession(session, domain.ClientManagerBuildRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ProfileKey: "scum-client-manager",
|
||||
TargetOS: "windows",
|
||||
TargetArch: "amd64",
|
||||
RepositoryURL: "https://github.com/F88888/scum_client.git",
|
||||
SourceRevision: "main",
|
||||
IdempotencyKey: "idem-client-manager",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected client-manager distribution request to be rejected")
|
||||
}
|
||||
if _, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentClientManager,
|
||||
ComponentKind: domain.DistributionComponentKind("client-manager"),
|
||||
ComponentKey: "scum-client-manager",
|
||||
}); err == nil || !strings.Contains(err.Error(), "componentKind") {
|
||||
t.Fatalf("expected client-manager key reset to be rejected, got %v", err)
|
||||
}
|
||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
ComponentKind: domain.DistributionComponentClientManager,
|
||||
ComponentKind: domain.DistributionComponentKind("client-manager"),
|
||||
ComponentKey: "scum-client-manager",
|
||||
Generation: 1,
|
||||
Key: "legacy-client-manager-key",
|
||||
|
||||
@@ -256,9 +256,6 @@ func (svc *CoreService) UpdateRunJobProgress(progress domain.RunJobProgress) (do
|
||||
if err := svc.projectDependencyAndRunUpdateProgress(job, stamp); err != nil {
|
||||
return domain.RunJobProgressResult{}, err
|
||||
}
|
||||
if err := svc.projectClientManagerLifecycleProgress(job, stamp); err != nil {
|
||||
return domain.RunJobProgressResult{}, err
|
||||
}
|
||||
return domain.RunJobProgressResult{Accepted: true, Job: assignmentFromJob(job, progress.LeaseToken), ServerTime: stamp}, nil
|
||||
}
|
||||
|
||||
@@ -347,9 +344,6 @@ func (svc *CoreService) CompleteRunJob(result domain.RunJobResult) (domain.RunJo
|
||||
if err := svc.projectDependencyAndRunUpdateResult(job, stamp); err != nil {
|
||||
return domain.RunJobResultResult{}, err
|
||||
}
|
||||
if err := svc.projectClientManagerLifecycleResult(job, stamp); err != nil {
|
||||
return domain.RunJobResultResult{}, err
|
||||
}
|
||||
if err := svc.projectPluginOperationsJobResult(job, stamp); err != nil {
|
||||
return domain.RunJobResultResult{}, err
|
||||
}
|
||||
@@ -425,26 +419,6 @@ func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) e
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "run.update.staged" {
|
||||
return validationError("Run self-update result type is invalid")
|
||||
}
|
||||
case domain.JobCapabilityClientManagerDeploy:
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.deployed" {
|
||||
return validationError("client-manager deploy result type is invalid")
|
||||
}
|
||||
case domain.JobCapabilityClientManagerControl:
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.control" {
|
||||
return validationError("client-manager control result type is invalid")
|
||||
}
|
||||
case domain.JobCapabilityClientManagerUpdate:
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.updated" || result.State == domain.JobStateFailed && result.ExecutionResult.Kind != "" && result.ExecutionResult.Kind != "client-manager.rollback.restored" {
|
||||
return validationError("client-manager update result type is invalid")
|
||||
}
|
||||
case domain.JobCapabilityClientManagerRollback:
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.rolled-back" {
|
||||
return validationError("client-manager rollback result type is invalid")
|
||||
}
|
||||
case domain.JobCapabilityClientManagerUninstall:
|
||||
if result.State == domain.JobStateSucceeded && result.ExecutionResult.Kind != "client-manager.uninstalled" {
|
||||
return validationError("client-manager uninstall result type is invalid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -157,9 +157,6 @@ func runtimeBindingKeys(profiles domain.GamePluginRuntimeProfiles, profile domai
|
||||
}
|
||||
}
|
||||
}
|
||||
if profile.ClientManagerRef != "" {
|
||||
add(profile.ClientManagerRef, true)
|
||||
}
|
||||
required := make([]string, 0, len(requiredSet))
|
||||
for key := range requiredSet {
|
||||
required = append(required, key)
|
||||
|
||||
@@ -93,7 +93,7 @@ func TestCoreServiceSCUMGuidedDeployDispatchesPluginOwnedSupervisedStartAction(t
|
||||
plugin.Version = "1.0.0"
|
||||
plugin.ServerType = "scum"
|
||||
plugin.Status = domain.GamePluginStatusInstalled
|
||||
plugin.RequiredRunCapabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, "process.restart", "files.list", "files.patch", domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall, "artifacts.read", "artifacts.write", "ai.invoke"}
|
||||
plugin.RequiredRunCapabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, "process.restart", "files.list", "files.patch", "artifacts.read", "artifacts.write", "ai.invoke"}
|
||||
plugin.LifecycleActions = domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json"}
|
||||
plugin.RuntimeProfiles.LifecycleProfiles = []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop}}}
|
||||
requireManualSCUMRuntimeBindings(&plugin, "local")
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
var clientManagerIdentifierPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$`)
|
||||
|
||||
func ValidateClientManagerInstallation(value domain.ClientManagerInstallation) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", value.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", value.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", value.PluginID)
|
||||
violations = appendRequired(violations, "profileKey", value.ProfileKey)
|
||||
violations = appendRequired(violations, "runEndpointId", value.RunEndpointID)
|
||||
if !validDistributionLogicalKey(value.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if value.TargetOS != "" || value.TargetArch != "" {
|
||||
violations = appendDistributionTargetViolations(violations, value.TargetOS, value.TargetArch)
|
||||
}
|
||||
if !validClientManagerLifecycleStatus(value.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if !validClientManagerHealth(value.Health) {
|
||||
violations = append(violations, "health is invalid")
|
||||
}
|
||||
if value.KeyGeneration < 0 || value.DeploymentGeneration < 0 {
|
||||
violations = append(violations, "keyGeneration and deploymentGeneration must not be negative")
|
||||
}
|
||||
if value.Checksum != "" && !validSHA256Checksum(value.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
for field, content := range map[string]string{"phase": value.Phase, "healthReason": value.HealthReason} {
|
||||
if len(content) > 240 || unsafeLifecycleText(content) {
|
||||
violations = append(violations, field+" must be bounded and redacted")
|
||||
}
|
||||
}
|
||||
if value.CreatedAt.IsZero() || value.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt and updatedAt are required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerLifecycleTransition(from, to domain.ClientManagerLifecycleStatus) error {
|
||||
if from == to {
|
||||
return nil
|
||||
}
|
||||
allowed := map[domain.ClientManagerLifecycleStatus][]domain.ClientManagerLifecycleStatus{
|
||||
domain.ClientManagerLifecycleRequested: {domain.ClientManagerLifecycleBuilding, domain.ClientManagerLifecycleAvailable, domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleBuilding: {domain.ClientManagerLifecycleAvailable, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleAvailable: {domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleDeploying: {domain.ClientManagerLifecycleInstalled, domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleFailed, domain.ClientManagerLifecycleUninstalled},
|
||||
domain.ClientManagerLifecycleInstalled: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleUninstalled, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleRegistering: {domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleOnline: {domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleDegraded: {domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleOffline: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleUninstalled, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleUpdating: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleRollingBack: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleStopping: {domain.ClientManagerLifecycleInstalled, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUninstalled, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleUninstalled: {domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleFailed},
|
||||
domain.ClientManagerLifecycleFailed: {domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleUninstalled},
|
||||
}
|
||||
for _, candidate := range allowed[from] {
|
||||
if candidate == to {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return finish([]string{"client-manager lifecycle transition is invalid"})
|
||||
}
|
||||
|
||||
func ValidateClientManagerSession(value domain.ClientManagerSession) error {
|
||||
var violations []string
|
||||
for field, content := range map[string]string{"id": value.ID, "installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "profileKey": value.ProfileKey, "runEndpointId": value.RunEndpointID, "artifactId": value.ArtifactID, "tokenHash": value.TokenHash} {
|
||||
violations = appendRequired(violations, field, content)
|
||||
}
|
||||
if value.KeyGeneration <= 0 || value.DeploymentGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration and deploymentGeneration must be positive")
|
||||
}
|
||||
if len(value.TokenHash) != 64 || !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(value.TokenHash) {
|
||||
violations = append(violations, "tokenHash must be a SHA-256 digest")
|
||||
}
|
||||
if !oneOf(string(value.Status), string(domain.ClientManagerSessionActive), string(domain.ClientManagerSessionRevoked), string(domain.ClientManagerSessionExpired)) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
if value.ExpiresAt.IsZero() || value.CreatedAt.IsZero() || value.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "session timestamps are required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerNonce(value domain.ClientManagerRegistrationNonce) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", value.ID)
|
||||
violations = appendRequired(violations, "installationId", value.InstallationID)
|
||||
if len(value.ID) != 64 || !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(value.ID) {
|
||||
violations = append(violations, "id must be a nonce SHA-256 digest")
|
||||
}
|
||||
if value.ExpiresAt.IsZero() || value.CreatedAt.IsZero() || !value.ExpiresAt.After(value.CreatedAt) {
|
||||
violations = append(violations, "nonce expiry must follow creation")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerDeployRequest(value domain.ClientManagerDeployRequest) error {
|
||||
return validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, value.DistributionID, value.IdempotencyKey, value.ExpectedDeploymentGeneration, true)
|
||||
}
|
||||
|
||||
func ValidateClientManagerControlRequest(value domain.ClientManagerControlRequest) error {
|
||||
if !oneOf(string(value.Operation), "start", "stop", "restart", "status", "rollback") {
|
||||
return finish([]string{"operation is invalid"})
|
||||
}
|
||||
return validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, "", value.IdempotencyKey, value.ExpectedDeploymentGeneration, false)
|
||||
}
|
||||
|
||||
func ValidateClientManagerUpdateRequest(value domain.ClientManagerUpdateRequest) error {
|
||||
var violations []string
|
||||
if !value.Approved {
|
||||
violations = append(violations, "approved must be true")
|
||||
}
|
||||
if err := validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, value.DistributionID, value.IdempotencyKey, value.ExpectedDeploymentGeneration, true); err != nil {
|
||||
violations = append(violations, err.Error())
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerUninstallRequest(value domain.ClientManagerUninstallRequest) error {
|
||||
var violations []string
|
||||
if !value.Confirmed {
|
||||
violations = append(violations, "confirmed must be true")
|
||||
}
|
||||
if err := validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, "", value.IdempotencyKey, value.ExpectedDeploymentGeneration, false); err != nil {
|
||||
violations = append(violations, err.Error())
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerRetryRequest(value domain.ClientManagerRetryRequest) error {
|
||||
return validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, "", value.IdempotencyKey, value.ExpectedDeploymentGeneration, false)
|
||||
}
|
||||
|
||||
func ValidateClientManagerLifecycleInputRequest(value domain.ClientManagerLifecycleInputRequest) error {
|
||||
var violations []string
|
||||
for field, content := range map[string]string{"runEndpointId": value.RunEndpointID, "sessionToken": value.SessionToken, "jobId": value.JobID, "leaseToken": value.LeaseToken} {
|
||||
violations = appendRequired(violations, field, content)
|
||||
}
|
||||
if value.Attempt <= 0 {
|
||||
violations = append(violations, "attempt must be positive")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerCompanionConfigInput(value domain.ClientManagerCompanionConfigInput) error {
|
||||
var violations []string
|
||||
if value.SchemaVersion != domain.ClientManagerCompanionConfigSchemaVersion {
|
||||
violations = append(violations, "schemaVersion is invalid")
|
||||
}
|
||||
for field, content := range map[string]string{
|
||||
"configTemplateKey": value.ConfigTemplateKey, "configTemplateRef": value.ConfigTemplateRef, "configOutputRef": value.ConfigOutputRef,
|
||||
"configSchemaRef": value.ConfigSchemaRef, "installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID,
|
||||
"pluginId": value.PluginID, "profileKey": value.ProfileKey, "artifactId": value.ArtifactID, "version": value.Version,
|
||||
"sourceRevision": value.SourceRevision, "targetOs": value.TargetOS, "targetArch": value.TargetArch, "proofMaterialEnv": value.ProofMaterialEnv,
|
||||
} {
|
||||
violations = appendRequired(violations, field, content)
|
||||
}
|
||||
if !clientManagerIdentifierPattern.MatchString(value.ConfigTemplateKey) {
|
||||
violations = append(violations, "configTemplateKey is invalid")
|
||||
}
|
||||
violations = append(violations, validateSafeRelativeRuntimePath("configTemplateRef", value.ConfigTemplateRef)...)
|
||||
if value.ConfigOutputRef != "config.yaml" {
|
||||
violations = append(violations, "configOutputRef must be config.yaml")
|
||||
}
|
||||
if !safeRelativeJSONRef(value.ConfigSchemaRef) {
|
||||
violations = append(violations, "configSchemaRef must be a safe relative JSON reference")
|
||||
}
|
||||
if value.ConfigFormat != "yaml" || value.PlatformBaseURLSource != "run-control" || value.RegistrationProof != "hmac-sha256" || value.ProofMaterialSource != "component-package" || value.SessionMode != "component-session" || value.TLSPolicy != "verify-system-roots" {
|
||||
violations = append(violations, "companion bootstrap security policy is invalid")
|
||||
}
|
||||
if !validCompanionProofEnvironment(value.ProofMaterialEnv) {
|
||||
violations = append(violations, "proofMaterialEnv is invalid")
|
||||
}
|
||||
for field, identifier := range map[string]string{"installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "pluginId": value.PluginID, "profileKey": value.ProfileKey, "artifactId": value.ArtifactID} {
|
||||
if !clientManagerIdentifierPattern.MatchString(identifier) {
|
||||
violations = append(violations, field+" is invalid")
|
||||
}
|
||||
}
|
||||
violations = appendDistributionTargetViolations(violations, value.TargetOS, value.TargetArch)
|
||||
if value.KeyGeneration <= 0 || value.DeploymentGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration and deploymentGeneration must be positive")
|
||||
}
|
||||
if len(value.Capabilities) == 0 || len(value.Capabilities) > 32 {
|
||||
violations = append(violations, "capabilities must be bounded")
|
||||
}
|
||||
capabilities := make(map[string]struct{}, len(value.Capabilities))
|
||||
for _, capability := range value.Capabilities {
|
||||
if !clientManagerIdentifierPattern.MatchString(capability) {
|
||||
violations = append(violations, "capability is invalid")
|
||||
}
|
||||
if _, exists := capabilities[capability]; exists {
|
||||
violations = append(violations, "capabilities must be unique")
|
||||
}
|
||||
capabilities[capability] = struct{}{}
|
||||
}
|
||||
for _, required := range []string{"component.register", "component.heartbeat", "component.health", "game-client.bridge"} {
|
||||
if _, exists := capabilities[required]; !exists {
|
||||
violations = append(violations, "capabilities must include "+required)
|
||||
}
|
||||
}
|
||||
if value.HeartbeatIntervalSeconds < 5 || value.HeartbeatIntervalSeconds > 300 || value.CommandPollIntervalSeconds < 1 || value.CommandPollIntervalSeconds > 60 || value.RequestTimeoutSeconds < 1 || value.RequestTimeoutSeconds > 60 {
|
||||
violations = append(violations, "companion timing policy is invalid")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerRegisterRequest(value domain.ClientManagerRegisterRequest) error {
|
||||
var violations []string
|
||||
for field, content := range map[string]string{"installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "profileKey": value.ProfileKey, "artifactId": value.ArtifactID, "version": value.Version, "sourceRevision": value.SourceRevision, "targetOs": value.TargetOS, "targetArch": value.TargetArch, "nonce": value.Nonce, "signature": value.Signature} {
|
||||
violations = appendRequired(violations, field, content)
|
||||
}
|
||||
violations = appendDistributionTargetViolations(violations, value.TargetOS, value.TargetArch)
|
||||
if value.KeyGeneration <= 0 || value.DeploymentGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration and deploymentGeneration must be positive")
|
||||
}
|
||||
if value.Timestamp.IsZero() {
|
||||
violations = append(violations, "timestamp is required")
|
||||
}
|
||||
if !regexp.MustCompile(`^[A-Za-z0-9_-]{16,128}$`).MatchString(value.Nonce) {
|
||||
violations = append(violations, "nonce is invalid")
|
||||
}
|
||||
if !regexp.MustCompile(`^sha256:[a-f0-9]{64}$`).MatchString(value.Signature) {
|
||||
violations = append(violations, "signature is invalid")
|
||||
}
|
||||
if len(value.Capabilities) == 0 {
|
||||
violations = append(violations, "capabilities must not be empty")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerHeartbeat(value domain.ClientManagerHeartbeat) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "installationId", value.InstallationID)
|
||||
violations = appendRequired(violations, "sessionToken", value.SessionToken)
|
||||
if value.Sequence == 0 {
|
||||
violations = append(violations, "sequence must be positive")
|
||||
}
|
||||
if !validClientManagerHealth(value.Health) || value.Health == domain.ClientManagerHealthUnknown {
|
||||
violations = append(violations, "health is invalid")
|
||||
}
|
||||
if len(value.HealthReason) > 240 || unsafeLifecycleText(value.HealthReason) {
|
||||
violations = append(violations, "healthReason must be bounded and redacted")
|
||||
}
|
||||
if value.SentAt.IsZero() {
|
||||
violations = append(violations, "sentAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func validateClientManagerOperationRequest(serverID, profileKey, distributionID, idempotencyKey string, generation int, distributionRequired bool) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", serverID)
|
||||
violations = appendRequired(violations, "profileKey", profileKey)
|
||||
violations = appendRequired(violations, "idempotencyKey", idempotencyKey)
|
||||
if distributionRequired {
|
||||
violations = appendRequired(violations, "distributionId", distributionID)
|
||||
}
|
||||
if !validDistributionLogicalKey(profileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if !clientManagerIdentifierPattern.MatchString(idempotencyKey) {
|
||||
violations = append(violations, "idempotencyKey is invalid")
|
||||
}
|
||||
if generation < 0 {
|
||||
violations = append(violations, "expectedDeploymentGeneration must not be negative")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func validClientManagerLifecycleStatus(value domain.ClientManagerLifecycleStatus) bool {
|
||||
return oneOf(string(value), "requested", "building", "available", "deploying", "installed", "registering", "online", "degraded", "offline", "updating", "rolling_back", "stopping", "uninstalled", "failed")
|
||||
}
|
||||
|
||||
func validClientManagerHealth(value domain.ClientManagerHealthStatus) bool {
|
||||
return oneOf(string(value), "unknown", "healthy", "degraded", "unhealthy", "offline")
|
||||
}
|
||||
|
||||
func unsafeLifecycleText(value string) bool {
|
||||
lowered := strings.ToLower(strings.TrimSpace(value))
|
||||
return strings.Contains(lowered, "secret://") || strings.Contains(lowered, "bearer ") || strings.Contains(lowered, "password=") || strings.Contains(lowered, "token=") || strings.Contains(lowered, "unix://") || strings.Contains(lowered, "tcp://") || looksLikeRawHostPath(value)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestValidateRuntimeProfilesRejectsLegacyClientManagers(t *testing.T) {
|
||||
profile := domain.RuntimeClientManagerProfile{Key: "scum-client-manager", DisplayName: "SCUM Client Manager"}
|
||||
err := ValidateGamePluginRuntimeProfiles(domain.GamePluginRuntimeProfiles{ClientManagers: []domain.RuntimeClientManagerProfile{profile}})
|
||||
if err == nil || !strings.Contains(err.Error(), "runtimeProfiles.clientManagers is no longer supported") {
|
||||
t.Fatalf("expected legacy client-manager profile rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClientManagerPersistenceContractsFailClosed(t *testing.T) {
|
||||
stamp := time.Date(2026, 7, 18, 4, 0, 0, 0, time.UTC)
|
||||
installation := domain.ClientManagerInstallation{ID: "cm-install-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client-manager", RunEndpointID: "run-1", TargetOS: "linux", TargetArch: "amd64", Status: domain.ClientManagerLifecycleOnline, Phase: "healthy", KeyGeneration: 1, DeploymentGeneration: 1, Health: domain.ClientManagerHealthHealthy, HealthReason: "ready", CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := ValidateClientManagerInstallation(installation); err != nil {
|
||||
t.Fatalf("validate historical installation projection: %v", err)
|
||||
}
|
||||
installation.HealthReason = "Bearer stolen-session"
|
||||
if err := ValidateClientManagerInstallation(installation); err == nil {
|
||||
t.Fatal("expected session-bearing health reason rejection")
|
||||
}
|
||||
session := domain.ClientManagerSession{ID: "cm-session-1", InstallationID: "cm-install-1", ServerInstanceID: "server-1", ProfileKey: "scum-client-manager", RunEndpointID: "run-1", ArtifactID: "artifact-1", KeyGeneration: 1, DeploymentGeneration: 1, TokenHash: strings.Repeat("a", 64), Capabilities: []string{"component.heartbeat"}, Status: domain.ClientManagerSessionActive, ExpiresAt: stamp.Add(time.Minute), CreatedAt: stamp, UpdatedAt: stamp}
|
||||
if err := ValidateClientManagerSession(session); err != nil {
|
||||
t.Fatalf("validate historical hashed session: %v", err)
|
||||
}
|
||||
if err := ValidateClientManagerHeartbeat(domain.ClientManagerHeartbeat{InstallationID: "cm-install-1", SessionToken: "component-session", Sequence: 1, Health: domain.ClientManagerHealthHealthy, HealthReason: "password=leak", Capabilities: []string{"component.heartbeat"}, SentAt: stamp}); err == nil {
|
||||
t.Fatal("expected unsafe heartbeat reason rejection")
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
@@ -57,9 +56,6 @@ func ValidateEncryptedComponentKey(key domain.EncryptedComponentKey) error {
|
||||
if !validDistributionComponentKind(key.ComponentKind) {
|
||||
violations = append(violations, "componentKind is invalid")
|
||||
}
|
||||
if key.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(key.ComponentKey) == "" {
|
||||
violations = append(violations, "componentKey is required for client-manager keys")
|
||||
}
|
||||
if key.ComponentKey != "" && !validDistributionLogicalKey(key.ComponentKey) {
|
||||
violations = append(violations, "componentKey is invalid")
|
||||
}
|
||||
@@ -125,44 +121,6 @@ func ValidateRunDistribution(distribution domain.RunDistribution) error {
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerDistribution(distribution domain.ClientManagerDistribution) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", distribution.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", distribution.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", distribution.PluginID)
|
||||
violations = appendRequired(violations, "profileKey", distribution.ProfileKey)
|
||||
violations = appendRequired(violations, "repositoryUrl", distribution.RepositoryURL)
|
||||
violations = appendRequired(violations, "sourceRevision", distribution.SourceRevision)
|
||||
violations = appendRequired(violations, "buildJobId", distribution.BuildJobID)
|
||||
violations = appendRequired(violations, "artifactId", distribution.ArtifactID)
|
||||
violations = appendRequired(violations, "secretRef", distribution.SecretRef)
|
||||
violations = appendDistributionTargetViolations(violations, distribution.TargetOS, distribution.TargetArch)
|
||||
violations = appendDistributionStatusViolations(violations, distribution.Status)
|
||||
if !validDistributionLogicalKey(distribution.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if distribution.Checksum != "" && !validSHA256Checksum(distribution.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
if distribution.Status == domain.DistributionStatusAvailable && distribution.Checksum == "" {
|
||||
violations = append(violations, "checksum is required when distribution is available")
|
||||
}
|
||||
if distribution.KeyGeneration <= 0 {
|
||||
violations = append(violations, "keyGeneration must be positive")
|
||||
}
|
||||
violations = append(violations, validateRepositoryURL("repositoryUrl", distribution.RepositoryURL)...)
|
||||
if !strings.HasPrefix(distribution.SecretRef, "secret://runtime-keys/") {
|
||||
violations = append(violations, "secretRef must be redacted runtime key ref")
|
||||
}
|
||||
if distribution.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if distribution.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateDependencyStatus(status domain.DependencyStatus) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", status.ID)
|
||||
@@ -205,40 +163,6 @@ func ValidateDependencyStatus(status domain.DependencyStatus) error {
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerBuildJob(job domain.ClientManagerBuildJob) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", job.ID)
|
||||
violations = appendRequired(violations, "serverInstanceId", job.ServerInstanceID)
|
||||
violations = appendRequired(violations, "pluginId", job.PluginID)
|
||||
violations = appendRequired(violations, "profileKey", job.ProfileKey)
|
||||
violations = appendRequired(violations, "repositoryUrl", job.RepositoryURL)
|
||||
violations = appendRequired(violations, "sourceRevision", job.SourceRevision)
|
||||
violations = appendDistributionTargetViolations(violations, job.TargetOS, job.TargetArch)
|
||||
if !validDistributionLogicalKey(job.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if job.Checksum != "" && !validSHA256Checksum(job.Checksum) {
|
||||
violations = append(violations, "checksum must be sha256:<hex>")
|
||||
}
|
||||
if job.LogsRef != "" && !validScopedInputRef(job.LogsRef) {
|
||||
violations = append(violations, "logsRef is not allowed")
|
||||
}
|
||||
if job.KeyGeneration < 0 {
|
||||
violations = append(violations, "keyGeneration must not be negative")
|
||||
}
|
||||
if !validDistributionJobStatus(job.Status) {
|
||||
violations = append(violations, "status is invalid")
|
||||
}
|
||||
violations = append(violations, validateRepositoryURL("repositoryUrl", job.RepositoryURL)...)
|
||||
if job.CreatedAt.IsZero() {
|
||||
violations = append(violations, "createdAt is required")
|
||||
}
|
||||
if job.UpdatedAt.IsZero() {
|
||||
violations = append(violations, "updatedAt is required")
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateRunUpdateJob(job domain.RunUpdateJob) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "id", job.ID)
|
||||
@@ -297,28 +221,6 @@ func ValidateRunDistributionGenerateRequest(request domain.RunDistributionGenera
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateClientManagerBuildRequest(request domain.ClientManagerBuildRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
|
||||
violations = appendRequired(violations, "profileKey", request.ProfileKey)
|
||||
violations = appendRequired(violations, "targetOs", request.TargetOS)
|
||||
violations = appendRequired(violations, "targetArch", request.TargetArch)
|
||||
violations = appendRequired(violations, "repositoryUrl", request.RepositoryURL)
|
||||
violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey)
|
||||
violations = appendDistributionTargetViolations(violations, request.TargetOS, request.TargetArch)
|
||||
if !validDistributionLogicalKey(request.ProfileKey) {
|
||||
violations = append(violations, "profileKey is invalid")
|
||||
}
|
||||
if request.SourceRevision != "" && !validDistributionLogicalKey(request.SourceRevision) {
|
||||
violations = append(violations, "sourceRevision is invalid")
|
||||
}
|
||||
if containsUnsafeRuntimeSecret(request.IdempotencyKey) || looksLikeRawHostPath(request.IdempotencyKey) {
|
||||
violations = append(violations, "idempotencyKey is unsafe")
|
||||
}
|
||||
violations = append(violations, validateRepositoryURL("repositoryUrl", request.RepositoryURL)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateComponentKeyResetRequest(request domain.ComponentKeyResetRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
|
||||
@@ -341,8 +243,8 @@ func ValidateComponentAuthenticationRequest(request domain.ComponentAuthenticati
|
||||
if !validDistributionComponentKind(request.ComponentKind) {
|
||||
violations = append(violations, "componentKind is invalid")
|
||||
}
|
||||
if request.ComponentKind == domain.DistributionComponentClientManager && strings.TrimSpace(request.ComponentKey) == "" {
|
||||
violations = append(violations, "componentKey is required for client-manager")
|
||||
if request.ComponentKind != domain.DistributionComponentRun {
|
||||
violations = append(violations, "componentKind must be run")
|
||||
}
|
||||
if request.ComponentKey != "" && !validDistributionLogicalKey(request.ComponentKey) {
|
||||
violations = append(violations, "componentKey is invalid")
|
||||
@@ -373,20 +275,6 @@ func appendDistributionStatusViolations(violations []string, status domain.Distr
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateRepositoryURL(field string, value string) []string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := url.ParseRequestURI(strings.TrimSpace(value))
|
||||
if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || !strings.HasSuffix(parsed.Path, ".git") {
|
||||
return []string{field + " must be an HTTPS git repository URL"}
|
||||
}
|
||||
for _, reason := range unsafePluginStringReasons(value) {
|
||||
return []string{field + ": " + reason}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validDistributionComponentKind(kind domain.DistributionComponentKind) bool {
|
||||
switch kind {
|
||||
case domain.DistributionComponentRun:
|
||||
|
||||
@@ -109,7 +109,7 @@ func validateRemoteAdapterInputs(field string, inputs map[string]string) []strin
|
||||
}
|
||||
var violations []string
|
||||
for key, value := range inputs {
|
||||
if !clientManagerIdentifierPattern.MatchString(key) || unsafeRemoteAdapterInputKey(key) {
|
||||
if !runtimeIdentifierPattern.MatchString(key) || unsafeRemoteAdapterInputKey(key) {
|
||||
violations = append(violations, field+" key is invalid or unsafe")
|
||||
}
|
||||
limit := 2048
|
||||
|
||||
@@ -33,6 +33,7 @@ var (
|
||||
gameClientBridgeCollectionPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,119}$`)
|
||||
gameClientBridgeFieldPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,79}$`)
|
||||
gameClientBridgeCaptureNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{0,79}$`)
|
||||
runtimeIdentifierPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$`)
|
||||
)
|
||||
|
||||
type ValidationError struct {
|
||||
@@ -487,7 +488,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
commandTypes := map[string]struct{}{}
|
||||
for index, command := range bridge.Commands {
|
||||
prefix := fmt.Sprintf("%s.commands[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(command.Type) || unsafeGameClientBridgeCommandType(command.Type) {
|
||||
if !runtimeIdentifierPattern.MatchString(command.Type) || unsafeGameClientBridgeCommandType(command.Type) {
|
||||
violations = append(violations, prefix+".type is invalid or unsafe")
|
||||
}
|
||||
if _, exists := commandTypes[command.Type]; exists {
|
||||
@@ -514,7 +515,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
for index, snapshot := range bridge.Snapshots {
|
||||
prefix := fmt.Sprintf("%s.snapshots[%d]", field, index)
|
||||
key := snapshot.Type + "\x00" + snapshot.SchemaVersion
|
||||
if !clientManagerIdentifierPattern.MatchString(snapshot.Type) || !clientManagerIdentifierPattern.MatchString(snapshot.SchemaVersion) {
|
||||
if !runtimeIdentifierPattern.MatchString(snapshot.Type) || !runtimeIdentifierPattern.MatchString(snapshot.SchemaVersion) {
|
||||
violations = append(violations, prefix+" type or schemaVersion is invalid")
|
||||
}
|
||||
if _, exists := snapshotTypes[key]; exists {
|
||||
@@ -531,7 +532,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
queryTemplates := map[string]domain.GameClientBridgeQueryTemplateDeclaration{}
|
||||
for index, template := range bridge.QueryTemplates {
|
||||
prefix := fmt.Sprintf("%s.queryTemplates[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(template.Key) {
|
||||
if !runtimeIdentifierPattern.MatchString(template.Key) {
|
||||
violations = append(violations, prefix+".key is invalid")
|
||||
}
|
||||
if _, exists := queryTemplates[template.Key]; exists {
|
||||
@@ -583,7 +584,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
lifecycleProjectionKeys := map[string]struct{}{}
|
||||
for index, projection := range bridge.LifecycleProjections {
|
||||
prefix := fmt.Sprintf("%s.lifecycleProjections[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(projection.Key) {
|
||||
if !runtimeIdentifierPattern.MatchString(projection.Key) {
|
||||
violations = append(violations, prefix+".key is invalid")
|
||||
}
|
||||
if _, exists := lifecycleProjectionKeys[projection.Key]; exists {
|
||||
@@ -620,7 +621,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
features := map[string]domain.GameClientBridgeFeatureDeclaration{}
|
||||
for index, feature := range bridge.Features {
|
||||
prefix := fmt.Sprintf("%s.features[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(feature.Key) || unsafeGameClientBridgeCommandType(feature.Key) {
|
||||
if !runtimeIdentifierPattern.MatchString(feature.Key) || unsafeGameClientBridgeCommandType(feature.Key) {
|
||||
violations = append(violations, prefix+".key is invalid or unsafe")
|
||||
}
|
||||
if _, exists := features[feature.Key]; exists {
|
||||
@@ -637,12 +638,12 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
violations = append(violations, prefix+" must require a handler or event producer")
|
||||
}
|
||||
for _, handler := range feature.RequiredHandlers {
|
||||
if !clientManagerIdentifierPattern.MatchString(handler) {
|
||||
if !runtimeIdentifierPattern.MatchString(handler) {
|
||||
violations = append(violations, prefix+".requiredHandlers contains an invalid handler")
|
||||
}
|
||||
}
|
||||
for _, producer := range feature.RequiredEventProducers {
|
||||
if !clientManagerIdentifierPattern.MatchString(producer) {
|
||||
if !runtimeIdentifierPattern.MatchString(producer) {
|
||||
violations = append(violations, prefix+".requiredEventProducers contains an invalid producer")
|
||||
}
|
||||
}
|
||||
@@ -1916,7 +1917,7 @@ func validatePluginPages(pages []domain.GamePluginPage) []string {
|
||||
violations = append(violations, duplicateViolations(prefix+".permissions", page.Permissions)...)
|
||||
violations = append(violations, validateBridgeActions(prefix+".bridgeActions", page.BridgeActions)...)
|
||||
for _, key := range page.FeatureKeys {
|
||||
if !clientManagerIdentifierPattern.MatchString(key) {
|
||||
if !runtimeIdentifierPattern.MatchString(key) {
|
||||
violations = append(violations, prefix+".featureKeys contains an invalid feature key")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
prefix := fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", profile.Key)...)
|
||||
violations = append(violations, recordRuntimeProfileKey(lifecycleKeys, prefix+".key", profile.Key)...)
|
||||
if !oneOf(profile.Mode, "local-process", "hosted-ftp-rcon", "ftp-only", "custom-client") {
|
||||
if !oneOf(profile.Mode, "local-process", "hosted-ftp-rcon", "ftp-only") {
|
||||
violations = append(violations, prefix+".mode is invalid")
|
||||
}
|
||||
if len(profile.Capabilities) == 0 {
|
||||
@@ -64,9 +64,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
violations = append(violations, validateProfileKey(fmt.Sprintf("%s.transportKeys[%d]", prefix, j), key)...)
|
||||
}
|
||||
violations = append(violations, duplicateViolations(prefix+".transportKeys", profile.TransportKeys)...)
|
||||
if profile.ClientManagerRef != "" {
|
||||
violations = append(violations, prefix+".clientManagerRef is no longer supported")
|
||||
}
|
||||
for j, key := range profile.DLLExtensionRefs {
|
||||
violations = append(violations, validateProfileKey(fmt.Sprintf("%s.dllExtensionRefs[%d]", prefix, j), key)...)
|
||||
}
|
||||
@@ -298,9 +295,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
violations = append(violations, prefix+".transportKey must reference a declared SQLite query transport")
|
||||
}
|
||||
}
|
||||
if len(profiles.ClientManagers) > 0 {
|
||||
violations = append(violations, "runtimeProfiles.clientManagers is no longer supported")
|
||||
}
|
||||
for i, extension := range profiles.DLLExtensions {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.dllExtensions[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", extension.Key)...)
|
||||
|
||||
@@ -241,7 +241,7 @@ export interface RuntimeDiscoveryProbeResponse {
|
||||
|
||||
export interface RuntimeLifecycleProfileResponse {
|
||||
key: string;
|
||||
mode: "local-process" | "hosted-ftp-rcon" | "ftp-only" | "custom-client";
|
||||
mode: "local-process" | "hosted-ftp-rcon" | "ftp-only";
|
||||
capabilities: string[];
|
||||
actionRefs?: Record<string, string>;
|
||||
transportKeys?: string[];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PluginBridgeExecuteEnvelope, PluginBridgeExecutionResult } from "./pluginBridge";
|
||||
import type { GameClientBridgeCommandFilterRequest, GameClientBridgeQueueRequest, GameClientBridgeSnapshotQuery } from "../api/types";
|
||||
import type { GameClientBridgeCommandFilterRequest, GameClientBridgeQueueRequest, GameClientBridgeSnapshotQuery, LogStreamCursorRequest, LogStreamCursorResponse, LogStreamListResponse } from "../api/types";
|
||||
|
||||
export interface PluginDataMutation {
|
||||
operation: "put" | "delete";
|
||||
@@ -20,5 +20,9 @@ export interface PluginPageWorkspaceActions {
|
||||
list: (filter?: GameClientBridgeCommandFilterRequest) => Promise<unknown>;
|
||||
snapshots: (query?: GameClientBridgeSnapshotQuery) => Promise<unknown>;
|
||||
};
|
||||
logs?: {
|
||||
listStreams: () => Promise<LogStreamListResponse>;
|
||||
query: (request: LogStreamCursorRequest) => Promise<LogStreamCursorResponse>;
|
||||
};
|
||||
dispatch?: (envelope: PluginBridgeExecuteEnvelope, signal?: AbortSignal) => Promise<PluginBridgeExecutionResult>;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ const apiMocks = vi.hoisted(() => ({
|
||||
listGameClientBridgeCommands: vi.fn(),
|
||||
listGameClientBridgeSnapshots: vi.fn(),
|
||||
listGamePlugins: vi.fn(),
|
||||
listLogStreams: vi.fn(),
|
||||
listPluginData: vi.fn(),
|
||||
putPluginData: vi.fn(),
|
||||
queueGameClientBridgeCommand: vi.fn(),
|
||||
queryLogStream: vi.fn(),
|
||||
transactPluginData: vi.fn()
|
||||
}));
|
||||
|
||||
@@ -53,7 +55,7 @@ const plugin: GamePluginResponse = {
|
||||
manifestRef: "artifact://manifests/game.scum/1.0.0",
|
||||
createFormSchemaRef: "schemas/create-form.schema.json",
|
||||
requiredRunCapabilities: [],
|
||||
declaredPermissions: ["server.read", "server.game-client.read", "server.game-client.command"],
|
||||
declaredPermissions: ["server.read", "server.logs.read", "server.game-client.read", "server.game-client.command"],
|
||||
permissions: { ai: true, logs: false, files: false, jobs: true, artifacts: false, remoteAccess: false },
|
||||
lifecycleActions: {},
|
||||
bridgeActions: ["server.instances.read"],
|
||||
@@ -64,7 +66,7 @@ const plugin: GamePluginResponse = {
|
||||
bundleKey: "scum-server-plugin",
|
||||
bundleVersion: "1.0.3",
|
||||
bundleIntegritySha256: "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
|
||||
permissions: ["server.read", "server.game-client.read", "server.game-client.command"],
|
||||
permissions: ["server.read", "server.logs.read", "server.game-client.read", "server.game-client.command"],
|
||||
bridgeActions: ["server.instances.read"]
|
||||
}],
|
||||
tags: ["scum"],
|
||||
@@ -97,6 +99,8 @@ beforeEach(() => {
|
||||
apiMocks.getGameClientBridgeCommand.mockResolvedValue({ id: "command-1" });
|
||||
apiMocks.listGameClientBridgeCommands.mockResolvedValue({ items: [], count: 0 });
|
||||
apiMocks.listGameClientBridgeSnapshots.mockResolvedValue({ items: [], count: 0 });
|
||||
apiMocks.listLogStreams.mockResolvedValue({ items: [{ id: "stream-login", serverInstanceId: "server-1", source: "file", streamKey: "scum.login", latestSeq: 2, storageBackend: "local-segments", retentionPolicy: "default", createdAt: "2026-08-15T12:00:00Z", updatedAt: "2026-08-15T12:00:00Z" }], count: 1 });
|
||||
apiMocks.queryLogStream.mockResolvedValue({ logStreamId: "stream-login", entries: [], nextSeq: 2, latestSeq: 2 });
|
||||
apiMocks.executePluginBridge.mockResolvedValue({ requestId: "request-1", action: "server.instances.read", status: "ok", result: { state: "ready" } });
|
||||
bundleMocks.loadPluginPageBundle.mockResolvedValue(({ workspaceActions }: { workspaceActions?: PluginPageWorkspaceActions }) => {
|
||||
capturedWorkspaceActions = workspaceActions;
|
||||
@@ -186,6 +190,7 @@ describe("PluginPageHostPage", () => {
|
||||
const actions = capturedWorkspaceActions;
|
||||
expect(actions?.pluginData).toBeDefined();
|
||||
expect(actions?.gameClient).toBeDefined();
|
||||
expect(actions?.logs).toBeDefined();
|
||||
expect(actions?.dispatch).toBeDefined();
|
||||
|
||||
const mutations = [
|
||||
@@ -211,6 +216,8 @@ describe("PluginPageHostPage", () => {
|
||||
await actions?.gameClient?.get("command-1");
|
||||
await actions?.gameClient?.list(commandFilter);
|
||||
await actions?.gameClient?.snapshots(snapshotQuery);
|
||||
await actions?.logs?.listStreams();
|
||||
await actions?.logs?.query({ logStreamId: "stream-login", afterSeq: 1, limit: 50 });
|
||||
await expect(actions?.dispatch?.(dispatchEnvelope)).resolves.toMatchObject({ status: "ok", result: { state: "ready" } });
|
||||
|
||||
expect(apiMocks.listPluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-1");
|
||||
@@ -221,6 +228,8 @@ describe("PluginPageHostPage", () => {
|
||||
expect(apiMocks.getGameClientBridgeCommand).toHaveBeenCalledWith("server-1", "command-1");
|
||||
expect(apiMocks.listGameClientBridgeCommands).toHaveBeenCalledWith("server-1", commandFilter);
|
||||
expect(apiMocks.listGameClientBridgeSnapshots).toHaveBeenCalledWith("server-1", snapshotQuery);
|
||||
expect(apiMocks.listLogStreams).toHaveBeenCalledWith("server-1");
|
||||
expect(apiMocks.queryLogStream).toHaveBeenCalledWith({ logStreamId: "stream-login", afterSeq: 1, limit: 50 });
|
||||
expect(apiMocks.executePluginBridge).toHaveBeenCalledWith({
|
||||
requestId: "request-1",
|
||||
pluginId: "game.scum",
|
||||
|
||||
@@ -81,6 +81,18 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin, embedded
|
||||
list: (filter) => platformApiClient.listGameClientBridgeCommands(serverId, filter),
|
||||
snapshots: (query) => platformApiClient.listGameClientBridgeSnapshots(serverId, query)
|
||||
},
|
||||
logs: {
|
||||
listStreams: () => {
|
||||
const context = hostContextRef.current;
|
||||
if (!serverId || !context?.permissions.includes("server.logs.read")) throw new Error("plugin page is missing server.logs.read");
|
||||
return platformApiClient.listLogStreams(serverId);
|
||||
},
|
||||
query: (request) => {
|
||||
const context = hostContextRef.current;
|
||||
if (!serverId || !context?.permissions.includes("server.logs.read")) throw new Error("plugin page is missing server.logs.read");
|
||||
return platformApiClient.queryLogStream(request);
|
||||
}
|
||||
},
|
||||
dispatch: (envelope, signal) => {
|
||||
const context = hostContextRef.current;
|
||||
if (!context) return Promise.resolve({ requestId: envelope.requestId, action: envelope.action, status: "error", error: { code: "host_unavailable", message: "plugin bridge host is unavailable" } });
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ Remote access declarations describe whether a plugin can use `ftp`, `rsync`, or
|
||||
Runtime profiles are declarative contracts, not executable scripts. A profile can declare:
|
||||
|
||||
- discovery probes for logical targets such as Java, Steam app, service, file, or toolchain checks.
|
||||
- lifecycle modes such as `local-process`, `hosted-ftp-rcon`, `ftp-only`, or `custom-client`.
|
||||
- lifecycle modes such as `local-process`, `hosted-ftp-rcon`, or `ftp-only`.
|
||||
- dependency probes and typed install plans for supported OS targets.
|
||||
- log sources for stdout/stderr, file tailing, FTP polling, or plugin-declared typed channels. Log bodies remain opaque and verbatim to Run and Platform.
|
||||
- transport profiles for declared file, FTP/rsync, SQL, RCON, and run-mediated operations.
|
||||
|
||||
@@ -8,6 +8,11 @@ export type PluginDataActions = {
|
||||
transact: (collection: string, mutations: PluginDataMutation[]) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export type LogActions = {
|
||||
listStreams: () => Promise<unknown>;
|
||||
query: (request: { logStreamId: string; afterSeq: number; limit: number }) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export type PluginBridgeExecuteEnvelope = { requestId: string; action: string; payload?: Record<string, string> };
|
||||
export type PluginBridgeExecutionResult = { status?: string; result?: Record<string, string>; error?: { message?: string } };
|
||||
|
||||
@@ -73,6 +78,7 @@ export async function queuePlayerAttributePatch(actions: SCUMWorkspaceActions, p
|
||||
|
||||
export type SCUMWorkspaceActions = {
|
||||
pluginData?: PluginDataActions;
|
||||
logs?: LogActions;
|
||||
dispatch?: (envelope: PluginBridgeExecuteEnvelope, signal?: AbortSignal) => Promise<PluginBridgeExecutionResult>;
|
||||
};
|
||||
|
||||
@@ -128,7 +134,8 @@ export const scumCollections = {
|
||||
mapSettings: "scum_map_settings",
|
||||
vehicles: "scum_vehicles",
|
||||
flags: "scum_flags",
|
||||
trajectories: "scum_trajectories"
|
||||
trajectories: "scum_trajectories",
|
||||
logCursors: "scum_log_cursors"
|
||||
} as const;
|
||||
|
||||
type SurfaceKey = keyof SCUMSurfaceData;
|
||||
@@ -144,13 +151,56 @@ const pageCollections: Record<PageKey, SurfaceKey[]> = {
|
||||
|
||||
export async function loadSCUMSurface(actions: SCUMWorkspaceActions, pageKey: string): Promise<SCUMSurfaceData> {
|
||||
if (!actions.pluginData) throw new Error("通用 pluginData 能力不可用。");
|
||||
const canonical = canonicalPageKey(pageKey);
|
||||
if (pageCollections[canonical].includes("players")) await projectSCUMLoginLogs(actions).catch(() => undefined);
|
||||
const data: SCUMSurfaceData = { ...emptySCUMSurfaceData };
|
||||
const keys = pageCollections[canonicalPageKey(pageKey)];
|
||||
const keys = pageCollections[canonical];
|
||||
const records = await Promise.all(keys.map(async (key) => [key, await actions.pluginData!.list(scumCollections[key])] as const));
|
||||
for (const [key, response] of records) data[key] = collectionRecords(response);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function projectSCUMLoginLogs(actions: SCUMWorkspaceActions, limitPerStream = 200): Promise<number> {
|
||||
if (!actions.pluginData || !actions.logs) return 0;
|
||||
const streamResponse = await actions.logs.listStreams();
|
||||
const streams = logStreams(streamResponse).filter((stream) => textValue(stream.streamKey) === "scum.login" && textValue(stream.id));
|
||||
if (!streams.length) return 0;
|
||||
const existingPlayers = collectionRecords(await actions.pluginData.list(scumCollections.players));
|
||||
const playerByIdentity = playerIndex(existingPlayers);
|
||||
const playerMutations = new Map<string, PluginDataMutation>();
|
||||
const activityMutations: PluginDataMutation[] = [];
|
||||
let projected = 0;
|
||||
for (const stream of streams) {
|
||||
const streamId = textValue(stream.id);
|
||||
const cursorKey = `scum.login:${streamId}`;
|
||||
const cursor = firstCollectionRecord(await actions.pluginData.list(scumCollections.logCursors, cursorKey));
|
||||
const afterSeq = Math.max(0, numberValue(cursor?.nextSeq ?? cursor?.lastSeq));
|
||||
const response = await actions.logs.query({ logStreamId: streamId, afterSeq, limit: limitPerStream });
|
||||
const entries = logEntries(response);
|
||||
let lastSeq = afterSeq;
|
||||
for (const entry of entries) {
|
||||
const seq = numberValue(entry.seq);
|
||||
if (seq > lastSeq) lastSeq = seq;
|
||||
const event = parseSCUMLoginLogEntry(entry, streamId);
|
||||
if (!event) continue;
|
||||
const match = findPlayer(playerByIdentity, event);
|
||||
const playerKeyValue = match ? textValue(existingPlayers[match.index]._recordKey) || scumLogPlayerKey(event) : scumLogPlayerKey(event);
|
||||
const existing = match ? existingPlayers[match.index] : {};
|
||||
const player = scumLoginPlayerRecord(existing, event);
|
||||
playerMutations.set(playerKeyValue, { operation: "put", key: playerKeyValue, value: player });
|
||||
if (match) existingPlayers[match.index] = { ...player, _recordKey: playerKeyValue };
|
||||
else { existingPlayers.push({ ...player, _recordKey: playerKeyValue }); addPlayerToIndex(playerByIdentity, player, existingPlayers.length - 1); }
|
||||
activityMutations.push({ operation: "put", key: `${streamId}:${seq || stableTextHash(textValue(event.rawLine))}`, value: scumLoginActivityRecord(event) });
|
||||
projected++;
|
||||
}
|
||||
const nextSeq = Math.max(lastSeq, numberValue((response as RecordMap).nextSeq), numberValue((response as RecordMap).latestSeq));
|
||||
if (nextSeq > afterSeq) await actions.pluginData.put(scumCollections.logCursors, cursorKey, { streamId, streamKey: "scum.login", nextSeq, updatedAt: new Date().toISOString() });
|
||||
}
|
||||
if (playerMutations.size) await actions.pluginData.transact(scumCollections.players, [...playerMutations.values()]);
|
||||
if (activityMutations.length) await actions.pluginData.transact(scumCollections.activityEvents, activityMutations);
|
||||
return projected;
|
||||
}
|
||||
|
||||
export function mergePlayerSnapshots(players: RecordMap[], playersResponse: unknown): RecordMap[] {
|
||||
const playerSnapshot = latestSnapshotPayload(playersResponse);
|
||||
const merged = players.map((player) => ({ ...player }));
|
||||
@@ -322,6 +372,69 @@ function collectionRecords(response: unknown): RecordMap[] {
|
||||
});
|
||||
}
|
||||
|
||||
function firstCollectionRecord(response: unknown): RecordMap | undefined { return collectionRecords(response)[0]; }
|
||||
|
||||
function logStreams(response: unknown): RecordMap[] { return isRecord(response) && Array.isArray(response.items) ? response.items.filter(isRecord) : []; }
|
||||
function logEntries(response: unknown): RecordMap[] { return isRecord(response) && Array.isArray(response.entries) ? response.entries.filter(isRecord) : []; }
|
||||
|
||||
function parseSCUMLoginLogEntry(entry: RecordMap, streamId: string): RecordMap | null {
|
||||
const line = textValue(entry.line);
|
||||
if (!line.trim()) return null;
|
||||
const fields = isRecord(entry.fields) ? entry.fields : {};
|
||||
const explicitType = firstText(fields, "eventType", "type", "action").toLowerCase();
|
||||
const lowered = line.toLowerCase();
|
||||
const logout = /\b(logged\s*out|logout|disconnected|left)\b/.test(explicitType) || /(?:\blogged\s*out\b|\blogout\s*:|\bdisconnected\b|\bleft\s+the\s+server\b)/i.test(line);
|
||||
const login = /\b(logged\s*in|login|connected|joined)\b/.test(explicitType) || /(?:\blogged\s*in\b|\blogin\s*:|\bconnected\b|\bjoined\s+the\s+server\b)/i.test(line);
|
||||
if (!login && !logout) return null;
|
||||
const steamId = firstText(fields, "steamId", "steamID", "userId", "userID", "playerId", "gamePlayerId") || firstRegex(line, /\b\d{17}\b/);
|
||||
const displayName = firstText(fields, "displayName", "playerName", "name", "characterName") || extractSCUMPlayerName(line, steamId);
|
||||
const ip = firstText(fields, "lastLoginIp", "loginIp", "ipAddress", "ip") || firstRegex(line, /\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
||||
if (!steamId && !displayName && !ip) return null;
|
||||
const occurredAt = textValue(entry.timestamp) || new Date().toISOString();
|
||||
return { eventType: logout ? "logout" : "login", steamId, gamePlayerId: steamId, displayName, lastLoginIp: ip, loginIp: ip, occurredAt, observedAt: occurredAt, rawLine: line, streamId, seq: numberValue(entry.seq), source: "plugin.log.scum.login" };
|
||||
}
|
||||
|
||||
function scumLoginPlayerRecord(existing: RecordMap, event: RecordMap): RecordMap {
|
||||
const occurredAt = textValue(event.occurredAt);
|
||||
const login = textValue(event.eventType) === "login";
|
||||
const identity = { steamId: textValue(event.steamId), gamePlayerId: textValue(event.gamePlayerId), displayName: textValue(event.displayName) };
|
||||
const network = textValue(event.lastLoginIp) ? { lastLoginIp: textValue(event.lastLoginIp), loginIp: textValue(event.loginIp) } : {};
|
||||
const status = login
|
||||
? { online: true, status: "online", lastSeenAt: occurredAt, lastLoginAt: occurredAt, lastLoginObservedAt: occurredAt, lastLoginRawLine: textValue(event.rawLine) }
|
||||
: { online: false, status: "offline", lastSeenAt: occurredAt, lastLogoutAt: occurredAt, lastLogoutObservedAt: occurredAt, lastLogoutRawLine: textValue(event.rawLine) };
|
||||
return { ...existing, ...withoutEmpty(identity), ...network, ...status, source: "plugin.log.scum.login", updatedAt: occurredAt };
|
||||
}
|
||||
|
||||
function scumLoginActivityRecord(event: RecordMap): RecordMap {
|
||||
return withoutEmpty({ id: `${textValue(event.streamId)}:${numberValue(event.seq) || stableTextHash(textValue(event.rawLine))}`, eventType: textValue(event.eventType), type: textValue(event.eventType), steamId: textValue(event.steamId), gamePlayerId: textValue(event.gamePlayerId), displayName: textValue(event.displayName), lastLoginIp: textValue(event.lastLoginIp), loginIp: textValue(event.loginIp), occurredAt: textValue(event.occurredAt), observedAt: textValue(event.observedAt), rawLine: textValue(event.rawLine), streamId: textValue(event.streamId), seq: numberValue(event.seq), source: "plugin.log.scum.login" });
|
||||
}
|
||||
|
||||
function scumLogPlayerKey(event: RecordMap): string {
|
||||
const identity = firstText(event, "steamId", "gamePlayerId");
|
||||
if (identity) return identity;
|
||||
const name = textValue(event.displayName).trim().toLowerCase();
|
||||
if (name) return `name:${safeCommandId(name)}`;
|
||||
return `log:${stableTextHash(textValue(event.rawLine))}`;
|
||||
}
|
||||
|
||||
function extractSCUMPlayerName(line: string, steamId: string): string {
|
||||
for (const pattern of [/\b(?:player|user|name|character)\s*[:=]\s*["']?([^"'(),;\[\]]{2,80})/i, /\b(?:player|user|character)\s+["']([^"']{2,80})["']/i, /["']([^"']{2,80})["']\s*(?:\(|\[|SteamID\s*)?\d{17}/i, /\d{17}\s*(?:\)|\])?\s*["']([^"']{2,80})["']/]) {
|
||||
const match = pattern.exec(line);
|
||||
if (match?.[1]) return match[1].trim();
|
||||
}
|
||||
if (steamId) {
|
||||
const index = line.indexOf(steamId);
|
||||
const before = index > 0 ? line.slice(0, index).replace(/.*\b(?:player|user|character)\b\s*/i, "").replace(/[\[(,:;-]+$/g, "").trim() : "";
|
||||
if (before && before.length <= 80) return before;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function firstRegex(value: string, pattern: RegExp): string { return pattern.exec(value)?.[0] ?? ""; }
|
||||
function numberValue(value: unknown): number { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; }
|
||||
function withoutEmpty(value: RecordMap): RecordMap { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== "" && item !== undefined && item !== null)); }
|
||||
function stableTextHash(value: string): string { let hash = 2166136261; for (let index = 0; index < value.length; index++) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0).toString(16); }
|
||||
|
||||
function latestSnapshotPayload(response: unknown): RecordMap | undefined {
|
||||
if (!isRecord(response) || !Array.isArray(response.items)) return undefined;
|
||||
const snapshots = response.items.filter(isRecord).sort((left, right) => snapshotOrder(right) - snapshotOrder(left));
|
||||
|
||||
@@ -1064,6 +1064,7 @@
|
||||
"bundleIntegritySha256": "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
|
||||
"permissions": [
|
||||
"server.read",
|
||||
"server.logs.read",
|
||||
"server.remote.access",
|
||||
"server.game-client.read",
|
||||
"server.game-client.command",
|
||||
|
||||
@@ -521,7 +521,7 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": { "$ref": "#/$defs/logicalKey" },
|
||||
"mode": { "enum": ["local-process", "hosted-ftp-rcon", "ftp-only", "custom-client"] },
|
||||
"mode": { "enum": ["local-process", "hosted-ftp-rcon", "ftp-only"] },
|
||||
"capabilities": { "type": "array", "items": { "$ref": "#/$defs/runCapability" }, "uniqueItems": true, "minItems": 1 },
|
||||
"actionRefs": {
|
||||
"type": "object",
|
||||
|
||||
@@ -369,7 +369,7 @@ export interface RuntimeDiscoveryProbe {
|
||||
|
||||
export interface RuntimeLifecycleProfile {
|
||||
key: string;
|
||||
mode: "local-process" | "hosted-ftp-rcon" | "ftp-only" | "custom-client";
|
||||
mode: "local-process" | "hosted-ftp-rcon" | "ftp-only";
|
||||
capabilities: RunCapability[];
|
||||
actionRefs?: Partial<Record<PluginLifecycleAction, string>>;
|
||||
transportKeys?: string[];
|
||||
|
||||
@@ -426,7 +426,7 @@ describe("plugin manifest validation", () => {
|
||||
pages: Array<{ pageKey: string; commandTypes?: string[]; queryTemplateKeys?: string[] }>;
|
||||
};
|
||||
pages: Array<{ key: string; permissions?: string[]; bridgeActions?: string[] }>;
|
||||
runtimeProfiles?: { transportProfiles?: Array<{ key: string; kind: string; targetKey?: string; capabilities: string[] }> };
|
||||
runtimeProfiles?: { transportProfiles?: Array<{ key: string; kind: string; targetKey?: string; capabilities: string[] }>; dataTargets?: unknown[] };
|
||||
};
|
||||
const expectedKeys = ["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions", "scum.tasks", "scum.events", "scum.native-timed-gifts"];
|
||||
const expectedColumnsByKey: Record<string, string[]> = {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { migrateConfigurationRecord, migrateGiftGrantRecord, migratePlayerProfileRecord, migratePlayerRecord, migrateStatePatchRecord, migrateTrajectoryHistoryRecord, migrateTrajectoryRecord, migrationStatus } from "../examples/scum-server-plugin/features/migration.js";
|
||||
import { buildPlayerAttributeMutation, createGiftDelivery, deleteGiftDefinition, loadSCUMSurface, mergePlayerSnapshots, parseGiftCommands, parseGiftItems, playerAttributeDrafts, playerAttributeSqlPreview, queueGiftDelivery, queuePlayerAttributePatch, resetGiftClaim, resetPendingGift, resolveMapBounds, saveEventProduce, saveGiftDefinition, saveMapSettings, scumCollections, startEvent, type RecordMap, type SCUMSurfaceData, type SCUMWorkspaceActions } from "../examples/scum-server-plugin/features/page-data.js";
|
||||
import { buildPlayerAttributeMutation, createGiftDelivery, deleteGiftDefinition, loadSCUMSurface, mergePlayerSnapshots, parseGiftCommands, parseGiftItems, playerAttributeDrafts, playerAttributeSqlPreview, projectSCUMLoginLogs, queueGiftDelivery, queuePlayerAttributePatch, resetGiftClaim, resetPendingGift, resolveMapBounds, saveEventProduce, saveGiftDefinition, saveMapSettings, scumCollections, startEvent, type RecordMap, type SCUMSurfaceData, type SCUMWorkspaceActions } from "../examples/scum-server-plugin/features/page-data.js";
|
||||
import { collectMapPoints, mapPointStyle } from "../examples/scum-server-plugin/features/page.js";
|
||||
import { renderPluginPage } from "../examples/scum-server-plugin/page-bundle/index.js";
|
||||
import { configurationCatalog, validateConfigPatch, validateStatePatch, validateVehicleSpawn, vehicleSpawnCatalog } from "../examples/scum-server-plugin/features/schemas.js";
|
||||
@@ -103,6 +103,27 @@ describe("SCUM plugin feature module", () => {
|
||||
expect(dataClientSource).not.toContain("scum-client-manager");
|
||||
});
|
||||
|
||||
it("projects SCUM login log lines inside the plugin-owned page module", async () => {
|
||||
const pluginData = pluginDataActions({ list: async (collection, key) => {
|
||||
if (collection === scumCollections.players) return { items: [{ key: "76561198000000001", value: { steamId: "76561198000000001", displayName: "Old", normalBalance: 100 } }] };
|
||||
if (collection === scumCollections.logCursors && key) return { items: [{ key, value: { nextSeq: 1 } }] };
|
||||
return { items: [], count: 0 };
|
||||
} });
|
||||
const logs = {
|
||||
listStreams: vi.fn(async () => ({ items: [{ id: "stream-login", streamKey: "scum.login", latestSeq: 3 }], count: 1 })),
|
||||
query: vi.fn(async () => ({ logStreamId: "stream-login", entries: [
|
||||
{ seq: 2, timestamp: "2026-08-10T00:01:00Z", line: "Login: Player 'Mira' SteamID 76561198000000001 IP 203.0.113.7" },
|
||||
{ seq: 3, timestamp: "2026-08-10T00:04:00Z", line: "Logout: Player 'Mira' SteamID 76561198000000001 IP 203.0.113.7" }
|
||||
], nextSeq: 3, latestSeq: 3 }))
|
||||
};
|
||||
await expect(projectSCUMLoginLogs({ pluginData, logs })).resolves.toBe(2);
|
||||
expect(logs.query).toHaveBeenCalledWith({ logStreamId: "stream-login", afterSeq: 1, limit: 200 });
|
||||
expect(pluginData.transact).toHaveBeenCalledWith(scumCollections.players, [expect.objectContaining({ key: "76561198000000001", value: expect.objectContaining({ displayName: "Mira", online: false, status: "offline", normalBalance: 100, lastLoginIp: "203.0.113.7", lastLogoutObservedAt: "2026-08-10T00:04:00Z", source: "plugin.log.scum.login" }) })]);
|
||||
expect(pluginData.transact).toHaveBeenCalledWith(scumCollections.activityEvents, expect.arrayContaining([expect.objectContaining({ key: "stream-login:2", value: expect.objectContaining({ eventType: "login", rawLine: expect.stringContaining("Login:") }) }), expect.objectContaining({ key: "stream-login:3", value: expect.objectContaining({ eventType: "logout", rawLine: expect.stringContaining("Logout:") }) })]));
|
||||
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.logCursors, "scum.login:stream-login", expect.objectContaining({ nextSeq: 3 }));
|
||||
expect(dataClientSource).toContain("projectSCUMLoginLogs");
|
||||
});
|
||||
|
||||
it("uses workflows as the manifest activity key and keeps activity as a compatibility alias", async () => {
|
||||
const list = vi.fn(async (collection: string) => ({ items: [{ key: `${collection}-1`, value: { collection } }], count: 1 }));
|
||||
await loadSCUMSurface({ pluginData: pluginDataActions({ list }) }, "workflows");
|
||||
|
||||
@@ -1106,8 +1106,7 @@ const localRuntimeProfiles = {
|
||||
}],
|
||||
logSources: source.runtimeProfiles?.logSources ?? [],
|
||||
transportProfiles: source.runtimeProfiles?.transportProfiles ?? [],
|
||||
dataTargets: source.runtimeProfiles?.dataTargets ?? [],
|
||||
clientManagers: []
|
||||
dataTargets: source.runtimeProfiles?.dataTargets ?? []
|
||||
};
|
||||
const localGameClientBridge = JSON.parse(JSON.stringify(source.gameClientBridge ?? {}));
|
||||
delete localGameClientBridge.companion;
|
||||
|
||||
Reference in New Issue
Block a user