feat: add UE4SS DLL runtime extension
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-22
|
||||
@@ -0,0 +1,87 @@
|
||||
## Context
|
||||
|
||||
`game.scum` currently declares a Windows client-manager package that is built from a Git repository. The replacement is not a user-built executable: it is a publisher-built UE4SS `main.dll` that UE4SS loads into SCUM during the normal server process startup. The supplied RCON project requires UE4SS EngineTick and therefore cannot be loaded by an external EXE or attached safely to an already running SCUM process.
|
||||
|
||||
The Platform already owns plugin manifest validation, runtime binding selection, lifecycle job dispatch, and safe browser DTOs. Run already owns bounded HTTPS dependency downloads, scoped workspaces, direct executable vectors, and lifecycle process supervision. Neither currently has a typed server DLL extension plan or a real RCON adapter.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Let a plugin declare a prebuilt Windows UE4SS DLL release and update policy without exposing user-side compilation, arbitrary commands, host paths, or secrets.
|
||||
- Have Run synchronize the declared release automatically before each SCUM `process.start`, only after Platform has frozen the declaration into the leased job input.
|
||||
- Stage the DLL atomically below a logical server workspace, preserve the previous managed DLL for rollback, generate a local-only RCON configuration, and load through the normal UE4SS startup chain.
|
||||
- Expose safe declaration and synchronization state to the existing plugin/server management views.
|
||||
- Reject Linux for this Windows DLL extension explicitly.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Do not run a compiler, CMake, UE4SS source checkout, or arbitrary executable on an operator machine.
|
||||
- Do not inject a live process, use `rundll32`, `LD_PRELOAD`, remote threads, manual mapping, or a generic DLL loader.
|
||||
- Do not claim a Linux `.dll` equivalent. A future Linux `.so` loader is a separate, game-specific change.
|
||||
- Do not delete or migrate existing client-manager installations automatically.
|
||||
- Do not add per-command confirmation or persisted raw-command history for the future RCON console.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision: Model the payload as a Windows UE4SS DLL extension, not a client manager or executable
|
||||
|
||||
The manifest adds `runtimeProfiles.dllExtensions`. A ready profile has a fixed release URL/checksum/size/version, Windows amd64 target, compatible SCUM executable checksum, UE4SS ABI marker, logical target key, update policy, and loopback health contract. `kind` is fixed to `ue4ss-dll`, `activation` is fixed to `server-start`, and `supportedTargets` is fixed to Windows amd64. An explicit `unpublished` declaration may document an intended release URL but cannot be selected by a lifecycle profile or frozen into a job; this prevents placeholder hashes from reaching an operator machine.
|
||||
|
||||
The SCUM manifest declares the extension while retaining legacy client-manager metadata for existing installations. New server starts use the extension profile rather than building a companion executable.
|
||||
|
||||
Alternative considered: reuse `clientManagers`. Rejected because a UE4SS DLL is loaded by SCUM rather than supervised as an independent component process.
|
||||
|
||||
### Decision: Synchronize within the existing `process.start` lifecycle job
|
||||
|
||||
Platform copies the selected DLL extension plan into the fenced start-job execution input. Run performs sync before it launches the server executable. The job is the automatic update check: every start compares the installed managed release checksum/version with the declaration and updates only when necessary.
|
||||
|
||||
This avoids a new always-on download poller, preserves control/job/log/artifact channel isolation, and means Run never obtains unleased plugin configuration. A version change takes effect at the next normal server start. Future scheduled maintenance can queue an ordinary restart/start lifecycle job.
|
||||
|
||||
Alternative considered: let Run periodically poll plugin URLs directly. Rejected because it makes Run independently trust mutable plugin configuration and consumes network/resources outside Platform job fencing.
|
||||
|
||||
### Decision: Use release-pinned, scoped, atomic deployment
|
||||
|
||||
Run accepts only HTTPS URLs with a declared `sha256:` checksum and a bounded size. It downloads to a per-server extension staging root, verifies bytes before activation, and replaces only the managed DLL path atomically. It records a small local managed-release marker and retains one previous DLL copy for rollback. It never extracts archives, follows unsafe redirects, or accepts arbitrary target paths.
|
||||
|
||||
The generated `config.ini` contains a locally supplied RCON password reference/value only in the protected server workspace; it is not returned to Platform, plugins, browser DTOs, artifacts, or logs. `mods.txt` is updated idempotently using the exact declared extension key. UE4SS bootstrap files are not silently installed by this first change; the extension reports a blocked health result when the expected UE4SS layout is absent.
|
||||
|
||||
Alternative considered: download a whole UE4SS/bootstrap package on every server. Rejected for the first release because shared loader ownership and rollback across other mods requires a separate extension-runtime manager.
|
||||
|
||||
### Decision: Windows-only activation and explicit Linux rejection
|
||||
|
||||
The supplied module links Windows APIs and produces a `.dll`. Windows activation is the standard UE4SS startup path: the already-installed UE4SS loader reads `mods.txt` and loads the DLL after SCUM starts. Run does not mount or execute the DLL itself.
|
||||
|
||||
Linux would require a separately built `.so`, an independently verified UE4SS/Linux loader path, and game-specific compatibility. Run returns `unsupported_extension_platform` for this profile on Linux and does not try `LD_PRELOAD`.
|
||||
|
||||
### Decision: Address compatibility is owned by the DLL release
|
||||
|
||||
Native SCUM executor addresses are not configured in Platform or Run. The publisher-built DLL holds the vetted resolver table keyed by SCUM executable compatibility data and fails closed when its native probe cannot resolve exactly once. Platform/Run only select the right declared DLL release by compatible SCUM executable checksum.
|
||||
|
||||
Alternative considered: Run-side signature scanning or automatic generation. Rejected because it would make the executor a reverse-engineering authority and could execute an incorrect native address.
|
||||
|
||||
### Decision: Safe UI projections and no raw command history
|
||||
|
||||
Plugin detail and server detail show release version, checksum prefix, loading mode, supported target, update-on-start policy, last safe sync state, and Linux incompatibility. They do not show raw paths, URLs with credentials, RCON passwords, Run sessions, or native signatures. A future RCON console is direct operator input with transient delivery; it is not a confirmation dialog or a persisted command-history feature.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [A SCUM update changes native code] → DLL fails its internal probe; Run leaves the previous managed release in place and reports extension health unavailable rather than guessing an address.
|
||||
- [A malicious/changed CDN response] → checksum, HTTPS validation, bounded size, and staged verification block activation.
|
||||
- [UE4SS is not installed or incompatible] → sync completes the managed DLL only when safe, then reports a clear blocked health state; it does not copy proxy DLLs or alter shared loader files.
|
||||
- [A server host has multiple instances] → extension target and port are scoped by the existing server workspace/runtime binding; no global path is projected.
|
||||
- [Start job fails after update] → previous managed DLL is retained and a retry sees a coherent marker; full SCUM process rollback remains the existing lifecycle responsibility.
|
||||
- [Linux host selected] → the job is rejected before download and UI projects the unsupported platform state.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Register a new `game.scum` manifest version with a Windows UE4SS extension profile and preserve legacy client-manager declarations.
|
||||
2. On the next Windows start, Platform freezes the extension plan into the lifecycle job and Run syncs it before process launch.
|
||||
3. Operators ensure the approved UE4SS runtime is already present once; the DLL and generated configuration are then managed automatically.
|
||||
4. Roll back by publishing/reselecting the previous pinned DLL declaration and restarting the server; Run restores the previous managed DLL if activation fails.
|
||||
5. Existing client-manager installations are not stopped, deleted, or migrated automatically.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- A future change may manage the shared UE4SS bootstrap/runtime as a separately owned, reference-counted extension dependency.
|
||||
- A future Linux SCUM/UE4SS `.so` implementation requires its own compatibility and loader design.
|
||||
@@ -0,0 +1,33 @@
|
||||
## Why
|
||||
|
||||
The current SCUM plugin declares a separately built companion client, but the new SCUM RCON integration is a prebuilt UE4SS DLL that must load with the SCUM server process. Operators must not compile it, Run must not execute arbitrary binaries or inject a running process, and the platform currently has no declared, version-pinned DLL extension lifecycle.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a plugin-declared Windows-only UE4SS DLL extension profile with a fixed HTTPS release URL, SHA-256 checksum, file size, compatible SCUM executable checksum, UE4SS ABI marker, logical deployment target, and loopback RCON health probe.
|
||||
- Freeze the declared extension plan into the existing fenced `process.start` job. Run stages the approved DLL, generated configuration, and deterministic `mods.txt` entry before the normal SCUM process starts; there is no independent sync job or background poller.
|
||||
- Add Run-side verified DLL synchronization with bounded HTTPS download, scoped file ownership, atomic replacement, rollback, no runtime process injection, and an explicit Linux rejection for the Windows DLL extension.
|
||||
- Surface declared extension metadata and safe per-server state in plugin and server management views. The UI shows the release/version/checksum/loading mode and that Linux is unsupported, without exposing host paths, RCON passwords, or Run credentials.
|
||||
- Replace the SCUM example's new-server dependency on the `scum-client-manager` path with the UE4SS RCON extension declaration. Existing client-manager records remain compatible and are not deleted by this change.
|
||||
- Provision the extension's loopback-only Source RCON configuration for the existing declared remote-access boundary. This change does not create a second command console, per-command confirmation, or raw-command history.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `plugin-runtime-dll-extensions`: Plugin manifest, Platform validation, safe projections, and server-start ordering for versioned Windows UE4SS DLL extensions.
|
||||
- `run-ue4ss-dll-synchronization`: Independent Run download, staging, activation, UE4SS configuration, health preconditions, and rollback behavior for an approved UE4SS DLL extension.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
- `plugins/`: manifest schema, validator, SCUM example declaration, and fixtures.
|
||||
- `platform/`: domain/DTO/validation/job contracts, server start dispatch, fenced extension-sync input, safe state projection, and API handlers.
|
||||
- `platform_web/`: API types and existing plugin/server detail surfaces for declared extension state.
|
||||
- Independent `run/`: protocol copies, extension-sync executor, generated local-only RCON configuration, tests, and documentation. It remains an independent Git repository and is not added to this repository.
|
||||
- No user-side compiler, arbitrary executable launch, runtime remote-process injection, Linux DLL support, raw host-path exposure, raw RCON-password exposure, or unrelated product areas are introduced.
|
||||
|
||||
The supplied CDN URL currently has no downloadable DLL. The SCUM example therefore carries an explicit unpublished declaration rather than an invented checksum, and it is not bound to `process.start` until the publisher supplies the real release pins. Ready declarations remain fully immutable and are the only declarations that can be frozen into a start job.
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Plugin declares a bounded Windows UE4SS DLL extension
|
||||
The plugin manifest SHALL support `runtimeProfiles.dllExtensions` entries only for a fixed `ue4ss-dll` kind, Windows amd64 target, `server-start` activation, logical target key, declared DLL relative path, and update-on-start policy. A `ready` release SHALL additionally have an HTTPS release URL, SHA-256 checksum, bounded byte size, SCUM executable checksum, and UE4SS ABI marker. An `unpublished` declaration MAY document an intended release URL but SHALL not be frozen into a start job. The manifest validator SHALL reject executable references, shell arguments, Linux targets, unsafe URLs, non-DLL paths, missing ready-release integrity metadata, and unsafe logical keys.
|
||||
|
||||
#### Scenario: Valid SCUM DLL declaration registers
|
||||
- **WHEN** `game.scum` registers a Windows UE4SS DLL extension with all required immutable release and compatibility fields
|
||||
- **THEN** Platform persists a typed declaration and exposes only a safe projection to browser clients.
|
||||
|
||||
#### Scenario: Unsafe DLL declaration is rejected
|
||||
- **WHEN** a manifest declares a non-HTTPS URL, missing checksum, Linux target, executable command, unsafe path, or missing compatibility checksum
|
||||
- **THEN** manifest validation rejects the registration before it is persisted.
|
||||
|
||||
#### Scenario: Unpublished SCUM release stays inactive
|
||||
- **WHEN** a plugin documents an unpublished UE4SS DLL release before the publisher has supplied its immutable release pins
|
||||
- **THEN** the declaration is safe to display but cannot be referenced by a server-start lifecycle profile or dispatched to Run.
|
||||
|
||||
### Requirement: Server start freezes the declared extension plan
|
||||
Platform SHALL add the selected compatible DLL extension plan to the fenced `process.start` Run job input. The frozen plan SHALL contain only logical target identifiers and release integrity/compatibility metadata, and SHALL reject a start when the selected runtime profile or endpoint platform does not support the extension.
|
||||
|
||||
#### Scenario: Compatible Windows server start includes extension plan
|
||||
- **WHEN** a Windows amd64 SCUM server with a selected compatible runtime profile is started
|
||||
- **THEN** its Run start job includes the current immutable DLL extension plan before process execution.
|
||||
|
||||
#### Scenario: Linux server is rejected without a download
|
||||
- **WHEN** a Linux endpoint is selected for a UE4SS DLL extension profile
|
||||
- **THEN** Platform rejects the extension path with an explicit unsupported-platform result and does not dispatch a DLL download.
|
||||
|
||||
### Requirement: Browser receives a safe extension projection
|
||||
Platform SHALL project extension key, display name, release version, checksum prefix, supported OS/architecture, loading mode, update-on-start policy, compatibility marker, and safe synchronization state to plugin and server management views. It SHALL NOT project RCON passwords, raw host paths, signed Run job data, native signatures, or raw local file metadata.
|
||||
|
||||
#### Scenario: Server detail renders declaration without secrets
|
||||
- **WHEN** an authorized operator views a SCUM plugin or server detail
|
||||
- **THEN** the UI displays safe DLL extension metadata and does not contain credentials, host paths, or native resolver data.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Run synchronizes a frozen DLL plan before server start
|
||||
Run SHALL synchronize a declared UE4SS DLL extension before executing the associated `process.start` command. It SHALL compare the local managed-release marker against the frozen version/checksum, skip unchanged releases, and otherwise download, checksum-verify, stage, and atomically activate the declared DLL path within the scoped server workspace.
|
||||
|
||||
#### Scenario: Changed approved DLL updates before launch
|
||||
- **WHEN** a start job carries a valid extension plan whose release differs from the managed marker
|
||||
- **THEN** Run verifies and activates the new DLL before launching the server process.
|
||||
|
||||
#### Scenario: Unchanged approved DLL is reused
|
||||
- **WHEN** a start job carries the same version and checksum as the managed marker
|
||||
- **THEN** Run does not download the DLL again and proceeds to server launch.
|
||||
|
||||
### Requirement: Run preserves DLL deployment safety boundaries
|
||||
Run SHALL require HTTPS, a SHA-256 checksum, bounded content length, a safe logical target, and a declared `.dll` path. It SHALL write only managed extension files, retain one previous DLL for rollback, redact sensitive generated configuration, and never execute the DLL, inject a running process, load arbitrary libraries, or expose raw local paths/credentials through Platform-facing results.
|
||||
|
||||
#### Scenario: Invalid or mismatched payload does not activate
|
||||
- **WHEN** download fails, exceeds its byte limit, has a checksum mismatch, or the declared DLL path is unsafe
|
||||
- **THEN** Run keeps the prior managed DLL intact and returns a safe failed start result.
|
||||
|
||||
### Requirement: Run explicitly rejects Linux UE4SS DLL activation
|
||||
Run SHALL reject a `ue4ss-dll` extension plan when its local platform is not Windows amd64. It SHALL not attempt to map the DLL, invoke a shell loader, or substitute `LD_PRELOAD`.
|
||||
|
||||
#### Scenario: Linux extension start fails closed
|
||||
- **WHEN** a Linux Run receives a UE4SS DLL extension plan
|
||||
- **THEN** it returns `unsupported_extension_platform` before any download or process launch.
|
||||
|
||||
### Requirement: Extension synchronization does not block independent channels
|
||||
DLL download and verification SHALL run inside the claimed lifecycle job with bounded context while Run control heartbeat, job acknowledgement/result, cancellation polling, log spool upload, and artifact transfer remain independently scheduled.
|
||||
|
||||
#### Scenario: Slow DLL download preserves control and log traffic
|
||||
- **WHEN** a DLL download is blocked or slow during a start job
|
||||
- **THEN** Run continues its control heartbeat and durable log upload within their configured deadlines.
|
||||
@@ -0,0 +1,24 @@
|
||||
## 1. Plugin and Platform contracts
|
||||
|
||||
- [x] 1.1 Add the typed UE4SS DLL extension declaration to plugin schema, validator, fixtures, and the SCUM manifest with Windows-only compatibility metadata and an inactive unpublished-release guard.
|
||||
- [x] 1.2 Add domain, DTO, validator, and safe API projection types for declared DLL extensions and frozen start-job extension plans.
|
||||
- [x] 1.3 Freeze compatible extension plans into SCUM `process.start` jobs and reject incompatible Linux endpoint plans before Run dispatch.
|
||||
- [x] 1.4 Add focused Platform tests for registration, safe projection, start-job fencing, and unsupported-platform behavior.
|
||||
|
||||
## 2. Management console
|
||||
|
||||
- [x] 2.1 Extend typed frontend API/schema contracts for safe DLL extension declarations.
|
||||
- [x] 2.2 Render the declared Windows-only UE4SS extension and update-on-start policy in existing plugin/server detail surfaces without paths or secrets.
|
||||
- [x] 2.3 Add focused frontend tests for declaration rendering and Linux unsupported state.
|
||||
|
||||
## 3. Independent Run extension synchronization
|
||||
|
||||
- [x] 3.1 Mirror the frozen extension plan in the independent Run protocol and validate bounded, Windows-only DLL fields.
|
||||
- [x] 3.2 Implement scoped staged DLL download, checksum verification, managed-release marker, atomic activation, previous-DLL rollback, generated UE4SS mod configuration, and deterministic mods index update.
|
||||
- [x] 3.3 Gate `process.start` on extension synchronization and fail closed on non-Windows targets, unsafe payloads, or missing UE4SS layout without executing/loading the DLL directly.
|
||||
- [x] 3.4 Add Run unit tests for update/no-op/rollback/Linux rejection and channel independence.
|
||||
|
||||
## 4. Verification and delivery
|
||||
|
||||
- [x] 4.1 Run manifest, Platform, frontend, and Run focused tests; run `openspec validate add-scum-ue4ss-dll-runtime-extension --strict` and `scripts/check-structure.sh`.
|
||||
- [ ] 4.2 Review scoped diffs, stage only task files in browser and independent Run repositories, commit, and push both configured branches.
|
||||
@@ -1480,6 +1480,34 @@ func TestRuntimeBindingAPIIsAuthorizedValidatedAndRedacted(t *testing.T) {
|
||||
assertErrorResponse(t, missingServer, http.StatusNotFound, errorCodeNotFound)
|
||||
}
|
||||
|
||||
func TestGamePluginManifestAPISafelyProjectsDLLReleaseDeclaration(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
registration := validGamePluginManifestRegistrationRequest()
|
||||
registration.Manifest.RuntimeProfiles = dto.GamePluginRuntimeProfilesBody{DLLExtensions: []dto.RuntimeDLLExtensionProfileBody{{
|
||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []dto.RuntimeTargetBody{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
}}}
|
||||
|
||||
recorder := performJSON(t, router, http.MethodPost, "/api/v1/game-plugins/register-manifest", registration)
|
||||
assertStatus(t, recorder, http.StatusCreated)
|
||||
body := recorder.Body.String()
|
||||
response := decodeBody[dto.GamePluginResponse](t, recorder)
|
||||
if len(response.RuntimeProfiles.DLLExtensions) != 1 {
|
||||
t.Fatalf("expected DLL declaration projection, got %+v", response.RuntimeProfiles)
|
||||
}
|
||||
projected := response.RuntimeProfiles.DLLExtensions[0]
|
||||
if projected.ReleaseHost != "cdn.npc0.com" || projected.ReleaseFilename != "scum_simple_rcon_ue4s.dll" {
|
||||
t.Fatalf("expected safe DLL release projection, got %+v", projected)
|
||||
}
|
||||
for _, unsafe := range []string{"https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", "ue4ss/Mods/", "targetKey", "modKey", "rconPort"} {
|
||||
if strings.Contains(body, unsafe) {
|
||||
t.Fatalf("browser projection exposed DLL deployment internals %q: %s", unsafe, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGamePluginRegistryResponseDoesNotExposeRawInternals(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
recorder := performJSON(t, router, http.MethodPost, "/api/v1/game-plugins/register-manifest", validGamePluginManifestRegistrationRequest())
|
||||
|
||||
@@ -277,6 +277,7 @@ type RunJobReconcileResult struct {
|
||||
|
||||
func CopyRunJobAssignment(assignment RunJobAssignment) RunJobAssignment {
|
||||
assignment.ExecutionInput.Inputs = CopyStringMap(assignment.ExecutionInput.Inputs)
|
||||
assignment.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), assignment.ExecutionInput.DLLExtensions...)
|
||||
return assignment
|
||||
}
|
||||
|
||||
|
||||
@@ -368,6 +368,7 @@ type RuntimeLifecycleProfile struct {
|
||||
ActionRefs PluginLifecycleActions
|
||||
TransportKeys []string
|
||||
ClientManagerRef string
|
||||
DLLExtensionRefs []string
|
||||
Platforms []string
|
||||
}
|
||||
|
||||
@@ -455,6 +456,40 @@ type RuntimeClientManagerProfile struct {
|
||||
UpdatePolicy RuntimeClientManagerUpdatePolicy
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionProfile struct {
|
||||
Key string
|
||||
DisplayName string
|
||||
Kind string
|
||||
Activation string
|
||||
Version string
|
||||
ReleaseState string
|
||||
ReleaseURL string
|
||||
Checksum string
|
||||
SizeBytes int64
|
||||
TargetKey string
|
||||
ModKey string
|
||||
DLLRef string
|
||||
SCUMExecutableChecksum string
|
||||
UE4SSABI string
|
||||
SupportedTargets []RuntimeTarget
|
||||
UpdateOnStart bool
|
||||
RCONPort int
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionPlan struct {
|
||||
Key string
|
||||
Version string
|
||||
ReleaseURL string
|
||||
Checksum string
|
||||
SizeBytes int64
|
||||
TargetKey string
|
||||
ModKey string
|
||||
DLLRef string
|
||||
SCUMExecutableChecksum string
|
||||
UE4SSABI string
|
||||
RCONPort int
|
||||
}
|
||||
|
||||
type RuntimeConfigTemplate struct {
|
||||
Key string
|
||||
TemplateRef string
|
||||
@@ -470,6 +505,7 @@ type GamePluginRuntimeProfiles struct {
|
||||
LogEvents []RuntimeLogEvent
|
||||
TransportProfiles []RuntimeTransportProfile
|
||||
ClientManagers []RuntimeClientManagerProfile
|
||||
DLLExtensions []RuntimeDLLExtensionProfile
|
||||
}
|
||||
|
||||
type GamePluginManifest struct {
|
||||
@@ -812,6 +848,7 @@ type JobExecutionInput struct {
|
||||
LifecycleOperation string
|
||||
TargetVersion string
|
||||
Inputs map[string]string
|
||||
DLLExtensions []RuntimeDLLExtensionPlan
|
||||
}
|
||||
|
||||
type JobExecutionResult struct {
|
||||
@@ -1397,6 +1434,7 @@ func CopyGamePluginRuntimeProfiles(profiles GamePluginRuntimeProfiles) GamePlugi
|
||||
for i := range profiles.LifecycleProfiles {
|
||||
profiles.LifecycleProfiles[i].Capabilities = CopyStringSlice(profiles.LifecycleProfiles[i].Capabilities)
|
||||
profiles.LifecycleProfiles[i].TransportKeys = CopyStringSlice(profiles.LifecycleProfiles[i].TransportKeys)
|
||||
profiles.LifecycleProfiles[i].DLLExtensionRefs = CopyStringSlice(profiles.LifecycleProfiles[i].DLLExtensionRefs)
|
||||
profiles.LifecycleProfiles[i].Platforms = CopyStringSlice(profiles.LifecycleProfiles[i].Platforms)
|
||||
}
|
||||
profiles.DependencyProbes = append([]RuntimeDependencyProbe(nil), profiles.DependencyProbes...)
|
||||
@@ -1424,6 +1462,10 @@ func CopyGamePluginRuntimeProfiles(profiles GamePluginRuntimeProfiles) GamePlugi
|
||||
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...)
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
@@ -1527,6 +1569,7 @@ func CopyRunEndpoint(endpoint RunEndpoint) RunEndpoint {
|
||||
|
||||
func CopyJob(job Job) Job {
|
||||
job.ExecutionInput.Inputs = CopyStringMap(job.ExecutionInput.Inputs)
|
||||
job.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...)
|
||||
return job
|
||||
}
|
||||
|
||||
|
||||
+14
-13
@@ -92,18 +92,19 @@ type RunJobResultRequest struct {
|
||||
}
|
||||
|
||||
type RunJobExecutionInputBody struct {
|
||||
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
||||
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
||||
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
||||
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
|
||||
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||
PluginID string `json:"pluginId,omitempty"`
|
||||
LifecycleOperation string `json:"lifecycleOperation,omitempty"`
|
||||
TargetVersion string `json:"targetVersion,omitempty"`
|
||||
Inputs map[string]string `json:"inputs,omitempty"`
|
||||
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
||||
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
||||
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
||||
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
|
||||
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||
PluginID string `json:"pluginId,omitempty"`
|
||||
LifecycleOperation string `json:"lifecycleOperation,omitempty"`
|
||||
TargetVersion string `json:"targetVersion,omitempty"`
|
||||
Inputs map[string]string `json:"inputs,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionPlanBody `json:"dllExtensions,omitempty"`
|
||||
}
|
||||
|
||||
type RunJobExecutionResultBody struct {
|
||||
@@ -529,7 +530,7 @@ func RunJobAssignmentFromDomain(assignment domain.RunJobAssignment) RunJobAssign
|
||||
State: assignment.State,
|
||||
Progress: progressReportFromDomain(assignment.Progress),
|
||||
ResultRef: assignment.ResultRef,
|
||||
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs)},
|
||||
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions)},
|
||||
LeaseToken: assignment.LeaseToken,
|
||||
Attempt: assignment.Attempt,
|
||||
MaxAttempts: assignment.MaxAttempts,
|
||||
|
||||
+47
-47
@@ -330,29 +330,29 @@ type GamePluginCreateRequest struct {
|
||||
}
|
||||
|
||||
type GamePluginResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Version string `json:"version"`
|
||||
ServerType string `json:"serverType"`
|
||||
ServerDisplayName string `json:"serverDisplayName,omitempty"`
|
||||
SupportedOS []string `json:"supportedOs,omitempty"`
|
||||
ManifestRef string `json:"manifestRef"`
|
||||
CreateFormSchemaRef string `json:"createFormSchemaRef"`
|
||||
RequiredRunCapabilities []string `json:"requiredRunCapabilities"`
|
||||
DeclaredPermissions []string `json:"declaredPermissions"`
|
||||
Permissions PluginPermissionsResponse `json:"permissions"`
|
||||
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions"`
|
||||
BridgeActions []string `json:"bridgeActions"`
|
||||
Pages []GamePluginPageBody `json:"pages"`
|
||||
Tags []string `json:"tags"`
|
||||
AIPurposes []string `json:"aiPurposes"`
|
||||
ProductionLifecycle GamePluginProductionLifecycleBody `json:"productionLifecycle"`
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
Status domain.GamePluginStatus `json:"status"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Version string `json:"version"`
|
||||
ServerType string `json:"serverType"`
|
||||
ServerDisplayName string `json:"serverDisplayName,omitempty"`
|
||||
SupportedOS []string `json:"supportedOs,omitempty"`
|
||||
ManifestRef string `json:"manifestRef"`
|
||||
CreateFormSchemaRef string `json:"createFormSchemaRef"`
|
||||
RequiredRunCapabilities []string `json:"requiredRunCapabilities"`
|
||||
DeclaredPermissions []string `json:"declaredPermissions"`
|
||||
Permissions PluginPermissionsResponse `json:"permissions"`
|
||||
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions"`
|
||||
BridgeActions []string `json:"bridgeActions"`
|
||||
Pages []GamePluginPageBody `json:"pages"`
|
||||
Tags []string `json:"tags"`
|
||||
AIPurposes []string `json:"aiPurposes"`
|
||||
ProductionLifecycle GamePluginProductionLifecycleBody `json:"productionLifecycle"`
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesResponseBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
Status domain.GamePluginStatus `json:"status"`
|
||||
}
|
||||
|
||||
type GamePluginListResponse struct {
|
||||
@@ -361,30 +361,30 @@ type GamePluginListResponse struct {
|
||||
}
|
||||
|
||||
type MarketplacePluginResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Version string `json:"version"`
|
||||
ServerType string `json:"serverType"`
|
||||
ServerDisplayName string `json:"serverDisplayName,omitempty"`
|
||||
SupportedOS []string `json:"supportedOs,omitempty"`
|
||||
ManifestRef string `json:"manifestRef"`
|
||||
CreateFormSchemaRef string `json:"createFormSchemaRef"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
DeclaredPermissions []string `json:"declaredPermissions"`
|
||||
Permissions PluginPermissionsResponse `json:"permissions"`
|
||||
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions"`
|
||||
BridgeActions []string `json:"bridgeActions"`
|
||||
Pages []GamePluginPageBody `json:"pages"`
|
||||
Tags []string `json:"tags"`
|
||||
AIPurposes []string `json:"aiPurposes"`
|
||||
ProductionLifecycle GamePluginProductionLifecycleBody `json:"productionLifecycle"`
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
Status domain.GamePluginStatus `json:"status"`
|
||||
Source string `json:"source"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Version string `json:"version"`
|
||||
ServerType string `json:"serverType"`
|
||||
ServerDisplayName string `json:"serverDisplayName,omitempty"`
|
||||
SupportedOS []string `json:"supportedOs,omitempty"`
|
||||
ManifestRef string `json:"manifestRef"`
|
||||
CreateFormSchemaRef string `json:"createFormSchemaRef"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
DeclaredPermissions []string `json:"declaredPermissions"`
|
||||
Permissions PluginPermissionsResponse `json:"permissions"`
|
||||
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions"`
|
||||
BridgeActions []string `json:"bridgeActions"`
|
||||
Pages []GamePluginPageBody `json:"pages"`
|
||||
Tags []string `json:"tags"`
|
||||
AIPurposes []string `json:"aiPurposes"`
|
||||
ProductionLifecycle GamePluginProductionLifecycleBody `json:"productionLifecycle"`
|
||||
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
|
||||
RuntimeProfiles GamePluginRuntimeProfilesResponseBody `json:"runtimeProfiles,omitempty"`
|
||||
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
|
||||
ValidationViolations []string `json:"validationViolations,omitempty"`
|
||||
Status domain.GamePluginStatus `json:"status"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type MarketplacePluginListResponse struct {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package dto
|
||||
|
||||
import "browser.local/platform/domain"
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type RuntimeTargetBody struct {
|
||||
OS string `json:"os"`
|
||||
@@ -23,6 +28,7 @@ type RuntimeLifecycleProfileBody struct {
|
||||
ActionRefs PluginLifecycleActionsBody `json:"actionRefs,omitempty"`
|
||||
TransportKeys []string `json:"transportKeys,omitempty"`
|
||||
ClientManagerRef string `json:"clientManagerRef,omitempty"`
|
||||
DLLExtensionRefs []string `json:"dllExtensionRefs,omitempty"`
|
||||
Platforms []string `json:"platforms,omitempty"`
|
||||
}
|
||||
|
||||
@@ -150,6 +156,62 @@ type RuntimeClientManagerProfileBody struct {
|
||||
UpdatePolicy RuntimeClientManagerUpdatePolicyBody `json:"updatePolicy,omitempty"`
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionProfileBody struct {
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Kind string `json:"kind"`
|
||||
Activation string `json:"activation"`
|
||||
Version string `json:"version"`
|
||||
ReleaseState string `json:"releaseState"`
|
||||
ReleaseURL string `json:"releaseUrl,omitempty"`
|
||||
ReleaseHost string `json:"releaseHost,omitempty"`
|
||||
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
ModKey string `json:"modKey"`
|
||||
DLLRef string `json:"dllRef"`
|
||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum,omitempty"`
|
||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||
UpdateOnStart bool `json:"updateOnStart"`
|
||||
RCONPort int `json:"rconPort"`
|
||||
}
|
||||
|
||||
// RuntimeDLLExtensionProfileResponseBody is the browser-safe projection of a
|
||||
// declared DLL extension. The immutable deployment path, RCON port, and full
|
||||
// release URL remain internal to the manifest/start-job contracts.
|
||||
type RuntimeDLLExtensionProfileResponseBody struct {
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Kind string `json:"kind"`
|
||||
Activation string `json:"activation"`
|
||||
Version string `json:"version"`
|
||||
ReleaseState string `json:"releaseState"`
|
||||
ReleaseHost string `json:"releaseHost,omitempty"`
|
||||
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum,omitempty"`
|
||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||
UpdateOnStart bool `json:"updateOnStart"`
|
||||
}
|
||||
|
||||
type RuntimeDLLExtensionPlanBody struct {
|
||||
Key string `json:"key"`
|
||||
Version string `json:"version"`
|
||||
ReleaseURL string `json:"releaseUrl"`
|
||||
Checksum string `json:"checksum"`
|
||||
SizeBytes int64 `json:"sizeBytes"`
|
||||
TargetKey string `json:"targetKey"`
|
||||
ModKey string `json:"modKey"`
|
||||
DLLRef string `json:"dllRef"`
|
||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum"`
|
||||
UE4SSABI string `json:"ue4ssAbi"`
|
||||
RCONPort int `json:"rconPort"`
|
||||
}
|
||||
|
||||
type GamePluginRuntimeProfilesBody struct {
|
||||
Discovery []RuntimeDiscoveryProbeBody `json:"discovery,omitempty"`
|
||||
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
|
||||
@@ -159,6 +221,22 @@ type GamePluginRuntimeProfilesBody struct {
|
||||
LogEvents []RuntimeLogEventBody `json:"logEvents,omitempty"`
|
||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||
ClientManagers []RuntimeClientManagerProfileBody `json:"clientManagers,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
||||
}
|
||||
|
||||
// GamePluginRuntimeProfilesResponseBody is intentionally distinct from the
|
||||
// manifest input shape so browser responses cannot accidentally include
|
||||
// machine-local DLL placement or RCON activation details.
|
||||
type GamePluginRuntimeProfilesResponseBody struct {
|
||||
Discovery []RuntimeDiscoveryProbeBody `json:"discovery,omitempty"`
|
||||
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
|
||||
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
|
||||
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
|
||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||
LogEvents []RuntimeLogEventBody `json:"logEvents,omitempty"`
|
||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||
ClientManagers []RuntimeClientManagerProfileBody `json:"clientManagers,omitempty"`
|
||||
DLLExtensions []RuntimeDLLExtensionProfileResponseBody `json:"dllExtensions,omitempty"`
|
||||
}
|
||||
|
||||
func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimeProfiles {
|
||||
@@ -167,7 +245,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, 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), ClientManagerRef: item.ClientManagerRef, 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)})
|
||||
@@ -207,17 +285,24 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
||||
}
|
||||
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 {
|
||||
extension.SupportedTargets = append(extension.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
|
||||
}
|
||||
profiles.DLLExtensions = append(profiles.DLLExtensions, extension)
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePluginRuntimeProfilesBody {
|
||||
func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePluginRuntimeProfilesResponseBody {
|
||||
profiles = domain.CopyGamePluginRuntimeProfiles(profiles)
|
||||
body := GamePluginRuntimeProfilesBody{}
|
||||
body := GamePluginRuntimeProfilesResponseBody{}
|
||||
for _, item := range profiles.Discovery {
|
||||
body.Discovery = append(body.Discovery, RuntimeDiscoveryProbeBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Required: item.Required, Expected: item.Expected, Platforms: item.Platforms})
|
||||
}
|
||||
for _, item := range profiles.LifecycleProfiles {
|
||||
body.LifecycleProfiles = append(body.LifecycleProfiles, RuntimeLifecycleProfileBody{Key: item.Key, Mode: item.Mode, Capabilities: item.Capabilities, ActionRefs: lifecycleActionsFromDomain(item.ActionRefs), TransportKeys: item.TransportKeys, ClientManagerRef: item.ClientManagerRef, Platforms: item.Platforms})
|
||||
body.LifecycleProfiles = append(body.LifecycleProfiles, RuntimeLifecycleProfileBody{Key: item.Key, Mode: item.Mode, Capabilities: item.Capabilities, ActionRefs: lifecycleActionsFromDomain(item.ActionRefs), TransportKeys: item.TransportKeys, ClientManagerRef: item.ClientManagerRef, DLLExtensionRefs: item.DLLExtensionRefs, Platforms: item.Platforms})
|
||||
}
|
||||
for _, item := range profiles.DependencyProbes {
|
||||
body.DependencyProbes = append(body.DependencyProbes, RuntimeDependencyProbeBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Required: item.Required, MinimumVersion: item.MinimumVersion, Platforms: item.Platforms})
|
||||
@@ -257,5 +342,36 @@ func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePl
|
||||
}
|
||||
body.ClientManagers = append(body.ClientManagers, manager)
|
||||
}
|
||||
for _, item := range profiles.DLLExtensions {
|
||||
host, filename := safeDLLReleaseLocation(item.ReleaseURL)
|
||||
extension := RuntimeDLLExtensionProfileResponseBody{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseHost: host, ReleaseFilename: filename, Checksum: safeDLLChecksumPrefix(item.Checksum), SizeBytes: item.SizeBytes, SCUMExecutableChecksum: safeDLLChecksumPrefix(item.SCUMExecutableChecksum), UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart}
|
||||
for _, target := range item.SupportedTargets {
|
||||
extension.SupportedTargets = append(extension.SupportedTargets, RuntimeTargetBody{OS: target.OS, Arch: target.Arch})
|
||||
}
|
||||
body.DLLExtensions = append(body.DLLExtensions, extension)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func dllExtensionPlansFromDomain(plans []domain.RuntimeDLLExtensionPlan) []RuntimeDLLExtensionPlanBody {
|
||||
items := make([]RuntimeDLLExtensionPlanBody, 0, len(plans))
|
||||
for _, item := range plans {
|
||||
items = append(items, RuntimeDLLExtensionPlanBody{Key: item.Key, Version: item.Version, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, SCUMExecutableChecksum: item.SCUMExecutableChecksum, UE4SSABI: item.UE4SSABI, RCONPort: item.RCONPort})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func safeDLLReleaseLocation(value string) (string, string) {
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil || parsed == nil {
|
||||
return "", ""
|
||||
}
|
||||
return parsed.Hostname(), path.Base(parsed.Path)
|
||||
}
|
||||
|
||||
func safeDLLChecksumPrefix(value string) string {
|
||||
if len(value) <= len("sha256:")+12 {
|
||||
return value
|
||||
}
|
||||
return value[:len("sha256:")+12] + "…"
|
||||
}
|
||||
|
||||
@@ -293,6 +293,8 @@ type JobExecutionInput struct {
|
||||
LifecycleOperation string `json:"lifecycleOperation,omitempty" db:"lifecycle_operation"`
|
||||
TargetVersion string `json:"targetVersion,omitempty" db:"target_version"`
|
||||
Inputs map[string]string `json:"inputs,omitempty" db:"inputs"`
|
||||
// DLLExtensions is the frozen, ready-only DLL plan delivered to a scoped start job.
|
||||
DLLExtensions []domain.RuntimeDLLExtensionPlan `json:"dllExtensions,omitempty" db:"dll_extensions"`
|
||||
}
|
||||
|
||||
type JobExecutionResult struct {
|
||||
@@ -882,11 +884,11 @@ func (job Job) ToDomain() domain.Job {
|
||||
}
|
||||
|
||||
func executionInputFromDomain(input domain.JobExecutionInput) JobExecutionInput {
|
||||
return JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs)}
|
||||
return JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...)}
|
||||
}
|
||||
|
||||
func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
|
||||
return domain.JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs)}
|
||||
return domain.JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...)}
|
||||
}
|
||||
|
||||
func executionResultFromDomain(result domain.JobExecutionResult) JobExecutionResult {
|
||||
|
||||
@@ -600,7 +600,7 @@ func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignmen
|
||||
State: job.State,
|
||||
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Message: job.Progress.Message},
|
||||
ResultRef: job.ResultRef,
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs)},
|
||||
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...)},
|
||||
LeaseToken: leaseToken,
|
||||
Attempt: job.Attempt,
|
||||
MaxAttempts: job.RetryPolicy.MaxAttempts,
|
||||
|
||||
@@ -161,6 +161,19 @@ func (svc *CoreService) dispatchExistingServerLifecycle(command domain.ServerLif
|
||||
if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
if action == domain.ServerLifecycleActionStart {
|
||||
binding, bindingErr := svc.runtimeBindingForServer(instance.ID)
|
||||
if bindingErr != nil && !errors.Is(bindingErr, repo.ErrNotFound) {
|
||||
return domain.ServerLifecycleResult{}, bindingErr
|
||||
}
|
||||
if bindingErr == nil {
|
||||
if profile, exists := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, binding.ProfileKey); exists && len(profile.DLLExtensionRefs) > 0 {
|
||||
if _, extensionErr := lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint); extensionErr != nil {
|
||||
return domain.ServerLifecycleResult{}, extensionErr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := svc.requireCompleteRuntimeBindings(instance.OwnerUserID, instance.ID, "server.lifecycle."+string(action)+".denied"); err != nil {
|
||||
return domain.ServerLifecycleResult{}, err
|
||||
}
|
||||
@@ -203,7 +216,8 @@ func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, act
|
||||
return domain.Job{}, err
|
||||
}
|
||||
actionRef := binding.ProfileKey
|
||||
if profile, ok := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, binding.ProfileKey); ok {
|
||||
profile, hasProfile := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, binding.ProfileKey)
|
||||
if hasProfile {
|
||||
if ref := runtimeProfileActionRef(profile.ActionRefs, action); ref != "" {
|
||||
actionRef = ref
|
||||
}
|
||||
@@ -213,6 +227,17 @@ func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, act
|
||||
if strings.TrimSpace(actionRef) == "" {
|
||||
return domain.Job{}, validationError(fmt.Sprintf("plugin %s lifecycle action is required", action))
|
||||
}
|
||||
var dllExtensions []domain.RuntimeDLLExtensionPlan
|
||||
if action == domain.ServerLifecycleActionStart && hasProfile {
|
||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
dllExtensions, err = lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint)
|
||||
if err != nil {
|
||||
return domain.Job{}, err
|
||||
}
|
||||
}
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: lifecycleJobID(instance.ID, action, idempotencyKey),
|
||||
ServerInstanceID: instance.ID,
|
||||
@@ -224,6 +249,7 @@ func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, act
|
||||
WorkspaceScope: binding.ProfileKey,
|
||||
PluginID: plugin.ID,
|
||||
LifecycleOperation: lifecycleExecutionOperation(action),
|
||||
DLLExtensions: dllExtensions,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -274,6 +300,37 @@ func runtimeLifecycleProfileForKey(profiles domain.GamePluginRuntimeProfiles, ke
|
||||
return domain.RuntimeLifecycleProfile{}, false
|
||||
}
|
||||
|
||||
func lifecycleDLLExtensionPlans(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile, endpoint domain.RunEndpoint) ([]domain.RuntimeDLLExtensionPlan, error) {
|
||||
if len(profile.DLLExtensionRefs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
byKey := make(map[string]domain.RuntimeDLLExtensionProfile, len(profiles.DLLExtensions))
|
||||
for _, extension := range profiles.DLLExtensions {
|
||||
byKey[extension.Key] = extension
|
||||
}
|
||||
plans := make([]domain.RuntimeDLLExtensionPlan, 0, len(profile.DLLExtensionRefs))
|
||||
for _, key := range profile.DLLExtensionRefs {
|
||||
extension, exists := byKey[key]
|
||||
if !exists || extension.ReleaseState != "ready" {
|
||||
return nil, validationError("extension_release_unavailable: selected DLL release is not ready")
|
||||
}
|
||||
if !runtimeDLLExtensionSupportsTarget(extension, endpoint.Platform, endpoint.Architecture) {
|
||||
return nil, validationError("unsupported_extension_platform: UE4SS DLL requires windows/amd64")
|
||||
}
|
||||
plans = append(plans, domain.RuntimeDLLExtensionPlan{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort})
|
||||
}
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
func runtimeDLLExtensionSupportsTarget(extension domain.RuntimeDLLExtensionProfile, platform string, architecture string) bool {
|
||||
for _, target := range extension.SupportedTargets {
|
||||
if strings.EqualFold(target.OS, platform) && strings.EqualFold(target.Arch, architecture) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (svc *CoreService) validateLifecycleIdempotency(runEndpointID string, idempotencyKey string, serverInstanceID string, capability string) error {
|
||||
existing, err := svc.store.Jobs().GetByIdempotency(runEndpointID, idempotencyKey)
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
|
||||
@@ -85,6 +85,65 @@ func TestCoreServiceServerLifecycleWorkflows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceFreezesReadyDLLExtensionIntoWindowsStartJob(t *testing.T) {
|
||||
svc, sessionToken := newLifecycleRunService(t)
|
||||
setLifecycleEndpointTarget(t, svc, "windows", "amd64")
|
||||
plugin := createLifecyclePlugin(t, svc)
|
||||
attachReadyLifecycleDLLExtension(t, svc, &plugin)
|
||||
|
||||
created, err := svc.CreateServerInstanceWorkflow(domain.ServerLifecycleCreate{ID: "server-dll", PluginID: plugin.ID, RunEndpointID: "run-local", Name: "SCUM DLL", IdempotencyKey: "idem-dll-create", ProfileKey: "local"})
|
||||
if err != nil {
|
||||
t.Fatalf("create DLL lifecycle server: %v", err)
|
||||
}
|
||||
claimAndCompleteLifecycleJobForServer(t, svc, sessionToken, created.Instance.ID, domain.LifecycleCapabilityInstall, domain.JobStateSucceeded)
|
||||
ready, err := svc.GetServerInstance(created.Instance.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get ready DLL server: %v", err)
|
||||
}
|
||||
|
||||
started, err := svc.StartServerInstance(domain.ServerLifecycleCommand{ServerInstanceID: ready.ID, ExpectedConfigVersion: ready.ConfigVersion, IdempotencyKey: "idem-dll-start"})
|
||||
if err != nil {
|
||||
t.Fatalf("start DLL lifecycle server: %v", err)
|
||||
}
|
||||
if len(started.Job.ExecutionInput.DLLExtensions) != 1 {
|
||||
t.Fatalf("expected one frozen DLL plan, got %+v", started.Job.ExecutionInput)
|
||||
}
|
||||
if got := started.Job.ExecutionInput.DLLExtensions[0]; got.Key != "scum-simple-rcon" || got.Checksum != "sha256:"+strings.Repeat("a", 64) || got.ReleaseURL != "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll" {
|
||||
t.Fatalf("unexpected frozen DLL plan: %+v", got)
|
||||
}
|
||||
|
||||
plugin.RuntimeProfiles.DLLExtensions[0].Checksum = "sha256:" + strings.Repeat("c", 64)
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update plugin after start fence: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: sessionToken, Capabilities: []string{domain.LifecycleCapabilityStart}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || len(claim.Job.ExecutionInput.DLLExtensions) != 1 {
|
||||
t.Fatalf("claim frozen start job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
if got := claim.Job.ExecutionInput.DLLExtensions[0].Checksum; got != "sha256:"+strings.Repeat("a", 64) {
|
||||
t.Fatalf("queued start job was not fenced to the original DLL pin: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceRejectsLinuxDLLExtensionStartBeforeDispatch(t *testing.T) {
|
||||
svc, sessionToken := newLifecycleRunService(t)
|
||||
setLifecycleEndpointTarget(t, svc, "windows", "amd64")
|
||||
plugin := createLifecyclePlugin(t, svc)
|
||||
attachReadyLifecycleDLLExtension(t, svc, &plugin)
|
||||
created, err := svc.CreateServerInstanceWorkflow(domain.ServerLifecycleCreate{ID: "server-dll-linux", PluginID: plugin.ID, RunEndpointID: "run-local", Name: "SCUM DLL Linux", IdempotencyKey: "idem-dll-linux-create", ProfileKey: "local"})
|
||||
if err != nil {
|
||||
t.Fatalf("create DLL lifecycle server: %v", err)
|
||||
}
|
||||
claimAndCompleteLifecycleJobForServer(t, svc, sessionToken, created.Instance.ID, domain.LifecycleCapabilityInstall, domain.JobStateSucceeded)
|
||||
ready, _ := svc.GetServerInstance(created.Instance.ID)
|
||||
setLifecycleEndpointTarget(t, svc, "linux", "amd64")
|
||||
|
||||
_, err = svc.StartServerInstance(domain.ServerLifecycleCommand{ServerInstanceID: ready.ID, ExpectedConfigVersion: ready.ConfigVersion, IdempotencyKey: "idem-dll-linux-start"})
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported_extension_platform") {
|
||||
t.Fatalf("expected explicit Linux DLL rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceServerLifecycleRejectsInvalidCommands(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
@@ -272,6 +331,34 @@ func createLifecyclePlugin(t *testing.T, svc *CoreService) domain.GamePlugin {
|
||||
return plugin
|
||||
}
|
||||
|
||||
func attachReadyLifecycleDLLExtension(t *testing.T, svc *CoreService, plugin *domain.GamePlugin) {
|
||||
t.Helper()
|
||||
plugin.RuntimeProfiles.LifecycleProfiles[0].Platforms = []string{"windows"}
|
||||
plugin.RuntimeProfiles.LifecycleProfiles[0].DLLExtensionRefs = []string{"scum-simple-rcon"}
|
||||
plugin.RuntimeProfiles.DLLExtensions = []domain.RuntimeDLLExtensionProfile{{
|
||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
}}
|
||||
if err := svc.store.GamePlugins().Update(*plugin); err != nil {
|
||||
t.Fatalf("attach ready DLL extension: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setLifecycleEndpointTarget(t *testing.T, svc *CoreService, platform string, architecture string) {
|
||||
t.Helper()
|
||||
endpoint, err := svc.store.RunEndpoints().Get("run-local")
|
||||
if err != nil {
|
||||
t.Fatalf("get lifecycle endpoint: %v", err)
|
||||
}
|
||||
endpoint.Platform = platform
|
||||
endpoint.Architecture = architecture
|
||||
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
||||
t.Fatalf("set lifecycle endpoint target: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func claimAndCompleteLifecycleJob(t *testing.T, svc *CoreService, sessionToken string, capability string, state domain.JobState) {
|
||||
t.Helper()
|
||||
claimAndCompleteLifecycleJobForServer(t, svc, sessionToken, "", capability, state)
|
||||
|
||||
@@ -1012,6 +1012,15 @@ func ValidateJob(job domain.Job) error {
|
||||
if job.ExecutionInput.ExpectedChecksum != "" && !validSHA256Checksum(job.ExecutionInput.ExpectedChecksum) {
|
||||
violations = append(violations, "executionInput.expectedChecksum must be sha256:<hex>")
|
||||
}
|
||||
if len(job.ExecutionInput.DLLExtensions) > 16 {
|
||||
violations = append(violations, "executionInput.dllExtensions must not exceed 16")
|
||||
}
|
||||
if len(job.ExecutionInput.DLLExtensions) > 0 && (job.Capability != domain.LifecycleCapabilityStart || job.ExecutionInput.LifecycleOperation != "start" || job.ServerInstanceID == "") {
|
||||
violations = append(violations, "executionInput.dllExtensions are allowed only for scoped process.start jobs")
|
||||
}
|
||||
for i, plan := range job.ExecutionInput.DLLExtensions {
|
||||
violations = append(violations, validateRuntimeDLLExtensionPlan(fmt.Sprintf("executionInput.dllExtensions[%d]", i), plan)...)
|
||||
}
|
||||
violations = append(violations, validateRemoteAdapterInputs("executionInput.inputs", job.ExecutionInput.Inputs)...)
|
||||
if job.ExecutionResult.Checksum != "" && !validSHA256Checksum(job.ExecutionResult.Checksum) {
|
||||
violations = append(violations, "executionResult.checksum must be sha256:<hex>")
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestValidateGamePluginRuntimeProfilesAcceptsReadyUE4SSDLLExtension(t *testing.T) {
|
||||
profiles := validRuntimeDLLExtensionProfiles()
|
||||
if err := ValidateGamePluginRuntimeProfiles(profiles); err != nil {
|
||||
t.Fatalf("expected ready DLL extension profile to validate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginRuntimeProfilesRejectsUnsafeOrUnpublishedDLLExtension(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mutate func(*domain.GamePluginRuntimeProfiles)
|
||||
want string
|
||||
}{
|
||||
{name: "linux target", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
|
||||
profiles.DLLExtensions[0].SupportedTargets = []domain.RuntimeTarget{{OS: "linux", Arch: "amd64"}}
|
||||
}, want: "windows/amd64"},
|
||||
{name: "unsafe URL", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
|
||||
profiles.DLLExtensions[0].ReleaseURL = "https://127.0.0.1/plugin.dll"
|
||||
}, want: "public credential-free HTTPS DLL URL"},
|
||||
{name: "query URL", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
|
||||
profiles.DLLExtensions[0].ReleaseURL = "https://cdn.npc0.com/plugin.dll?release=1"
|
||||
}, want: "public credential-free HTTPS DLL URL"},
|
||||
{name: "unsafe path", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
|
||||
profiles.DLLExtensions[0].DLLRef = "ue4ss/Mods/scum_simple_rcon/dlls/plugin.exe"
|
||||
}, want: "main.dll path"},
|
||||
{name: "missing pin", mutate: func(profiles *domain.GamePluginRuntimeProfiles) { profiles.DLLExtensions[0].Checksum = "" }, want: "SHA-256"},
|
||||
{name: "unpublished reference", mutate: func(profiles *domain.GamePluginRuntimeProfiles) {
|
||||
profiles.DLLExtensions[0].ReleaseState = "unpublished"
|
||||
}, want: "unpublished DLL extension"},
|
||||
}
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
profiles := validRuntimeDLLExtensionProfiles()
|
||||
testCase.mutate(&profiles)
|
||||
err := ValidateGamePluginRuntimeProfiles(profiles)
|
||||
if err == nil || !strings.Contains(err.Error(), testCase.want) {
|
||||
t.Fatalf("expected %q validation error, got %v", testCase.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateJobRejectsDLLPlanOutsideProcessStart(t *testing.T) {
|
||||
profiles := validRuntimeDLLExtensionProfiles()
|
||||
extension := profiles.DLLExtensions[0]
|
||||
job := domain.Job{ID: "dll-job", ServerInstanceID: "server-1", RunEndpointID: "run-1", Capability: domain.LifecycleCapabilityStop, IdempotencyKey: "dll-stop", State: domain.JobStateQueued, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{LifecycleOperation: "stop", DLLExtensions: []domain.RuntimeDLLExtensionPlan{{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}}}}
|
||||
if err := ValidateJob(job); err == nil || !strings.Contains(err.Error(), "process.start") {
|
||||
t.Fatalf("expected process.start plan restriction, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validRuntimeDLLExtensionProfiles() domain.GamePluginRuntimeProfiles {
|
||||
return domain.GamePluginRuntimeProfiles{
|
||||
LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "run-local", Mode: "local-process", Capabilities: []string{domain.LifecycleCapabilityStart}, Platforms: []string{"windows"}, DLLExtensionRefs: []string{"scum-simple-rcon"}}},
|
||||
DLLExtensions: []domain.RuntimeDLLExtensionProfile{{
|
||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||
}},
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
var (
|
||||
runtimeLogEventSchemaRefPattern = regexp.MustCompile(`^[A-Za-z0-9_./-]+\.json$`)
|
||||
runtimeLogEventTypePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,119}$`)
|
||||
runtimeDLLModKeyPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,79}$`)
|
||||
runtimeDLLABIPattern = regexp.MustCompile(`^[A-Za-z0-9._-]{1,80}$`)
|
||||
)
|
||||
|
||||
func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles) error {
|
||||
@@ -22,6 +24,8 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
lifecycleKeys := map[string]struct{}{}
|
||||
transportKeys := map[string]struct{}{}
|
||||
managerKeys := map[string]struct{}{}
|
||||
dllExtensionKeys := map[string]struct{}{}
|
||||
dllExtensionStates := map[string]string{}
|
||||
discoveryKeys := map[string]struct{}{}
|
||||
dependencyKeys := map[string]struct{}{}
|
||||
installPlanKeys := map[string]struct{}{}
|
||||
@@ -65,6 +69,10 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
if profile.ClientManagerRef != "" {
|
||||
violations = append(violations, validateProfileKey(prefix+".clientManagerRef", profile.ClientManagerRef)...)
|
||||
}
|
||||
for j, key := range profile.DLLExtensionRefs {
|
||||
violations = append(violations, validateProfileKey(fmt.Sprintf("%s.dllExtensionRefs[%d]", prefix, j), key)...)
|
||||
}
|
||||
violations = append(violations, duplicateViolations(prefix+".dllExtensionRefs", profile.DLLExtensionRefs)...)
|
||||
violations = append(violations, validateRuntimePlatforms(prefix+".platforms", profile.Platforms)...)
|
||||
}
|
||||
for i, probe := range profiles.DependencyProbes {
|
||||
@@ -349,6 +357,15 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, extension := range profiles.DLLExtensions {
|
||||
prefix := fmt.Sprintf("runtimeProfiles.dllExtensions[%d]", i)
|
||||
violations = append(violations, validateProfileKey(prefix+".key", extension.Key)...)
|
||||
violations = append(violations, recordRuntimeProfileKey(dllExtensionKeys, prefix+".key", extension.Key)...)
|
||||
if extension.Key != "" {
|
||||
dllExtensionStates[extension.Key] = extension.ReleaseState
|
||||
}
|
||||
violations = append(violations, validateRuntimeDLLExtensionProfile(prefix, extension)...)
|
||||
}
|
||||
for i, profile := range profiles.LifecycleProfiles {
|
||||
for _, key := range profile.TransportKeys {
|
||||
if _, ok := transportKeys[key]; !ok {
|
||||
@@ -360,10 +377,107 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
||||
violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d].clientManagerRef references undeclared client manager", i))
|
||||
}
|
||||
}
|
||||
if len(profile.DLLExtensionRefs) > 0 {
|
||||
if profile.Mode != "local-process" || !containsString(profile.Capabilities, domain.LifecycleCapabilityStart) || len(profile.Platforms) != 1 || profile.Platforms[0] != "windows" {
|
||||
violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d] DLL extensions require a windows local-process start profile", i))
|
||||
}
|
||||
for _, key := range profile.DLLExtensionRefs {
|
||||
state, exists := dllExtensionStates[key]
|
||||
if !exists {
|
||||
violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d].dllExtensionRefs references undeclared DLL extension %q", i, key))
|
||||
continue
|
||||
}
|
||||
if state != "ready" {
|
||||
violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d].dllExtensionRefs references unpublished DLL extension %q", i, key))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func validateRuntimeDLLExtensionProfile(prefix string, extension domain.RuntimeDLLExtensionProfile) []string {
|
||||
var violations []string
|
||||
if extension.Kind != "ue4ss-dll" || extension.Activation != "server-start" {
|
||||
violations = append(violations, prefix+".kind and activation must be ue4ss-dll/server-start")
|
||||
}
|
||||
if !validSemanticVersion(extension.Version) {
|
||||
violations = append(violations, prefix+".version must be semantic")
|
||||
}
|
||||
violations = append(violations, validateSafeRuntimeValue(prefix+".displayName", extension.DisplayName)...)
|
||||
violations = append(violations, validateProfileKey(prefix+".targetKey", extension.TargetKey)...)
|
||||
if !runtimeDLLModKeyPattern.MatchString(extension.ModKey) {
|
||||
violations = append(violations, prefix+".modKey is invalid")
|
||||
}
|
||||
if extension.DLLRef != "ue4ss/Mods/"+extension.ModKey+"/dlls/main.dll" {
|
||||
violations = append(violations, prefix+".dllRef must be the declared UE4SS main.dll path")
|
||||
}
|
||||
if extension.UpdateOnStart != true {
|
||||
violations = append(violations, prefix+".updateOnStart must be true")
|
||||
}
|
||||
if extension.RCONPort < 1024 || extension.RCONPort > 65535 {
|
||||
violations = append(violations, prefix+".rconPort must be an unprivileged port")
|
||||
}
|
||||
if len(extension.SupportedTargets) != 1 || extension.SupportedTargets[0].OS != "windows" || extension.SupportedTargets[0].Arch != "amd64" {
|
||||
violations = append(violations, prefix+".supportedTargets must contain only windows/amd64")
|
||||
}
|
||||
if extension.ReleaseState != "ready" && extension.ReleaseState != "unpublished" {
|
||||
violations = append(violations, prefix+".releaseState is invalid")
|
||||
}
|
||||
if extension.ReleaseURL != "" {
|
||||
violations = append(violations, validateRuntimeDLLReleaseURL(prefix+".releaseUrl", extension.ReleaseURL)...)
|
||||
}
|
||||
if extension.ReleaseState == "ready" {
|
||||
if extension.ReleaseURL == "" {
|
||||
violations = append(violations, prefix+".releaseUrl is required for a ready release")
|
||||
}
|
||||
if !validSHA256Checksum(extension.Checksum) || !validSHA256Checksum(extension.SCUMExecutableChecksum) {
|
||||
violations = append(violations, prefix+".checksum and scumExecutableChecksum must be SHA-256")
|
||||
}
|
||||
if extension.SizeBytes < 1 || extension.SizeBytes > 128*1024*1024 {
|
||||
violations = append(violations, prefix+".sizeBytes is out of bounds")
|
||||
}
|
||||
if !runtimeDLLABIPattern.MatchString(extension.UE4SSABI) {
|
||||
violations = append(violations, prefix+".ue4ssAbi is invalid")
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateRuntimeDLLReleaseURL(field string, value string) []string {
|
||||
parsed, err := url.Parse(value)
|
||||
host := ""
|
||||
if parsed != nil {
|
||||
host = strings.ToLower(parsed.Hostname())
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if err != nil || parsed == nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.Fragment != "" || parsed.RawQuery != "" || parsed.Port() != "" && parsed.Port() != "443" || host == "localhost" || strings.HasSuffix(host, ".localhost") || strings.HasSuffix(host, ".local") || ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() || ip.IsLinkLocalUnicast()) || !strings.HasSuffix(strings.ToLower(parsed.Path), ".dll") {
|
||||
return []string{field + " must be a public credential-free HTTPS DLL URL"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRuntimeDLLExtensionPlan(prefix string, plan domain.RuntimeDLLExtensionPlan) []string {
|
||||
return validateRuntimeDLLExtensionProfile(prefix, domain.RuntimeDLLExtensionProfile{
|
||||
Key: plan.Key,
|
||||
Kind: "ue4ss-dll",
|
||||
Activation: "server-start",
|
||||
Version: plan.Version,
|
||||
ReleaseState: "ready",
|
||||
ReleaseURL: plan.ReleaseURL,
|
||||
Checksum: plan.Checksum,
|
||||
SizeBytes: plan.SizeBytes,
|
||||
TargetKey: plan.TargetKey,
|
||||
ModKey: plan.ModKey,
|
||||
DLLRef: plan.DLLRef,
|
||||
SCUMExecutableChecksum: plan.SCUMExecutableChecksum,
|
||||
UE4SSABI: plan.UE4SSABI,
|
||||
SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}},
|
||||
UpdateOnStart: true,
|
||||
RCONPort: plan.RCONPort,
|
||||
})
|
||||
}
|
||||
|
||||
func validateProfileKey(field, value string) []string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return []string{field + " is required"}
|
||||
|
||||
@@ -239,6 +239,7 @@ export interface RuntimeLifecycleProfileResponse {
|
||||
actionRefs?: Record<string, string>;
|
||||
transportKeys?: string[];
|
||||
clientManagerRef?: string;
|
||||
dllExtensionRefs?: string[];
|
||||
platforms?: string[];
|
||||
}
|
||||
|
||||
@@ -302,6 +303,23 @@ export interface RuntimeClientManagerProfileResponse {
|
||||
updatePolicy?: { strategy: string; requireApproval: boolean; healthConfirmationSeconds: number; retainPrevious: boolean };
|
||||
}
|
||||
|
||||
export interface RuntimeDLLExtensionProfileResponse {
|
||||
key: string;
|
||||
displayName: string;
|
||||
kind: "ue4ss-dll";
|
||||
activation: "server-start";
|
||||
version: string;
|
||||
releaseState: "ready" | "unpublished";
|
||||
releaseHost?: string;
|
||||
releaseFilename?: string;
|
||||
checksum?: string;
|
||||
sizeBytes?: number;
|
||||
scumExecutableChecksum?: string;
|
||||
ue4ssAbi?: string;
|
||||
supportedTargets: Array<{ os: string; arch: string }>;
|
||||
updateOnStart: boolean;
|
||||
}
|
||||
|
||||
export interface GamePluginRuntimeProfilesResponse {
|
||||
discovery?: RuntimeDiscoveryProbeResponse[];
|
||||
lifecycleProfiles?: RuntimeLifecycleProfileResponse[];
|
||||
@@ -311,6 +329,7 @@ export interface GamePluginRuntimeProfilesResponse {
|
||||
logEvents?: RuntimeLogEventResponse[];
|
||||
transportProfiles?: RuntimeTransportProfileResponse[];
|
||||
clientManagers?: RuntimeClientManagerProfileResponse[];
|
||||
dllExtensions?: RuntimeDLLExtensionProfileResponse[];
|
||||
}
|
||||
|
||||
export interface GamePluginResponse {
|
||||
@@ -365,6 +384,7 @@ export interface MarketplacePluginResponse {
|
||||
aiPurposes: string[];
|
||||
productionLifecycle: PluginProductionLifecycleDeclaration;
|
||||
validationViolations?: string[];
|
||||
runtimeProfiles?: GamePluginRuntimeProfilesResponse;
|
||||
gameClientBridge?: GameClientBridgeManifestResponse;
|
||||
status: GamePluginStatus;
|
||||
source: string;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { GamePluginRuntimeProfilesResponse, RuntimeDLLExtensionProfileResponse } from "../api/types";
|
||||
|
||||
interface RuntimeDLLExtensionsPanelProps {
|
||||
runtimeProfiles?: GamePluginRuntimeProfilesResponse;
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
export function RuntimeDLLExtensionsPanel({ runtimeProfiles, embedded = false }: RuntimeDLLExtensionsPanelProps) {
|
||||
const extensions = runtimeProfiles?.dllExtensions ?? [];
|
||||
if (extensions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const content = <RuntimeDLLExtensionsContent extensions={extensions} />;
|
||||
return embedded ? <section className="console-module" aria-label="UE4SS DLL extensions">{content}</section> : <article className="console-panel" aria-label="UE4SS DLL extensions">{content}</article>;
|
||||
}
|
||||
|
||||
function RuntimeDLLExtensionsContent({ extensions }: { extensions: RuntimeDLLExtensionProfileResponse[] }) {
|
||||
return (
|
||||
<>
|
||||
<div className="panel-header">
|
||||
<h2>UE4SS DLL 扩展</h2>
|
||||
<span className="page-status">声明式、固定版本、启动前同步</span>
|
||||
</div>
|
||||
<p className="page-status">Run 不直接执行 DLL;已安装的 UE4SS 会在 SCUM 正常启动时加载它。</p>
|
||||
<div className="console-record-list">
|
||||
{extensions.map((extension) => (
|
||||
<article key={extension.key} className="console-record">
|
||||
<div className="console-record-head">
|
||||
<strong>{extension.displayName}</strong>
|
||||
<span className={extension.releaseState === "ready" ? "status-pill status-active" : "status-pill status-disabled"}>{extension.releaseState === "ready" ? "已固定发布" : "待发布"}</span>
|
||||
</div>
|
||||
<div className="console-record-meta">
|
||||
<span>{extension.key}</span>
|
||||
<span>版本 {extension.version}</span>
|
||||
</div>
|
||||
<dl className="detail-list">
|
||||
<div>
|
||||
<dt>下载声明</dt>
|
||||
<dd>{releaseLocation(extension)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>DLL 校验</dt>
|
||||
<dd>{extension.checksum || "待发布后固定"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>SCUM 兼容</dt>
|
||||
<dd>{extension.scumExecutableChecksum || "待发布后固定"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>UE4SS ABI</dt>
|
||||
<dd>{extension.ue4ssAbi || "待发布后固定"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="tag-list" aria-label={`${extension.displayName} activation policy`}>
|
||||
<span>启动前自动校验和更新</span>
|
||||
<span>运行:SCUM 服务器受监管启动</span>
|
||||
<span>由 UE4SS 正常加载</span>
|
||||
<span>{supportedTargets(extension)}</span>
|
||||
<span>Linux 启动前拒绝</span>
|
||||
</div>
|
||||
{extension.releaseState === "unpublished" && <p className="page-status">发布者尚未提供完整哈希、大小与兼容性固定值;此声明可查看,但不会派发给 Run。</p>}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function releaseLocation(extension: RuntimeDLLExtensionProfileResponse): string {
|
||||
return [extension.releaseHost, extension.releaseFilename].filter((value): value is string => Boolean(value)).join(" / ") || "待发布";
|
||||
}
|
||||
|
||||
function supportedTargets(extension: RuntimeDLLExtensionProfileResponse): string {
|
||||
return extension.supportedTargets.map((target) => `${target.os} ${target.arch}`).join(", ") || "Windows amd64";
|
||||
}
|
||||
@@ -24,6 +24,20 @@ const marketplacePlugin: MarketplacePluginResponse = {
|
||||
tags: ["example"],
|
||||
aiPurposes: ["logs.diagnose"],
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
|
||||
runtimeProfiles: {
|
||||
dllExtensions: [{
|
||||
key: "scum-simple-rcon-ue4ss",
|
||||
displayName: "SCUM Simple RCON UE4SS DLL",
|
||||
kind: "ue4ss-dll",
|
||||
activation: "server-start",
|
||||
version: "0.1.0-unpublished",
|
||||
releaseState: "unpublished",
|
||||
releaseHost: "cdn.npc0.com",
|
||||
releaseFilename: "scum_simple_rcon_ue4s.dll",
|
||||
supportedTargets: [{ os: "windows", arch: "amd64" }],
|
||||
updateOnStart: true
|
||||
}]
|
||||
},
|
||||
status: "installed",
|
||||
source: "platform-registry"
|
||||
};
|
||||
@@ -58,6 +72,10 @@ describe("PluginsPage", () => {
|
||||
expect(html).toContain("安装");
|
||||
expect(html).toContain("启用");
|
||||
expect(html).toContain("停用");
|
||||
expect(html).toContain("SCUM Simple RCON UE4SS DLL");
|
||||
expect(html).toContain("启动前自动校验和更新");
|
||||
expect(html).toContain("运行:SCUM 服务器受监管启动");
|
||||
expect(html).toContain("Linux 启动前拒绝");
|
||||
expect(html).toContain('role="dialog"');
|
||||
expect(html).not.toContain("billing");
|
||||
expect(html).not.toContain("/Users/");
|
||||
@@ -66,6 +84,7 @@ describe("PluginsPage", () => {
|
||||
expect(html).not.toContain("sk-");
|
||||
expect(html).not.toContain("password=");
|
||||
expect(html).not.toContain("apiKeyRef");
|
||||
expect(html).not.toContain("ue4ss/Mods/");
|
||||
});
|
||||
|
||||
it("keeps standalone fallback visibly isolated", () => {
|
||||
@@ -90,6 +109,7 @@ describe("PluginsPage", () => {
|
||||
expect(pluginsPageSource).toContain("retryDetail");
|
||||
expect(pluginsPageSource).toContain("当前账号为只读模式");
|
||||
expect(pluginsPageSource).toContain("平台会返回持久状态");
|
||||
expect(pluginsPageSource).toContain("UE4SS DLL 声明");
|
||||
expect(pluginsPageSource).toContain("actionPending !== null");
|
||||
expect(pluginsPageSource).toContain("operations?.begin");
|
||||
expect(pluginsPageSource).toContain("operations?.succeed");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { platformApiClient } from "../api/client";
|
||||
import type { GamePluginStatus, MarketplacePluginFilterRequest, MarketplacePluginResponse, MarketplacePluginStateAction } from "../api/types";
|
||||
import { ConfirmDialog, ManagementDialog } from "../components/OperationControls";
|
||||
import { PluginLifecycleWorkbench } from "../components/PluginLifecycleWorkbench";
|
||||
import { RuntimeDLLExtensionsPanel } from "../components/RuntimeDLLExtensionsPanel";
|
||||
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
||||
import { PageFrame } from "../components/PageFrame";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
@@ -294,7 +295,7 @@ export function PluginsPage({ initialState, session, operations }: PluginsPagePr
|
||||
<ConfirmDialog
|
||||
open={confirmAction !== null}
|
||||
title="确认插件状态变更"
|
||||
description={detail ? `将对 ${detail.name} 执行“${stateActionLabel(confirmAction ?? "disable")}”。平台会返回持久状态,失败时保留当前状态并允许重试。` : "请确认插件状态变更。"}
|
||||
description={detail ? pluginStateChangeDescription(detail, confirmAction ?? "disable") : "请确认插件状态变更。"}
|
||||
confirmLabel={stateActionLabel(confirmAction ?? "disable")}
|
||||
danger={confirmAction === "disable"}
|
||||
busy={actionPending !== null}
|
||||
@@ -347,6 +348,7 @@ function PluginDetail({ plugin, actionPending, actionsDisabled, onAction }: Plug
|
||||
<dd>{plugin.validationViolations?.length ? plugin.validationViolations.join(", ") : "manifest validated"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<RuntimeDLLExtensionsPanel runtimeProfiles={plugin.runtimeProfiles} embedded />
|
||||
<div className="action-strip plugin-detail-actions">
|
||||
<button type="button" className="primary-command" disabled={actionsDisabled || actionPending !== null || plugin.status === "installed"} onClick={() => onAction("install")} title="安装插件状态">
|
||||
<PlugZap size={16} />
|
||||
@@ -397,3 +399,16 @@ function stateActionLabel(action: MarketplacePluginStateAction): string {
|
||||
}
|
||||
return action === "disable" ? "停用" : "启用";
|
||||
}
|
||||
|
||||
function pluginStateChangeDescription(plugin: MarketplacePluginResponse, action: MarketplacePluginStateAction): string {
|
||||
const description = `将对 ${plugin.name} 执行“${stateActionLabel(action)}”。平台会返回持久状态,失败时保留当前状态并允许重试。`;
|
||||
if (action !== "install" && action !== "enable") {
|
||||
return description;
|
||||
}
|
||||
const extensions = plugin.runtimeProfiles?.dllExtensions ?? [];
|
||||
if (extensions.length === 0) {
|
||||
return description;
|
||||
}
|
||||
const releases = extensions.map((extension) => [extension.releaseHost, extension.releaseFilename].filter(Boolean).join(" / ") || extension.displayName).join(";");
|
||||
return `${description} UE4SS DLL 声明:Windows amd64 的 SCUM 启动前会校验并按固定发布下载或更新 ${releases};Run 不直接执行 DLL,Linux 会拒绝启动。`;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { configDiffViewFromPreview } from "./ServerDetailPage";
|
||||
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
|
||||
import clientManagerLifecyclePanelSource from "../components/ClientManagerLifecyclePanel.tsx?raw";
|
||||
import runtimeDLLExtensionsPanelSource from "../components/RuntimeDLLExtensionsPanel.tsx?raw";
|
||||
import artifactTransferSource from "../utils/artifactTransfer.ts?raw";
|
||||
import type { ServerConfigDiffPreviewResponse } from "../api/types";
|
||||
|
||||
@@ -103,6 +104,17 @@ describe("ServerDetailPage config write approval", () => {
|
||||
expect(serverDetailPageSource).not.toContain("sqlite://");
|
||||
});
|
||||
|
||||
it("renders declared UE4SS DLL update policy without local paths or RCON secrets", () => {
|
||||
expect(serverDetailPageSource).toContain("RuntimeDLLExtensionsPanel");
|
||||
expect(runtimeDLLExtensionsPanelSource).toContain("启动前自动校验和更新");
|
||||
expect(runtimeDLLExtensionsPanelSource).toContain("运行:SCUM 服务器受监管启动");
|
||||
expect(runtimeDLLExtensionsPanelSource).toContain("由 UE4SS 正常加载");
|
||||
expect(runtimeDLLExtensionsPanelSource).toContain("Linux 启动前拒绝");
|
||||
for (const forbidden of ["dllRef", "targetKey", "rconPort", "password="]) {
|
||||
expect(runtimeDLLExtensionsPanelSource).not.toContain(forbidden);
|
||||
}
|
||||
});
|
||||
|
||||
it("loads the dependency catalog only after runtime actions expose dependency operations", () => {
|
||||
const runtimeDistributionSectionSource = serverDetailPageSource.split("function RuntimeDistributionSection")[1]?.split("function RuntimeBindingFields")[0] ?? "";
|
||||
expect(runtimeDistributionSectionSource).toContain('action.key === "dependencies-check" || action.key === "dependencies-install"');
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ConfirmDialog, DiffView, UsageMeter } from "../components/OperationCont
|
||||
import { ClientManagerLifecyclePanel } from "../components/ClientManagerLifecyclePanel";
|
||||
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
||||
import { PluginLifecycleWorkbench } from "../components/PluginLifecycleWorkbench";
|
||||
import { RuntimeDLLExtensionsPanel } from "../components/RuntimeDLLExtensionsPanel";
|
||||
import {
|
||||
RuntimeTaskProgressDialog,
|
||||
runtimeBuildStages,
|
||||
@@ -293,6 +294,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
|
||||
onChanged={() => void refresh()}
|
||||
/>
|
||||
)}
|
||||
{section === "overview" && <RuntimeDLLExtensionsPanel runtimeProfiles={plugins.find((plugin) => plugin.id === instance.data.pluginId)?.runtimeProfiles} />}
|
||||
{section === "overview" && (
|
||||
<RuntimeDistributionSection
|
||||
instance={instance.data}
|
||||
|
||||
@@ -849,6 +849,28 @@
|
||||
"retainPrevious": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"dllExtensions": [
|
||||
{
|
||||
"key": "scum-simple-rcon-ue4ss",
|
||||
"displayName": "SCUM Simple RCON UE4SS DLL",
|
||||
"kind": "ue4ss-dll",
|
||||
"activation": "server-start",
|
||||
"version": "0.1.0-unpublished",
|
||||
"releaseState": "unpublished",
|
||||
"releaseUrl": "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll",
|
||||
"targetKey": "ue4ss/scum-simple-rcon",
|
||||
"modKey": "scum_simple_rcon",
|
||||
"dllRef": "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||
"supportedTargets": [
|
||||
{
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
}
|
||||
],
|
||||
"updateOnStart": true,
|
||||
"rconPort": 27015
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,12 @@
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/runtimeClientManagerProfile" },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"dllExtensions": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/runtimeDLLExtensionProfile" },
|
||||
"uniqueItems": true,
|
||||
"maxItems": 16
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -428,6 +434,7 @@
|
||||
},
|
||||
"transportKeys": { "type": "array", "items": { "$ref": "#/$defs/logicalKey" }, "uniqueItems": true },
|
||||
"clientManagerRef": { "$ref": "#/$defs/logicalKey" },
|
||||
"dllExtensionRefs": { "type": "array", "items": { "$ref": "#/$defs/logicalKey" }, "uniqueItems": true, "maxItems": 16 },
|
||||
"platforms": { "type": "array", "items": { "$ref": "#/$defs/runtimePlatform" }, "uniqueItems": true }
|
||||
}
|
||||
},
|
||||
@@ -668,6 +675,36 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"runtimeDLLExtensionProfile": {
|
||||
"type": "object",
|
||||
"required": ["key", "displayName", "kind", "activation", "version", "releaseState", "targetKey", "modKey", "dllRef", "supportedTargets", "updateOnStart", "rconPort"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": { "$ref": "#/$defs/logicalKey" },
|
||||
"displayName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"kind": { "const": "ue4ss-dll" },
|
||||
"activation": { "const": "server-start" },
|
||||
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$", "maxLength": 40 },
|
||||
"releaseState": { "enum": ["unpublished", "ready"] },
|
||||
"releaseUrl": { "type": "string", "pattern": "^https://[a-zA-Z0-9._~:/\\[\\]@!$&'()*+,;=%-]+\\.dll$", "maxLength": 240 },
|
||||
"checksum": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" },
|
||||
"sizeBytes": { "type": "integer", "minimum": 1, "maximum": 134217728 },
|
||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
||||
"modKey": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,79}$" },
|
||||
"dllRef": { "type": "string", "pattern": "^ue4ss/Mods/[a-z0-9][a-z0-9_-]{0,79}/dlls/main\\.dll$", "maxLength": 160 },
|
||||
"scumExecutableChecksum": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" },
|
||||
"ue4ssAbi": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,80}$" },
|
||||
"supportedTargets": { "type": "array", "items": { "$ref": "#/$defs/runtimeTarget" }, "minItems": 1, "maxItems": 1, "uniqueItems": true },
|
||||
"updateOnStart": { "const": true },
|
||||
"rconPort": { "type": "integer", "minimum": 1024, "maximum": 65535 }
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": { "properties": { "releaseState": { "const": "ready" } }, "required": ["releaseState"] },
|
||||
"then": { "required": ["releaseUrl", "checksum", "sizeBytes", "scumExecutableChecksum", "ue4ssAbi"] }
|
||||
}
|
||||
]
|
||||
},
|
||||
"aiPurpose": {
|
||||
"enum": [
|
||||
"config.read",
|
||||
|
||||
@@ -461,6 +461,100 @@ function validateClientManagerProfiles(manifest: unknown): string[] {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateDLLExtensionProfiles(manifest: unknown): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) {
|
||||
return [];
|
||||
}
|
||||
type DLLExtensionProfile = {
|
||||
key?: string;
|
||||
kind?: string;
|
||||
activation?: string;
|
||||
releaseState?: string;
|
||||
releaseUrl?: string;
|
||||
checksum?: string;
|
||||
sizeBytes?: number;
|
||||
targetKey?: string;
|
||||
modKey?: string;
|
||||
dllRef?: string;
|
||||
scumExecutableChecksum?: string;
|
||||
ue4ssAbi?: string;
|
||||
supportedTargets?: Array<{ os?: string; arch?: string }>;
|
||||
updateOnStart?: boolean;
|
||||
rconPort?: number;
|
||||
};
|
||||
type LifecycleProfile = { key?: string; mode?: string; capabilities?: string[]; dllExtensionRefs?: string[]; platforms?: string[] };
|
||||
const runtimeProfiles = (manifest as { runtimeProfiles?: { dllExtensions?: DLLExtensionProfile[]; lifecycleProfiles?: LifecycleProfile[] } }).runtimeProfiles;
|
||||
const profiles = runtimeProfiles?.dllExtensions ?? [];
|
||||
const errors: string[] = [];
|
||||
const checksumPattern = /^sha256:[a-fA-F0-9]{64}$/;
|
||||
const privateIPv4 = /^(127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/;
|
||||
|
||||
for (const [index, profile] of profiles.entries()) {
|
||||
const location = `manifest.runtimeProfiles.dllExtensions[${index}]`;
|
||||
if (profile.kind !== "ue4ss-dll" || profile.activation !== "server-start") {
|
||||
errors.push(`${location}: only a server-start ue4ss-dll extension is allowed`);
|
||||
}
|
||||
if (profile.dllRef !== `ue4ss/Mods/${profile.modKey ?? ""}/dlls/main.dll`) {
|
||||
errors.push(`${location}.dllRef: must be the declared UE4SS mod main.dll path`);
|
||||
}
|
||||
if (profile.updateOnStart !== true) {
|
||||
errors.push(`${location}.updateOnStart: must be true for a managed DLL extension`);
|
||||
}
|
||||
if (!Number.isInteger(profile.rconPort) || (profile.rconPort ?? 0) < 1024 || (profile.rconPort ?? 0) > 65535) {
|
||||
errors.push(`${location}.rconPort: must be an unprivileged TCP port`);
|
||||
}
|
||||
const target = profile.supportedTargets?.[0];
|
||||
if (profile.supportedTargets?.length !== 1 || target?.os !== "windows" || target?.arch !== "amd64") {
|
||||
errors.push(`${location}.supportedTargets: UE4SS DLL extensions support only windows/amd64`);
|
||||
}
|
||||
if (profile.releaseState !== "ready" && profile.releaseState !== "unpublished") {
|
||||
errors.push(`${location}.releaseState: must be ready or unpublished`);
|
||||
}
|
||||
if (profile.releaseState === "ready") {
|
||||
if (!checksumPattern.test(profile.checksum ?? "") || !checksumPattern.test(profile.scumExecutableChecksum ?? "")) {
|
||||
errors.push(`${location}: release and SCUM executable SHA-256 checksums are required`);
|
||||
}
|
||||
if (!Number.isInteger(profile.sizeBytes) || (profile.sizeBytes ?? 0) < 1 || (profile.sizeBytes ?? 0) > 128 * 1024 * 1024) {
|
||||
errors.push(`${location}.sizeBytes: must be a bounded DLL size`);
|
||||
}
|
||||
if (!profile.ue4ssAbi) {
|
||||
errors.push(`${location}.ue4ssAbi: an ABI marker is required`);
|
||||
}
|
||||
}
|
||||
if (profile.releaseUrl) {
|
||||
try {
|
||||
const parsed = new URL(profile.releaseUrl);
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host === "::1" || privateIPv4.test(host) || !parsed.pathname.toLowerCase().endsWith(".dll")) {
|
||||
errors.push(`${location}.releaseUrl: must be an approved public HTTPS DLL release`);
|
||||
}
|
||||
} catch {
|
||||
errors.push(`${location}.releaseUrl: URL is invalid`);
|
||||
}
|
||||
} else if (profile.releaseState === "ready") {
|
||||
errors.push(`${location}.releaseUrl: a ready release URL is required`);
|
||||
}
|
||||
}
|
||||
|
||||
const declared = new Set(profiles.map((profile) => profile.key).filter((key): key is string => Boolean(key)));
|
||||
const stateByKey = new Map(profiles.map((profile) => [profile.key, profile.releaseState]));
|
||||
for (const [index, profile] of (runtimeProfiles?.lifecycleProfiles ?? []).entries()) {
|
||||
const extensionRefs = profile.dllExtensionRefs ?? [];
|
||||
if (extensionRefs.length > 0 && (profile.mode !== "local-process" || !profile.capabilities?.includes("process.start") || profile.platforms?.length !== 1 || profile.platforms[0] !== "windows")) {
|
||||
errors.push(`manifest.runtimeProfiles.lifecycleProfiles[${index}]: a DLL extension requires a windows local-process profile with process.start`);
|
||||
}
|
||||
for (const key of extensionRefs) {
|
||||
if (!declared.has(key)) {
|
||||
errors.push(`manifest.runtimeProfiles.lifecycleProfiles[${index}].dllExtensionRefs: undeclared DLL extension ${key}`);
|
||||
}
|
||||
if (stateByKey.get(key) !== "ready") {
|
||||
errors.push(`manifest.runtimeProfiles.lifecycleProfiles[${index}].dllExtensionRefs: DLL extension ${key} is not ready for activation`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) {
|
||||
return [];
|
||||
@@ -1011,6 +1105,7 @@ export function validateManifestFile(manifestPath: string): string[] {
|
||||
errors.push(...scanUnsafeValues(manifest, "manifest"));
|
||||
errors.push(...validateDependencyPlans(manifest));
|
||||
errors.push(...validateClientManagerProfiles(manifest));
|
||||
errors.push(...validateDLLExtensionProfiles(manifest));
|
||||
errors.push(...validateGameClientBridgeCatalog(manifest));
|
||||
errors.push(...validateGameClientBridgeSchemaFiles(manifest, manifestDir));
|
||||
errors.push(...validateGameClientBridgeCompanionConfig(manifest, manifestDir));
|
||||
|
||||
@@ -388,6 +388,7 @@ export interface RuntimeLifecycleProfile {
|
||||
actionRefs?: Partial<Record<PluginLifecycleAction, string>>;
|
||||
transportKeys?: string[];
|
||||
clientManagerRef?: string;
|
||||
dllExtensionRefs?: string[];
|
||||
platforms?: RuntimePlatform[];
|
||||
}
|
||||
|
||||
@@ -497,6 +498,26 @@ export interface RuntimeClientManagerProfile {
|
||||
};
|
||||
}
|
||||
|
||||
export interface RuntimeDLLExtensionProfile {
|
||||
key: string;
|
||||
displayName: string;
|
||||
kind: "ue4ss-dll";
|
||||
activation: "server-start";
|
||||
version: string;
|
||||
releaseState: "unpublished" | "ready";
|
||||
releaseUrl?: string;
|
||||
checksum?: `sha256:${string}`;
|
||||
sizeBytes?: number;
|
||||
targetKey: string;
|
||||
modKey: string;
|
||||
dllRef: string;
|
||||
scumExecutableChecksum?: `sha256:${string}`;
|
||||
ue4ssAbi?: string;
|
||||
supportedTargets: [{ os: "windows"; arch: "amd64" }];
|
||||
updateOnStart: true;
|
||||
rconPort: number;
|
||||
}
|
||||
|
||||
export interface GamePluginRuntimeProfiles {
|
||||
discovery?: RuntimeDiscoveryProbe[];
|
||||
lifecycleProfiles?: RuntimeLifecycleProfile[];
|
||||
@@ -506,6 +527,7 @@ export interface GamePluginRuntimeProfiles {
|
||||
logEvents?: RuntimeLogEventDeclaration[];
|
||||
transportProfiles?: RuntimeTransportProfile[];
|
||||
clientManagers?: RuntimeClientManagerProfile[];
|
||||
dllExtensions?: RuntimeDLLExtensionProfile[];
|
||||
}
|
||||
|
||||
export interface PluginArtifactReference {
|
||||
|
||||
@@ -173,6 +173,53 @@ describe("plugin manifest validation", () => {
|
||||
expect(validateManifestFile("examples/scum-server-plugin/manifest.json")).toEqual([]);
|
||||
});
|
||||
|
||||
it("accepts a pinned ready SCUM UE4SS DLL release and lifecycle reference", () => {
|
||||
const errors = validateTemporaryScumCompanionManifest((manifest) => {
|
||||
const extension = manifest.runtimeProfiles.dllExtensions[0];
|
||||
extension.releaseState = "ready";
|
||||
extension.version = "0.1.0";
|
||||
extension.checksum = `sha256:${"a".repeat(64)}`;
|
||||
extension.sizeBytes = 1048576;
|
||||
extension.scumExecutableChecksum = `sha256:${"b".repeat(64)}`;
|
||||
extension.ue4ssAbi = "ue4ss-3.0";
|
||||
manifest.runtimeProfiles.lifecycleProfiles[0].dllExtensionRefs = [extension.key];
|
||||
});
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects unpinned, unsafe, or unpublished SCUM UE4SS DLL activation", () => {
|
||||
const missingPins = validateTemporaryScumCompanionManifest((manifest) => {
|
||||
const extension = manifest.runtimeProfiles.dllExtensions[0];
|
||||
extension.releaseState = "ready";
|
||||
manifest.runtimeProfiles.lifecycleProfiles[0].dllExtensionRefs = [extension.key];
|
||||
});
|
||||
expect(missingPins.some((error) => error.includes("checksum") || error.includes("sizeBytes") || error.includes("ue4ssAbi"))).toBe(true);
|
||||
|
||||
const unsafeURL = validateTemporaryScumCompanionManifest((manifest) => {
|
||||
const extension = manifest.runtimeProfiles.dllExtensions[0];
|
||||
extension.releaseState = "ready";
|
||||
extension.releaseUrl = "https://127.0.0.1/scum.exe";
|
||||
extension.checksum = `sha256:${"a".repeat(64)}`;
|
||||
extension.sizeBytes = 1048576;
|
||||
extension.scumExecutableChecksum = `sha256:${"b".repeat(64)}`;
|
||||
extension.ue4ssAbi = "ue4ss-3.0";
|
||||
});
|
||||
expect(unsafeURL.some((error) => error.includes("releaseUrl"))).toBe(true);
|
||||
|
||||
const queryURL = validateTemporaryScumCompanionManifest((manifest) => {
|
||||
manifest.runtimeProfiles.dllExtensions[0].releaseUrl = "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll?release=.dll";
|
||||
});
|
||||
expect(queryURL.some((error) => error.includes("releaseUrl"))).toBe(true);
|
||||
|
||||
const unpublishedReference = validateTemporaryScumCompanionManifest((manifest) => {
|
||||
const extension = manifest.runtimeProfiles.dllExtensions[0];
|
||||
manifest.runtimeProfiles.lifecycleProfiles[0].dllExtensionRefs = [extension.key];
|
||||
manifest.runtimeProfiles.lifecycleProfiles[0].platforms = ["windows", "linux"];
|
||||
});
|
||||
expect(unpublishedReference.some((error) => error.includes("not ready for activation"))).toBe(true);
|
||||
expect(unpublishedReference.some((error) => error.includes("windows local-process"))).toBe(true);
|
||||
});
|
||||
|
||||
it("defines a generated SCUM companion config without inline proof or session material", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
||||
|
||||
Reference in New Issue
Block a user