feat: support custom server deployment drafts
This commit is contained in:
@@ -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"],
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user