feat: add UE4SS DLL runtime extension
This commit is contained in:
@@ -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