feat: support custom server deployment drafts

This commit is contained in:
npc0-hue
2026-07-24 16:56:28 +08:00
parent 292b380f3c
commit 220ef91a8e
36 changed files with 1520 additions and 85 deletions
@@ -19,7 +19,12 @@
"linux",
"darwin"
],
"createFormSchema": "schemas/create-form.schema.json"
"createFormSchema": "schemas/create-form.schema.json",
"createFields": [
{ "key": "serverName", "label": "服务器名称", "type": "text", "required": true, "configKey": "motd" },
{ "key": "gamePort", "label": "游戏端口", "type": "port", "required": true, "defaultValue": "25565", "configKey": "serverPort" },
{ "key": "rconPort", "label": "RCON 端口", "type": "port", "required": true, "defaultValue": "25575", "configKey": "rconPort" }
]
},
"capabilities": [
"process.install",
@@ -19,7 +19,13 @@
"windows",
"linux"
],
"createFormSchema": "schemas/create-form.schema.json"
"createFormSchema": "schemas/create-form.schema.json",
"createFields": [
{ "key": "serverName", "label": "SCUM 服务器名称", "type": "text", "required": true, "configKey": "serverName" },
{ "key": "gamePort", "label": "游戏端口", "type": "port", "required": true, "defaultValue": "7777", "configKey": "gamePort" },
{ "key": "queryPort", "label": "查询端口", "type": "port", "required": true, "defaultValue": "27015", "configKey": "queryPort" },
{ "key": "maxPlayers", "label": "最大玩家数", "type": "number", "required": true, "defaultValue": "64", "configKey": "maxPlayers" }
]
},
"capabilities": [
"process.install",
@@ -26,7 +26,12 @@
"type": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]*$", "maxLength": 80 },
"displayName": { "type": "string", "minLength": 1, "maxLength": 80 },
"supportedOS": { "type": "array", "items": { "enum": ["windows", "linux", "darwin"] } },
"createFormSchema": { "$ref": "#/$defs/relativeJsonRef" }
"createFormSchema": { "$ref": "#/$defs/relativeJsonRef" },
"createFields": {
"type": "array",
"maxItems": 32,
"items": { "$ref": "#/$defs/pluginCreateField" }
}
}
},
"bridge": {
@@ -191,6 +196,20 @@
}
},
"$defs": {
"pluginCreateField": {
"type": "object",
"required": ["key", "label", "type"],
"additionalProperties": false,
"properties": {
"key": { "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$", "maxLength": 80 },
"label": { "type": "string", "minLength": 1, "maxLength": 60 },
"type": { "enum": ["text", "number", "boolean", "select", "port"] },
"required": { "type": "boolean" },
"defaultValue": { "type": "string", "maxLength": 256 },
"options": { "type": "array", "maxItems": 32, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 120 } },
"configKey": { "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$", "maxLength": 80 }
}
},
"gameClientBridgeManifest": {
"type": "object",
"required": ["commands", "snapshots", "commandRetentionSeconds", "maxCommands"],
+20
View File
@@ -111,6 +111,25 @@ function scanUnsafeValues(value: unknown, location: string): string[] {
return [];
}
function validateCreateFieldDeclarations(manifest: unknown): string[] {
if (typeof manifest !== "object" || manifest === null || !("server" in manifest)) return [];
const server = (manifest as { server?: { createFields?: Array<{ key?: unknown; type?: unknown; defaultValue?: unknown; options?: unknown }> } }).server;
if (!Array.isArray(server?.createFields)) return [];
const errors: string[] = [];
for (const [index, field] of server.createFields.entries()) {
const location = `manifest.server.createFields[${index}]`;
for (const [name, value] of [["defaultValue", field.defaultValue], ["options", field.options]] as const) {
const values = Array.isArray(value) ? value : [value];
for (const item of values) {
if (typeof item !== "string") continue;
if (item.startsWith("/") || item.startsWith("\\\\") || /^[a-z]:[\\/]/i.test(item)) errors.push(`${location}.${name}: raw host path access is not allowed`);
errors.push(...unsafeStringReasons(item).map((reason) => `${location}.${name}: ${reason}`));
}
}
}
return errors;
}
function isSafeRelativeJsonRef(value: string): boolean {
return /^(?!\/)(?![A-Za-z]:)(?!.*:\/\/)(?!.*\.\.)[a-zA-Z0-9_./-]+\.json$/.test(value);
}
@@ -1103,6 +1122,7 @@ export function validateManifestFile(manifestPath: string): string[] {
}
errors.push(...scanUnsafeValues(manifest, "manifest"));
errors.push(...validateCreateFieldDeclarations(manifest));
errors.push(...validateDependencyPlans(manifest));
errors.push(...validateClientManagerProfiles(manifest));
errors.push(...validateDLLExtensionProfiles(manifest));
+11
View File
@@ -173,6 +173,17 @@ describe("plugin manifest validation", () => {
expect(validateManifestFile("examples/scum-server-plugin/manifest.json")).toEqual([]);
});
it("rejects unsupported or unsafe inline create-field declarations", () => {
const malformed = validateTemporaryScumCompanionManifest((manifest) => {
manifest.server.createFields[0].type = "path";
});
expect(malformed.some((error) => error.includes("createFields") && error.includes("type"))).toBe(true);
const unsafe = validateTemporaryScumCompanionManifest((manifest) => {
manifest.server.createFields[0].defaultValue = "/srv/hidden-server";
});
expect(unsafe.some((error) => error.includes("raw host path"))).toBe(true);
});
it("accepts a pinned ready SCUM UE4SS DLL release and lifecycle reference", () => {
const errors = validateTemporaryScumCompanionManifest((manifest) => {
const extension = manifest.runtimeProfiles.dllExtensions[0];