feat(plugin): add SCUM ownership migration foundation
This commit is contained in:
@@ -199,68 +199,18 @@ describe("plugin manifest validation", () => {
|
||||
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];
|
||||
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("declares Source RCON for the Windows local lifecycle without activating the unpublished DLL", () => {
|
||||
it("removes direct RCON, database, and DLL extension declarations", () => {
|
||||
const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json");
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
|
||||
runtimeProfiles?: {
|
||||
lifecycleProfiles?: Array<{ key: string; capabilities?: string[]; transportKeys?: string[]; dllExtensionRefs?: string[] }>;
|
||||
dllExtensions?: Array<{ key: string; releaseState: string }>;
|
||||
lifecycleProfiles?: Array<{ key: string; capabilities?: string[]; transportKeys?: string[] }>;
|
||||
transportProfiles?: Array<{ kind?: string }>;
|
||||
};
|
||||
};
|
||||
const local = manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "run-local");
|
||||
const extension = manifest.runtimeProfiles?.dllExtensions?.find((candidate) => candidate.key === "scum-simple-rcon-ue4ss");
|
||||
|
||||
expect(local?.capabilities).toContain("remote.run.rcon.command");
|
||||
expect(local?.transportKeys).toContain("rcon");
|
||||
expect(local?.dllExtensionRefs).toBeUndefined();
|
||||
expect(extension?.releaseState).toBe("unpublished");
|
||||
});
|
||||
|
||||
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);
|
||||
expect(local?.capabilities).not.toContain("remote.run.rcon.command");
|
||||
expect(local?.transportKeys).not.toContain("rcon");
|
||||
expect(manifest.runtimeProfiles?.transportProfiles?.some((profile) => profile.kind === "sqlite" || profile.kind === "mysql" || profile.kind === "rcon")).toBe(false);
|
||||
});
|
||||
|
||||
it("defines a generated SCUM companion config without inline proof or session material", () => {
|
||||
@@ -591,7 +541,7 @@ describe("plugin manifest validation", () => {
|
||||
expect(operationsPage?.snapshotTypes).toEqual(expect.arrayContaining(expectedTypes));
|
||||
});
|
||||
|
||||
it("declares read-only bounded SCUM database query templates", () => {
|
||||
it("does not declare direct database query templates", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
||||
permissions: string[];
|
||||
@@ -612,62 +562,12 @@ describe("plugin manifest validation", () => {
|
||||
pages: Array<{ key: string; permissions?: string[]; bridgeActions?: string[] }>;
|
||||
runtimeProfiles?: { transportProfiles?: Array<{ key: string; kind: string; targetKey?: string; capabilities: string[] }> };
|
||||
};
|
||||
const expectedKeys = ["scum.player.by-id", "scum.player.search", "scum.squad.members", "scum.vehicle.owner", "scum.flag.ownership"];
|
||||
const transport = manifest.runtimeProfiles?.transportProfiles?.find((profile) => profile.key === "sqlite-db");
|
||||
|
||||
expect(transport).toMatchObject({ kind: "sqlite", targetKey: "db/sqlite" });
|
||||
expect(transport?.capabilities).toContain("remote.run.db.sqlite.query");
|
||||
expect(manifest.gameClientBridge.queryTemplates.map((template) => template.key)).toEqual(expect.arrayContaining(expectedKeys));
|
||||
expect(new Set(manifest.gameClientBridge.queryTemplates.map((template) => template.key)).size).toBe(manifest.gameClientBridge.queryTemplates.length);
|
||||
for (const template of manifest.gameClientBridge.queryTemplates) {
|
||||
expect(template.engine).toBe("sqlite");
|
||||
expect(template.permission).toBe("server.game-client.read");
|
||||
expect(manifest.permissions).toContain(template.permission);
|
||||
expect(template.transportKey).toBe("sqlite-db");
|
||||
expect(template.targetKey).toBe("db/sqlite");
|
||||
expect(template.maxRows).toBeGreaterThanOrEqual(1);
|
||||
expect(template.maxRows).toBeLessThanOrEqual(500);
|
||||
expect(template.timeoutSeconds).toBeGreaterThanOrEqual(1);
|
||||
expect(template.timeoutSeconds).toBeLessThanOrEqual(60);
|
||||
expect(JSON.stringify(template).toLowerCase()).not.toMatch(/\bselect\b|\binsert\b|\bupdate\b|\bdelete\b|\bpragma\b|dsn|hostpath|socket|password|credential/);
|
||||
|
||||
for (const schemaRef of [template.parameterSchemaRef, template.resultSchemaRef]) {
|
||||
const schema = JSON.parse(fs.readFileSync(path.join(pluginDir, schemaRef), "utf8")) as Record<string, unknown>;
|
||||
const visit = (value: unknown): void => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(visit);
|
||||
return;
|
||||
}
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return;
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
if (record.type === "object" || Object.hasOwn(record, "properties")) {
|
||||
expect(record.additionalProperties).toBe(false);
|
||||
}
|
||||
if (record.type === "array") {
|
||||
expect(record.maxItems).toBeGreaterThan(0);
|
||||
}
|
||||
if (record.type === "string") {
|
||||
expect(record.maxLength).toBeGreaterThan(0);
|
||||
}
|
||||
if (record.type === "integer" || record.type === "number") {
|
||||
expect(record.maximum).toBeDefined();
|
||||
}
|
||||
Object.values(record).forEach(visit);
|
||||
};
|
||||
expect(schema.type).toBe("object");
|
||||
expect(schema.additionalProperties).toBe(false);
|
||||
expect(JSON.stringify(schema).toLowerCase()).not.toMatch(/\bselect\b[\s\S]*\bfrom\b|\binsert\s+into\b|\bdelete\s+from\b|\bpragma\b|dsn|hostpath|runsocket|password|credential/);
|
||||
visit(schema);
|
||||
}
|
||||
}
|
||||
|
||||
const operationsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "files-config");
|
||||
const operationsPluginPage = manifest.pages.find((page) => page.key === "files-config");
|
||||
expect(operationsPage?.queryTemplateKeys).toEqual(expect.arrayContaining(expectedKeys));
|
||||
expect(operationsPluginPage?.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.remote.access"]));
|
||||
expect(operationsPluginPage?.bridgeActions).toContain("remote.access.request");
|
||||
expect(manifest.gameClientBridge.queryTemplates ?? []).toEqual([]);
|
||||
expect(operationsPage?.queryTemplateKeys ?? []).toEqual([]);
|
||||
expect(operationsPluginPage?.permissions).not.toContain("server.remote.access");
|
||||
expect(operationsPluginPage?.bridgeActions).not.toContain("remote.access.request");
|
||||
});
|
||||
|
||||
it("declares typed SCUM semantic log events with bounded schemas", () => {
|
||||
|
||||
Reference in New Issue
Block a user