import { describe, expect, it } from "vitest"; import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { Ajv2020 } from "ajv/dist/2020.js"; import { bridgeError, canRequestBridgeAction, createAIInvocationRequest, createArtifactOpenRequest, createClientManagerRequest, createBridgeExecutionRequest, createLifecycleDispatchRequest, createProductionPluginLifecycleRequest, createBridgeRequest, createDependencyActionRequest, createLogBackfillRequest, createGameClientBridgeQueueRequest, createRemoteAccessRequest, createRunDistributionRequest, hasPluginPermission, parseArtifactReference, parseClientManagerLifecycleStatus, parseBridgeExecutionResponse, parseAIInvocationResponse, type GameClientBridgeQueryTemplateDeclaration, type GameClientBridgeOperationTemplateDeclaration, type GameClientBridgeProtectedRequestDeclaration, type GameClientBridgeCompanionDeclaration, type GamePluginManifest, type SCUMLiveDataManifestDeclaration, type RuntimeLogEventDeclaration, type RuntimeClientManagerProfile, type PluginLifecycleActionDeclaration, type PluginBridgeContext } from "../sdk/index.js"; import { validateLifecycleActionFile, validateManifestFile } from "../scripts/validate-manifest.js"; const pluginsRoot = fileURLToPath(new URL("..", import.meta.url)); type MutableBridgeManifest = { capabilities: string[]; permissions: string[]; remoteAccess?: { methods: string[]; runCapabilities?: string[]; databaseEngines?: string[]; }; runtimeProfiles?: { transportProfiles?: Array>; }; pages?: Array<{ key?: string; permissions?: string[]; bridgeActions?: string[] }>; gameClientBridge: { commands: Array>; snapshots: Array>; queryTemplates?: Array>; operationTemplates?: Array>; commandRetentionSeconds: number; maxCommands: number; pages: Array>; }; }; function bridgeObjectSchema(properties: Record, required: string[] = []): Record { return { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", additionalProperties: false, properties, ...(required.length > 0 ? { required } : {}) }; } function writeFixtureJSON(fixtureDir: string, relativePath: string, value: unknown): void { const target = path.join(fixtureDir, relativePath); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } function sha256FixtureDigest(fixtureDir: string, relativePath: string): string { return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(path.join(fixtureDir, relativePath))).digest("hex")}`; } function listProductionGoFiles(directory: string): string[] { return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const target = path.join(directory, entry.name); if (entry.isDirectory()) { return listProductionGoFiles(target); } return entry.isFile() && entry.name.endsWith(".go") && !entry.name.endsWith("_test.go") ? [target] : []; }); } function validateTemporaryBridgeManifest(mutate?: (manifest: MutableBridgeManifest, fixtureDir: string) => void): string[] { const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-bridge-manifest-")); try { fs.cpSync(path.join(pluginsRoot, "examples/dev-game-plugin"), fixtureDir, { recursive: true }); const manifestPath = path.join(fixtureDir, "manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as MutableBridgeManifest; manifest.capabilities = [...manifest.capabilities, "remote.run.db.sqlite.query", "remote.run.protected.rcon", "remote.run.protected.sql"]; manifest.permissions = [...manifest.permissions, "server.game-client.command", "server.game-client.read", "server.game-client.maintenance"]; manifest.remoteAccess = { methods: ["run"], runCapabilities: ["remote.run.db.sqlite.query", "remote.run.protected.rcon", "remote.run.protected.sql"], databaseEngines: ["sqlite"] }; manifest.runtimeProfiles = { transportProfiles: [ { key: "sqlite-db", kind: "sqlite", targetKey: "db/sqlite", capabilities: ["remote.run.db.sqlite.query"] }, { key: "scum-rcon", kind: "rcon", targetKey: "scum-rcon", capabilities: ["remote.run.protected.rcon"] }, { key: "scum-mutation-db", kind: "sqlite", targetKey: "scum-mutation-db", capabilities: ["remote.run.protected.sql"] } ] }; const overviewPage = manifest.pages?.find((page) => page.key === "overview"); if (overviewPage) { overviewPage.permissions = [...(overviewPage.permissions ?? []), "server.game-client.read", "server.game-client.command", "server.game-client.maintenance", "server.remote.access"]; overviewPage.bridgeActions = [...(overviewPage.bridgeActions ?? []), "remote.access.request"]; } manifest.gameClientBridge = { commands: [{ type: "announcement.send", title: "Send announcement", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/announcement.schema.json", resultSchemaRef: "schemas/bridge/announcement-result.schema.json", timeoutSeconds: 60, maxPayloadBytes: 4096 }], snapshots: [{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.schema.json", keepForSeconds: 3600, maxRecords: 100 }], queryTemplates: [{ key: "player.by-id", title: "Find player by ID", permission: "server.game-client.read", engine: "sqlite", transportKey: "sqlite-db", targetKey: "db/sqlite", parameterSchemaRef: "schemas/bridge/player-by-id.parameters.schema.json", resultSchemaRef: "schemas/bridge/player-by-id.result.schema.json", maxRows: 1, timeoutSeconds: 10 }], operationTemplates: [ { key: "player.fame.set", title: "Set player fame", permission: "server.game-client.command", approvalLevel: "operator", kind: "rcon", transportKey: "scum-rcon", targetKey: "scum-rcon", payloadSchemaRef: "schemas/bridge/player-fame-set.payload.schema.json", resultSchemaRef: "schemas/bridge/player-fame-set.result.schema.json", confirmationSchemaRef: "schemas/bridge/player-fame-set.confirmation.schema.json", timeoutSeconds: 60, maxPayloadBytes: 2048, safety: { requiresApproval: true, requiresConfirmation: true } }, { key: "player.attribute.855.set", title: "Set player attribute 855", permission: "server.game-client.maintenance", approvalLevel: "platform-admin", kind: "sqlite-mutation", transportKey: "scum-mutation-db", targetKey: "scum-mutation-db", payloadSchemaRef: "schemas/bridge/player-attribute-855-set.payload.schema.json", resultSchemaRef: "schemas/bridge/player-attribute-855-set.result.schema.json", confirmationSchemaRef: "schemas/bridge/player-attribute-855-set.confirmation.schema.json", timeoutSeconds: 120, maxPayloadBytes: 4096, maxRowsAffected: 1, mutation: { fieldKey: "855", tableKey: "prisoner", identityKey: "user_profile_id", valueKey: "value", confirmationQueryKey: "player.by-id", allowedValueType: "integer", minValue: 0, maxValue: 100000 }, safety: { requiresApproval: true, requiresOfflinePlayer: true, requiresBeforeValue: true, requiresConfirmation: true, backupRequired: true } } ], commandRetentionSeconds: 86400, maxCommands: 1000, pages: [{ pageKey: "overview", commandTypes: ["announcement.send"], snapshotTypes: ["players"], queryTemplateKeys: ["player.by-id"], operationKeys: ["player.fame.set", "player.attribute.855.set"] }] }; writeFixtureJSON(fixtureDir, "schemas/bridge/announcement.schema.json", bridgeObjectSchema({ message: { type: "string", minLength: 1, maxLength: 200 } }, ["message"])); writeFixtureJSON(fixtureDir, "schemas/bridge/announcement-result.schema.json", bridgeObjectSchema({ accepted: { type: "boolean" } }, ["accepted"])); writeFixtureJSON(fixtureDir, "schemas/bridge/players.schema.json", bridgeObjectSchema({ players: { type: "array", maxItems: 100, items: bridgeObjectSchema({ id: { type: "string", minLength: 1, maxLength: 80 } }, ["id"]) } }, ["players"])); writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.parameters.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"])); writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.result.schema.json", bridgeObjectSchema({ players: { type: "array", maxItems: 1, items: bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"]) } }, ["players"])); writeFixtureJSON(fixtureDir, "schemas/bridge/player-fame-set.payload.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 }, fame: { type: "integer", minimum: 0, maximum: 2147483647 } }, ["playerId", "fame"])); writeFixtureJSON(fixtureDir, "schemas/bridge/player-fame-set.result.schema.json", bridgeObjectSchema({ outcome: { enum: ["queued", "succeeded", "failed", "unknown"] } }, ["outcome"])); writeFixtureJSON(fixtureDir, "schemas/bridge/player-fame-set.confirmation.schema.json", bridgeObjectSchema({ playerId: { type: "string" }, fame: { type: "integer" } }, ["playerId", "fame"])); writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.payload.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 }, before: { type: "number" }, after: { type: "number" }, safetyWindow: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId", "before", "after", "safetyWindow"])); writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.result.schema.json", bridgeObjectSchema({ outcome: { enum: ["succeeded", "failed", "unknown"] }, rowsAffected: { type: "integer", minimum: 0, maximum: 1 } }, ["outcome", "rowsAffected"])); writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.confirmation.schema.json", bridgeObjectSchema({ playerId: { type: "string" }, value: { type: "number" } }, ["playerId", "value"])); mutate?.(manifest, fixtureDir); writeFixtureJSON(fixtureDir, "manifest.json", manifest); return validateManifestFile(manifestPath); } finally { fs.rmSync(fixtureDir, { recursive: true, force: true }); } } type MutableLogEventManifest = { permissions: string[]; runtimeProfiles?: { logSources?: Array>; logEvents?: Array>; }; }; function validateTemporaryLogEventManifest(mutate?: (manifest: MutableLogEventManifest, fixtureDir: string) => void): string[] { const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-log-event-manifest-")); try { fs.cpSync(path.join(pluginsRoot, "examples/dev-game-plugin"), fixtureDir, { recursive: true }); const manifestPath = path.join(fixtureDir, "manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as MutableLogEventManifest; manifest.runtimeProfiles = { logSources: [{ key: "server-events", kind: "file.tail", targetKey: "logs/server", streamKey: "game.server", cursorKind: "fingerprint", retentionDays: 30 }], logEvents: [{ key: "player-login", title: "Player login", sourceKey: "server-events", eventType: "game.login", permission: "server.logs.read", schemaRef: "schemas/log-events/login.event.schema.json", retentionDays: 30, severity: "info" }] }; writeFixtureJSON(fixtureDir, "schemas/log-events/login.event.schema.json", bridgeObjectSchema({ occurredAt: { type: "string", minLength: 1, maxLength: 40 }, playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["occurredAt", "playerId"])); mutate?.(manifest, fixtureDir); writeFixtureJSON(fixtureDir, "manifest.json", manifest); return validateManifestFile(manifestPath); } finally { fs.rmSync(fixtureDir, { recursive: true, force: true }); } } function validateTemporaryScumCompanionManifest(mutate: (manifest: Record, fixtureDir: string) => void): string[] { const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-scum-companion-manifest-")); try { fs.cpSync(path.join(pluginsRoot, "examples/scum-server-plugin"), fixtureDir, { recursive: true }); const manifestPath = path.join(fixtureDir, "manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as Record; mutate(manifest, fixtureDir); writeFixtureJSON(fixtureDir, "manifest.json", manifest); return validateManifestFile(manifestPath); } finally { fs.rmSync(fixtureDir, { recursive: true, force: true }); } } describe("plugin manifest validation", () => { it("accepts the development example manifest", () => { expect(validateManifestFile("examples/dev-game-plugin/manifest.json")).toEqual([]); }); it("accepts the SCUM server plugin manifest", () => { expect(validateManifestFile("examples/scum-server-plugin/manifest.json")).toEqual([]); }); it("removes unverified SCUM query and operation declarations", () => { const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; protectedRequest?: { kind: string } }>; queryTemplates: Array<{ key: string }>; operationTemplates: Array<{ key: string; kind: string }> } }; const commands = manifest.gameClientBridge.commands.filter((candidate) => candidate.protectedRequest); expect(commands).toEqual([]); expect(manifest.gameClientBridge.commands.map((command) => command.type)).not.toEqual(expect.arrayContaining(["config.read", "config.patch", "database.request", "management.rcon.request", "management.program.request"])); expect(manifest.gameClientBridge.queryTemplates).toEqual([]); expect(manifest.gameClientBridge.operationTemplates).toEqual([]); expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(false); }); it("keeps SCUM database-backed live-data gates disabled until current-service evidence exists", () => { const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration; remoteAccess: { runCapabilities: string[] } }; expect(manifest.capabilities).toContain("remote.run.db.sqlite.probe"); expect(manifest.remoteAccess.runCapabilities).toContain("remote.run.db.sqlite.probe"); expect(manifest.scumLiveData.probe).toMatchObject({ capability: "remote.run.db.sqlite.probe", targetKey: "scum-database" }); expect(manifest.runtimeProfiles?.dataTargets?.find((target) => target.key === "scum-database")).toMatchObject({ kind: "sqlite.snapshot", transportKey: "scum-database", sourceRootKey: "server-root", sourcePath: "SCUM/Saved/SaveFiles/SCUM.db", workspaceKey: "databases/scum-database", refreshPolicy: "on-demand-snapshot" }); expect(manifest.scumLiveData.capabilityGates.map((gate) => gate.capability)).toEqual(expect.arrayContaining(["players.read", "squads.read", "vehicles.read", "flags.read", "positions.read", "profile-xml.write", "economy-command.write", "gift-command.write"])); expect(manifest.scumLiveData.capabilityGates.every((gate) => gate.gate === "disabled" && gate.evidenceStatus === "missing")).toBe(true); expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/); }); it("requires SCUM schema probe targets to have generated Run workspace data targets", () => { const errors = validateTemporaryScumCompanionManifest((manifest) => { manifest.runtimeProfiles.dataTargets = []; }); expect(errors.some((error) => error.includes("probe.targetKey") && error.includes("runtime data target"))).toBe(true); }); it("rejects enabling SCUM live-data gates without compatible evidence and immutable digests", () => { const errors = validateTemporaryScumCompanionManifest((manifest) => { manifest.scumLiveData.capabilityGates[1] = { ...manifest.scumLiveData.capabilityGates[1], gate: "enabled", evidenceStatus: "missing" }; }); expect(errors.some((error) => error.includes("enabled gates require compatible evidence"))).toBe(true); expect(errors.some((error) => error.includes("enabled gates require a schema fingerprint"))).toBe(true); expect(errors.some((error) => error.includes("enabled gates require immutable asset digests"))).toBe(true); }); it("accepts safe SCUM live-data asset declarations without enabling unproven capabilities", () => { const fingerprint = `sha256:${"f".repeat(64)}`; const assetPaths = [ "assets/scum-live/login-parser.json", "assets/scum-live/queries/players-read.json", "assets/scum-live/rcon/gift-grant.json", "assets/scum-live/mutations/profile-xml-patch.json", "assets/scum-live/map/island.png", "assets/scum-live/map/transform.json", "assets/scum-live/gifts/catalog.json" ]; const errors = validateTemporaryScumCompanionManifest((manifest, fixtureDir) => { manifest.assetFiles = [...manifest.assetFiles, ...assetPaths.map((assetPath) => ({ path: assetPath, mode: 384 }))]; for (const assetPath of assetPaths) writeFixtureJSON(fixtureDir, assetPath, { packaged: true }); const digest = (assetPath: string) => sha256FixtureDigest(fixtureDir, assetPath); manifest.scumLiveData = { ...manifest.scumLiveData, logParsers: [{ key: "login-parser", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[0], digest: digest(assetPaths[0]), parserVersion: "login-v1", sourceKey: "scum-login-events", eventType: "scum.login", eventSchemaRef: "schemas/scum-live/login-event.schema.json", maxLineBytes: 4096, cursorPolicy: "source-generation-sequence", privacy: { stripNetworkIdentifiers: true, logicalEventIdentity: "native-or-sanitized-fields" } }], sqliteQueries: [{ key: "players-read", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[1], digest: digest(assetPaths[1]), capability: "players.read", requiredSchemaFingerprint: fingerprint, transportKey: "scum-database", targetKey: "scum-database", parameterSchemaRef: "schemas/scum-live/players-read.parameters.schema.json", resultSchemaRef: "schemas/scum-live/players-read.result.schema.json", maxRows: 100, timeoutMs: 5000, maxResultBytes: 65536 }], syncCadences: [{ capability: "players.read", intervalSeconds: 300, jitterPercent: 20, timeoutMs: 5000, maxConcurrentPerServer: 1 }], typedRconTemplates: [{ key: "gift-grant", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[2], digest: digest(assetPaths[2]), capability: "gift-command.write", requiredSchemaFingerprint: fingerprint, transportKey: "scum-management", targetKey: "scum-management", permission: "server.game-client.command", payloadSchemaRef: "schemas/scum-live/gift-grant.payload.schema.json", resultSchemaRef: "schemas/scum-live/gift-grant.result.schema.json", confirmationSchemaRef: "schemas/scum-live/gift-grant.confirmation.schema.json", timeoutMs: 5000, maxPayloadBytes: 4096 }], guardedMutations: [{ key: "profile-xml-patch", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[3], digest: digest(assetPaths[3]), capability: "profile-xml.write", requiredSchemaFingerprint: fingerprint, transportKey: "scum-database", targetKey: "scum-database", permission: "server.game-client.maintenance", payloadSchemaRef: "schemas/scum-live/profile-xml-patch.payload.schema.json", resultSchemaRef: "schemas/scum-live/profile-xml-patch.result.schema.json", confirmationSchemaRef: "schemas/scum-live/profile-xml-patch.confirmation.schema.json", timeoutMs: 10000, maxPayloadBytes: 8192, maxRowsAffected: 1, safety: { requiresExpectedChecksum: true, requiresBackupEvidence: true, requiresOfflineOrMaintenance: true, requiresReadAfterWrite: true } }], mapAssets: [{ key: "island-map", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[4], digest: digest(assetPaths[4]), requiredSchemaFingerprint: fingerprint, metadataSchemaRef: "schemas/scum-live/map-metadata.schema.json", transformAssetPath: assetPaths[5], transformDigest: digest(assetPaths[5]), worldBounds: { minX: -100000, minY: -100000, maxX: 100000, maxY: 100000 }, image: { width: 4096, height: 4096 } }], giftCatalogs: [{ key: "starter-gifts", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[6], digest: digest(assetPaths[6]), catalogVersion: "catalog-v1", itemSchemaRef: "schemas/scum-live/gift-item.schema.json", transportTemplateKeys: ["gift-grant"] }] }; }); expect(errors).toEqual([]); }); it("rejects SCUM live-data asset digests that do not match packaged files", () => { const errors = validateTemporaryScumCompanionManifest((manifest, fixtureDir) => { const assetPath = "assets/scum-live/login-parser.json"; manifest.assetFiles = [...manifest.assetFiles, { path: assetPath, mode: 384 }]; writeFixtureJSON(fixtureDir, assetPath, { packaged: true }); manifest.scumLiveData.logParsers = [{ key: "login-parser", adapterVersion: "scum-live-data-v1", assetPath, digest: `sha256:${"a".repeat(64)}`, parserVersion: "login-v1", sourceKey: "scum-login-events", eventType: "scum.login", eventSchemaRef: "schemas/scum-live/login-event.schema.json", maxLineBytes: 4096, cursorPolicy: "source-generation-sequence", privacy: { stripNetworkIdentifiers: true, logicalEventIdentity: "native-or-sanitized-fields" } }]; }); expect(errors.some((error) => error.includes("digest does not match packaged asset content"))).toBe(true); }); it("rejects unsafe SCUM live-data asset declarations", () => { const errors = validateTemporaryScumCompanionManifest((manifest) => { manifest.scumLiveData.sqliteQueries = [{ key: "players-read", adapterVersion: "scum-live-data-v1", assetPath: "/srv/scum/SCUM.db", digest: "sha256:bad", capability: "players.read", requiredSchemaFingerprint: "sha256:bad", transportKey: "scum-database", targetKey: "scum-database", parameterSchemaRef: "schemas/scum-live/players-read.parameters.schema.json", resultSchemaRef: "schemas/scum-live/players-read.result.schema.json", maxRows: 1000, timeoutMs: 70000, maxResultBytes: 2097152, rawSql: "SELECT * FROM prisoner" }]; manifest.scumLiveData.giftCatalogs = [{ key: "starter-gifts", adapterVersion: "scum-live-data-v1", assetPath: "assets/scum-live/gifts/catalog.json", digest: `sha256:${"2".repeat(64)}`, catalogVersion: "catalog-v1", itemSchemaRef: "schemas/scum-live/gift-item.schema.json", transportTemplateKeys: ["missing-template"] }]; }); expect(errors.some((error) => error.includes("assetPath") && error.includes("contained package-relative path"))).toBe(true); expect(errors.some((error) => error.includes("digest") && error.includes("sha256"))).toBe(true); expect(errors.some((error) => error.includes("must NOT have additional properties") || error.includes("additional properties"))).toBe(true); expect(errors.some((error) => error.includes("transportTemplateKeys") && error.includes("missing-template"))).toBe(true); }); it("declares SCUM install/update and start lifecycle through plugin assets", () => { const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as any; const assetPaths = manifest.assetFiles.map((file: { path: string }) => file.path); const installAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.install), "utf8")); const startAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.start), "utf8")); const installScript = fs.readFileSync(path.join(pluginDir, installAction.executableKey), "utf8"); const startScript = fs.readFileSync(path.join(pluginDir, startAction.executableKey), "utf8"); expect(manifest.runtimeProfiles.serverDeployments).toBeUndefined(); expect(assetPaths).toEqual(expect.arrayContaining(["actions/install.json", "actions/start.json", "bin/scum-install-update.cmd", "bin/scum-start.cmd"])); expect(installAction).toMatchObject({ executableKey: "bin/scum-install-update.cmd", environment: { SERVER_STEAM_APP_ID: "3792580", SERVER_STEAMCMD_UPDATE_ARGS: "+login anonymous +app_update 3792580 +quit" } }); expect(installAction.timeoutMs).toBe(7200000); expect(startAction).toMatchObject({ executableKey: "bin/scum-start.cmd", environment: { SERVER_LOG_FLAG: "-log" } }); expect(installScript).not.toContain("taskkill /IM SCUMServer.exe /F"); expect(installScript).toContain("call :stop_matching_scum"); expect(installScript).toContain("Get-CimInstance Win32_Process"); expect(installScript).toContain("[IO.Path]::GetFullPath($_.ExecutablePath) -ieq $target"); expect(installScript).toContain("SERVER_ROOT_WINDOWS=%SERVER_ROOT:/=\\%"); expect(installScript).toContain("SERVER_INSTALL_DIR=%SERVER_ROOT_WINDOWS%"); expect(installScript).toContain("SERVER_STEAMCMD_DIR=%SERVER_ROOT_DRIVE%\\steamcmd"); expect(installScript).toContain("STEAMCMD_DIR=%SERVER_STEAMCMD_DIR_WINDOWS%"); expect(installScript).toContain("SERVER_INSTALL_DIR_STEAM=%SERVER_INSTALL_DIR_WINDOWS:\\=/%"); expect(installScript).toContain("%SERVER_STEAMCMD_INSTALL_DIR_ARG% \"%SERVER_INSTALL_DIR_STEAM%\" %SERVER_STEAMCMD_UPDATE_ARGS%"); expect(installScript).toContain("SERVER_STEAMCMD_MAX_ATTEMPTS=3"); expect(installScript).toContain("%SERVER_INSTALL_DIR_WINDOWS%\\%SERVER_EXECUTABLE_REF:/=\\%"); expect(installScript).toContain("steamcmd\\steamapps\\common\\SCUM Server"); expect(installScript).toContain(".scum-exe-path"); expect(startScript).toContain(".scum-exe-path"); expect(startScript).toContain("call :install_or_update"); expect(startScript).toContain("scum-install-update.cmd"); expect(startScript).toContain("SCUM executable was not found. Running plugin install/update script."); expect(startScript).not.toContain("SCUM.log"); expect(startScript).toContain("%SERVER_INSTALL_DIR_WINDOWS%\\%SERVER_EXECUTABLE_REF:/=\\%"); expect(startScript).toContain("steamcmd\\steamapps\\common\\SCUM Server"); expect(startScript).toContain("SCUM_WORKDIR"); expect(startScript).toContain("-port=%SERVER_CREATE_GAMEPORT% -MaxPlayers=%SERVER_CREATE_MAXPLAYERS% %SERVER_LOG_FLAG%"); }); it("rejects lifecycle executable assets missing from the manifest seed declaration", () => { const errors = validateTemporaryScumCompanionManifest((manifest) => { manifest.assetFiles = manifest.assetFiles.filter((file: { path: string }) => file.path !== "bin/scum-start.cmd"); }); expect(errors.some((error) => error.includes("lifecycleAction.start.executableKey") && error.includes("manifest.assetFiles"))).toBe(true); }); 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("declares protected database and management transports without direct access", () => { 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[] }>; transportProfiles?: Array<{ key?: string; kind?: string; capabilities?: string[] }>; dataTargets?: Array>; }; }; const local = manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "run-local"); expect(local?.capabilities).not.toContain("remote.run.rcon.command"); expect(local?.transportKeys).not.toContain("rcon"); expect(manifest.runtimeProfiles?.transportProfiles).toEqual(expect.arrayContaining([ expect.objectContaining({ key: "scum-database", kind: "sqlite", capabilities: expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.protected.sql"]) }), expect.objectContaining({ key: "scum-management", kind: "rcon", capabilities: ["remote.run.protected.rcon"] }), expect.objectContaining({ key: "scum-program", kind: "program", capabilities: ["remote.run.program.command"] }) ])); expect(manifest.runtimeProfiles?.dataTargets).toEqual(expect.arrayContaining([ expect.objectContaining({ key: "scum-database", kind: "sqlite.snapshot", workspaceKey: "databases/scum-database", refreshPolicy: "on-demand-snapshot" }) ])); }); 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 { gameClientBridge: { companion?: GameClientBridgeCompanionDeclaration }; }; const companion = manifest.gameClientBridge.companion; expect(companion).toMatchObject({ profileKey: "scum-client-manager", configTemplateKey: "client-config", configFormat: "yaml", platformBaseUrlSource: "run-control", registrationProof: "hmac-sha256", proofMaterialSource: "component-package", proofMaterialEnv: "SCUM_COMPONENT_PROOF", sessionMode: "component-session", tlsPolicy: "verify-system-roots", heartbeatIntervalSeconds: 30, requestTimeoutSeconds: 15 }); const schema = JSON.parse(fs.readFileSync(path.join(pluginDir, companion!.configSchemaRef), "utf8")); const example = JSON.parse(fs.readFileSync(path.join(pluginDir, "schemas/companion/config.generated.example.json"), "utf8")); const validate = new Ajv2020({ strict: false, validateFormats: false }).compile(schema); expect(validate(example), JSON.stringify(validate.errors)).toBe(true); expect(JSON.stringify(example)).not.toMatch(/authKey|componentKey|credential|password|sessionToken|secret|\/api\/v1\/scum-clients\//i); expect(example).toMatchObject({ proof: { materialEnv: "SCUM_COMPONENT_PROOF" }, session: { mode: "component-session" }, tls: { policy: "verify-system-roots" } }); }); it("rejects unsafe SCUM companion bootstrap policy and inline session material", () => { const policyErrors = validateTemporaryScumCompanionManifest((manifest) => { manifest.gameClientBridge.companion.tlsPolicy = "skip-verification"; }); expect(policyErrors.some((error) => error.includes("tlsPolicy") || error.includes("secure component registration/session/TLS policy"))).toBe(true); const materialErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => { const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json"); const example = JSON.parse(fs.readFileSync(examplePath, "utf8")); example.proof.sessionToken = "inline-session-material"; writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example); }); expect(materialErrors.some((error) => error.includes("inline proof/session material") || error.includes("additional properties"))).toBe(true); const environmentErrors = validateTemporaryScumCompanionManifest((manifest) => { manifest.gameClientBridge.companion.proofMaterialEnv = "LD_PRELOAD"; }); expect(environmentErrors.some((error) => error.includes("proofMaterialEnv"))).toBe(true); }); it("rejects generated SCUM companion configs with incomplete capabilities or unsafe Platform URLs", () => { const capabilityErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => { const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json"); const example = JSON.parse(fs.readFileSync(examplePath, "utf8")); example.capabilities = ["component.register", "component.heartbeat", "component.health", "game-client.bridge"]; writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example); }); expect(capabilityErrors.some((error) => error.includes("capabilities"))).toBe(true); for (const unsafeURL of ["https://user:raw-token@example.test?session=raw-token#fragment", "https://?missing-host"]) { const urlErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => { const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json"); const example = JSON.parse(fs.readFileSync(examplePath, "utf8")); example.platform.baseUrl = unsafeURL; writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example); }); expect(urlErrors.some((error) => error.includes("platform.baseUrl"))).toBe(true); } }); it("cross-validates generated SCUM companion config against its declaration and runtime profile", () => { const proofErrors = validateTemporaryScumCompanionManifest((manifest) => { manifest.gameClientBridge.companion.proofMaterialEnv = "OTHER_COMPONENT_PROOF"; }); expect(proofErrors.some((error) => error.includes("proof.materialEnv") && error.includes("proofMaterialEnv"))).toBe(true); const profileErrors = validateTemporaryScumCompanionManifest((manifest) => { const manager = manifest.runtimeProfiles.clientManagers.find((candidate: Record) => candidate.key === "scum-client-manager"); manager.health.requiredCapabilities = manager.health.requiredCapabilities.filter((capability: string) => capability !== "logs.stream"); }); expect(profileErrors.some((error) => error.includes("capabilities") && error.includes("requiredCapabilities"))).toBe(true); const sessionErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => { const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json"); const example = JSON.parse(fs.readFileSync(examplePath, "utf8")); example.session.mode = "legacy-shared-token"; writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example); }); expect(sessionErrors.some((error) => error.includes("session"))).toBe(true); }); it("rejects unsafe optional fields declared only by the SCUM companion config schema", () => { const schemaErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => { const schemaPath = path.join(fixtureDir, "schemas/companion/config.schema.json"); const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8")); schema.properties.hostPath = { type: "string", minLength: 1, maxLength: 200 }; writeFixtureJSON(fixtureDir, "schemas/companion/config.schema.json", schema); }); expect(schemaErrors.some((error) => error.includes("hostPath") && error.includes("raw host path"))).toBe(true); }); it("rejects legacy companion endpoints, insecure TLS flags, and credential keys", () => { const cases = [ { name: "legacy shared-token endpoint", mutate(schema: Record, _example: Record): void { schema.description = "legacy /api/v1/scum-clients/commands endpoint"; } }, { name: "insecure TLS flag", mutate(schema: Record, _example: Record): void { schema.description = "InsecureSkipVerify"; } }, { name: "inline credential key", mutate(schema: Record, example: Record): void { schema.properties.credential = { type: "string", minLength: 1, maxLength: 200 }; example.credential = "legacy-shared-value"; } } ]; for (const testCase of cases) { const errors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => { const schemaPath = path.join(fixtureDir, "schemas/companion/config.schema.json"); const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json"); const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8")) as Record; const example = JSON.parse(fs.readFileSync(examplePath, "utf8")) as Record; testCase.mutate(schema, example); writeFixtureJSON(fixtureDir, "schemas/companion/config.schema.json", schema); writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example); }); expect(errors, testCase.name).toContain( "manifest.gameClientBridge.companion.configSchemaRef: companion config must not contain legacy endpoints, insecure TLS, or inline proof/session material" ); } }); it("keeps the production SCUM companion free of unsafe legacy transport and updater patterns", () => { const companionDir = path.join(pluginsRoot, "examples/scum-server-plugin/companion"); const forbiddenPatterns = [ { name: "legacy shared-token endpoint", pattern: /\/api\/v1\/scum-clients\//i }, { name: "legacy shared credential", pattern: /\b(?:SCUMClientCredential|scum_client_credential)\b/i }, { name: "disabled TLS verification", pattern: /\bInsecureSkipVerify\s*:\s*true\b/ }, { name: "arbitrary process or shell execution", pattern: /(?:\b(?:os\/exec|exec\.Command(?:Context)?|os\.StartProcess|syscall\.Exec)\b|\b(?:bash|zsh|powershell|pwsh|cmd(?:\.exe)?)\s+-[a-z/])/i }, { name: "direct socket transport", pattern: /(?:\bnet\.(?:Dial|DialTimeout)\s*\(|\b(?:tcp|unix|ws):\/\/)/i }, { name: "arbitrary URL self-update or download", pattern: /(?:\b(?:self_?update|update_?url|download_?url|updater)\b|\bhttp\.(?:Get|DefaultClient\.Get)\s*\(|\b(?:curl|wget)\b)/i } ]; const productionFiles = listProductionGoFiles(companionDir); expect(productionFiles.length).toBeGreaterThan(0); for (const file of productionFiles) { const source = fs.readFileSync(file, "utf8"); const relativeFile = path.relative(companionDir, file); for (const forbidden of forbiddenPatterns) { const match = source.match(forbidden.pattern); expect(match ? `${relativeFile}: ${match[0]}` : null, forbidden.name).toBeNull(); } } }); it("covers the SCUM 4.1 bridge and lifecycle declarations", () => { const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as { permissions: string[]; gameClientBridge: { commands: Array<{ type: string; permission: string; approvalLevel: string; payloadSchemaRef: string; resultSchemaRef?: string; timeoutSeconds: number; maxPayloadBytes: number; }>; snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>; pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[]; operationKeys?: string[] }>; }; pages: Array<{ key: string; permissions?: string[]; bridgeActions?: string[] }>; fileWorkspace?: { defaultDirectoryKey: string; directories: Array<{ key: string; label: string; scope: string }>; files: Array<{ key: string; directoryKey: string; label: string; kind: string; streamKey?: string; editable?: boolean }>; configFields: Array<{ key: string; fileKey: string; configKey: string; label: string }>; }; runtimeProfiles?: { lifecycleProfiles?: Array<{ key: string; capabilities?: string[] }>; logSources?: Array<{ key: string }>; clientManagers?: Array<{ key: string; build?: { workspaceRef?: string; entryRef?: string }; configTemplates?: Array<{ key?: string; templateRef?: string; outputRef?: string }>; deployment?: { arguments?: string[] }; health?: { intervalSeconds?: number; degradedAfterSeconds?: number; offlineAfterSeconds?: number }; }>; }; version: string; }; const installAction = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/actions/install.json"), "utf8")) as { environment?: Record }; const serialized = JSON.stringify(manifest).toLowerCase(); expect(serialized).not.toContain("local-proof"); expect(manifest.version).toBe("0.1.6"); expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server"); expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"])); expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([ "announcement.send", "companion.diagnostics", "player.lookup", "reward.deliver", "event.start", "restart.prepare", "maintenance.prepare" ])); expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"])); expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).toEqual(["players", "squads", "live-map", "gifts"]); expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).not.toContain("files-config"); expect(manifest.gameClientBridge.pages.every((page) => !(page.queryTemplateKeys?.length) && !(page.operationKeys?.length))).toBe(true); expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows")).toBeUndefined(); expect(manifest.pages.map((page) => page.key)).toEqual(["players", "squads", "live-map", "gifts"]); expect(manifest.pages.map((page) => page.key)).not.toContain("files-config"); expect(manifest.pages.every((page) => !(page.bridgeActions ?? []).includes("remote.access.request"))).toBe(true); expect(manifest.pages.find((page) => page.key === "players")?.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"])); expect(manifest.fileWorkspace).toBeUndefined(); expect(manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "scum-client")?.capabilities).not.toContain("remote.run.rcon.command"); expect(manifest.runtimeProfiles?.logSources?.map((source) => source.key)).toEqual(expect.arrayContaining(["scum-chat-events", "scum-server-events", "scum-login-events", "scum-client-events"])); }); it("declares bounded and permissioned SCUM bridge commands", () => { const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; permission: string; approvalLevel: string; payloadSchemaRef: string; resultSchemaRef?: string; timeoutSeconds: number; maxPayloadBytes: number; }>; }; }; const expected = { "announcement.send": { permission: "server.game-client.command", approvalLevel: "operator" }, "companion.diagnostics": { permission: "server.game-client.read", approvalLevel: "none" }, "player.lookup": { permission: "server.game-client.read", approvalLevel: "none" }, "reward.deliver": { permission: "server.game-client.command", approvalLevel: "operator" }, "event.start": { permission: "server.game-client.command", approvalLevel: "operator" }, "restart.prepare": { permission: "server.game-client.maintenance", approvalLevel: "operator" }, "maintenance.prepare": { permission: "server.game-client.maintenance", approvalLevel: "platform-admin" } } as const; expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining(Object.keys(expected))); for (const command of manifest.gameClientBridge.commands) { const policy = expected[command.type as keyof typeof expected]; if (!policy) { continue; } expect(command.permission).toBe(policy.permission); expect(command.approvalLevel).toBe(policy.approvalLevel); expect(command.timeoutSeconds).toBeGreaterThan(0); expect(command.timeoutSeconds).toBeLessThanOrEqual(3600); expect(command.maxPayloadBytes).toBeGreaterThan(0); expect(command.maxPayloadBytes).toBeLessThanOrEqual(65536); expect(command.resultSchemaRef).toBeTruthy(); } const schemaRefs = manifest.gameClientBridge.commands.flatMap((command) => [command.payloadSchemaRef, command.resultSchemaRef].filter((ref): ref is string => Boolean(ref))); for (const schemaRef of schemaRefs) { const schema = JSON.parse(fs.readFileSync(path.join(pluginDir, schemaRef), "utf8")) as Record; 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; 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); visit(schema.properties); } }); it("declares bounded SCUM snapshot schemas for local management reads", () => { const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as { gameClientBridge: { snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>; pages: Array<{ pageKey: string; snapshotTypes?: string[] }>; }; }; const expectedTypes = ["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"]; const snapshotsByType = new Map(manifest.gameClientBridge.snapshots.map((snapshot) => [snapshot.type, snapshot])); expect([...snapshotsByType.keys()]).toEqual(expect.arrayContaining(expectedTypes)); for (const type of expectedTypes) { const snapshot = snapshotsByType.get(type); expect(snapshot?.schemaVersion).toBe("1"); expect(snapshot?.schemaRef).toMatch(/^schemas\/bridge\/[a-z-]+\.snapshot\.schema\.json$/); const schema = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin", snapshot!.schemaRef), "utf8")) as { type?: string; additionalProperties?: boolean; properties?: Record; }; expect(schema.type).toBe("object"); expect(schema.additionalProperties).toBe(false); for (const property of Object.values(schema.properties ?? {})) { if (property.type === "array") { expect(property.maxItems).toBeGreaterThan(0); } } } const pageSnapshotTypes = manifest.gameClientBridge.pages.flatMap((page) => page.snapshotTypes ?? []); expect(pageSnapshotTypes).toEqual(expect.arrayContaining(["online.sessions", "players", "squads", "vehicles", "flags"])); expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "players")?.snapshotTypes).toEqual(expect.arrayContaining(["players", "online.sessions"])); expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map")?.snapshotTypes).toEqual(expect.arrayContaining(["players", "vehicles", "flags"])); }); it("rejects the retired SCUM log-projection source in map event schemas", () => { const schemaDir = path.join(pluginsRoot, "examples/scum-server-plugin/schemas/log-events"); for (const filename of ["player-position.event.schema.json", "vehicle-position.event.schema.json", "player-vehicle-enter.event.schema.json", "player-vehicle-leave.event.schema.json"]) { const schema = JSON.parse(fs.readFileSync(path.join(schemaDir, filename), "utf8")) as { properties?: { source?: { const?: string; enum?: string[] } } }; expect(schema.properties?.source).toEqual({ const: "companion" }); expect(schema.properties?.source?.enum).toBeUndefined(); } }); it("keeps SCUM.db query templates absent until current-service evidence exists", () => { const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { permissions: string[]; capabilities: string[]; remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[] }; gameClientBridge: { queryTemplates: Array<{ key: string; title?: string; permission: string; engine: string; transportKey: string; targetKey: string; parameterSchemaRef: string; resultSchemaRef: string; maxRows: number; timeoutSeconds: number; }>; pages: Array<{ pageKey: string; queryTemplateKeys?: string[] }>; }; pages: Array<{ key: string; permissions?: string[]; bridgeActions?: string[] }>; runtimeProfiles?: { transportProfiles?: Array<{ key: string; kind: string; targetKey?: string; capabilities: string[] }> }; }; const expectedKeys: string[] = []; const templatesByKey = new Map(manifest.gameClientBridge.queryTemplates.map((template) => [template.key, template])); expect([...templatesByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys)); expect(manifest.capabilities).toContain("remote.run.db.sqlite.query"); expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.query"); expect(manifest.remoteAccess?.databaseEngines).toContain("sqlite"); const sqliteTransport = manifest.runtimeProfiles?.transportProfiles?.find((profile) => profile.key === "scum-database"); expect(sqliteTransport).toMatchObject({ kind: "sqlite", targetKey: "scum-database" }); expect(sqliteTransport?.capabilities).toEqual(expect.arrayContaining(["remote.run.db.sqlite.query"])); for (const key of expectedKeys) { const template = templatesByKey.get(key)!; expect(template.engine).toBe("sqlite"); expect(template.transportKey).toBe("scum-database"); expect(template.targetKey).toBe("scum-database"); expect(JSON.stringify(template).toLowerCase()).not.toMatch(/select\s|from\s|sqlite:|scum\.db|databasepath|hostpath|dsn/); const parameters = JSON.parse(fs.readFileSync(path.join(pluginDir, template.parameterSchemaRef), "utf8")); const result = JSON.parse(fs.readFileSync(path.join(pluginDir, template.resultSchemaRef), "utf8")); expect(parameters).toMatchObject({ type: "object", additionalProperties: false }); expect(result).toMatchObject({ type: "object", additionalProperties: false, required: ["rows"] }); expect(result.properties.rows.maxItems).toBeLessThanOrEqual(template.maxRows); } const playersPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "players"); const squadsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "squads"); const mapPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map"); expect(playersPage?.queryTemplateKeys).toBeUndefined(); expect(squadsPage?.queryTemplateKeys).toBeUndefined(); expect(mapPage?.queryTemplateKeys).toBeUndefined(); for (const pageKey of ["players", "squads", "live-map"]) { const pluginPage = manifest.pages.find((page) => page.key === pageKey); expect(pluginPage?.permissions).toContain("server.game-client.read"); expect(pluginPage?.bridgeActions).not.toContain("remote.access.request"); } }); it("keeps SCUM RCON operation templates absent until command confirmation is verified", () => { const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { operationTemplates: Array<{ key: string; kind: string; permission: string; approvalLevel: string; payloadSchemaRef: string; resultSchemaRef?: string; confirmationSchemaRef?: string; safety?: Record }>; pages: Array<{ pageKey: string; operationKeys?: string[] }>; }; pages: Array<{ key: string; permissions?: string[] }>; }; const expectedKeys: string[] = []; const operationsByKey = new Map(manifest.gameClientBridge.operationTemplates.map((operation) => [operation.key, operation])); expect([...operationsByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys)); for (const key of expectedKeys) { const operation = operationsByKey.get(key)!; expect(operation.kind).toBe("rcon"); expect(operation.permission).toBe("server.game-client.command"); expect(operation.approvalLevel).toBe("operator"); expect(operation.safety).toMatchObject({ requiresApproval: true, requiresConfirmation: true }); const payload = JSON.parse(fs.readFileSync(path.join(pluginDir, operation.payloadSchemaRef), "utf8")); const result = JSON.parse(fs.readFileSync(path.join(pluginDir, operation.resultSchemaRef!), "utf8")); const confirmation = JSON.parse(fs.readFileSync(path.join(pluginDir, operation.confirmationSchemaRef!), "utf8")); expect(payload).toMatchObject({ type: "object", additionalProperties: false }); expect(result).toMatchObject({ type: "object", additionalProperties: false }); expect(confirmation).toMatchObject({ type: "object", additionalProperties: false }); expect(JSON.stringify(payload).toLowerCase()).not.toMatch(/rcon|commandtext|requesttext|sql|dsn|hostpath/); } const playersPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "players"); const giftsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "gifts"); expect(playersPage?.operationKeys).toBeUndefined(); expect(giftsPage?.operationKeys).toBeUndefined(); expect(manifest.pages.find((page) => page.key === "players")?.permissions).toContain("server.game-client.command"); expect(manifest.pages.find((page) => page.key === "gifts")?.permissions).toContain("server.game-client.command"); }); it("declares typed SCUM semantic log events with bounded schemas", () => { const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { permissions: string[]; runtimeProfiles?: { logSources?: Array<{ key: string; kind?: string; streamKey?: string; retentionDays?: number }>; logEvents?: Array; }; }; const expectedTypes = ["scum.chat", "scum.login", "scum.logout", "scum.kill", "scum.trade", "scum.mine", "scum.unlock", "scum.admin", "scum.performance"]; const logSources = new Map((manifest.runtimeProfiles?.logSources ?? []).map((source) => [source.key, source])); const logEvents = manifest.runtimeProfiles?.logEvents ?? []; expect(logEvents.map((event) => event.eventType)).toEqual(expect.arrayContaining(expectedTypes)); expect(logSources.get("scum-console-stdout")).toMatchObject({ kind: "process.stdout", streamKey: "scum.console.stdout" }); expect(logSources.get("scum-console-stderr")).toMatchObject({ kind: "process.stderr", streamKey: "scum.console.stderr" }); expect(new Set(logEvents.map((event) => event.key)).size).toBe(logEvents.length); expect(new Set(logEvents.map((event) => event.eventType)).size).toBe(logEvents.length); for (const event of logEvents) { const source = logSources.get(event.sourceKey); expect(source).toBeDefined(); expect(manifest.permissions).toContain(event.permission); expect(event.retentionDays).toBeGreaterThanOrEqual(1); expect(event.retentionDays).toBeLessThanOrEqual(source?.retentionDays ?? 365); expect(["info", "notice", "warning", "critical"]).toContain(event.severity); expect(event.schemaRef).toMatch(/^schemas\/log-events\/[a-z-]+\.event\.schema\.json$/); const schema = JSON.parse(fs.readFileSync(path.join(pluginDir, event.schemaRef), "utf8")) as Record; expect((schema.properties as Record>).occurredAt).toMatchObject({ type: "string", format: "date-time" }); 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; 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" && !Object.hasOwn(record, "enum") && !Object.hasOwn(record, "const")) { expect(record.maxLength).toBeGreaterThan(0); } if (record.type === "integer" || record.type === "number") { expect(record.minimum).toBeDefined(); expect(record.maximum).toBeDefined(); } Object.values(record).forEach(visit); }; expect(schema.type).toBe("object"); expect(JSON.stringify(schema).toLowerCase()).not.toMatch(/sqltext|shellcommand|hostpath|rawpath|password|credential|runsocket|directsocket/); visit(schema); } const login = logEvents.find((event) => event.eventType === "scum.login"); const loginSchema = JSON.parse(fs.readFileSync(path.join(pluginDir, login?.schemaRef ?? ""), "utf8")) as { properties?: Record> }; expect(loginSchema.properties?.networkFingerprint).toMatchObject({ type: "string", writeOnly: true }); }); it("rejects unsafe semantic log declarations and missing references", () => { const errors = validateTemporaryLogEventManifest((manifest) => { const event = manifest.runtimeProfiles!.logEvents![0]; event.eventType = "shell.execute"; event.sourceKey = "missing-source"; event.permission = "server.game-client.read"; event.schemaRef = "schemas/log-events/missing.event.schema.json"; event.retentionDays = 366; event.severity = "urgent"; }); expect(errors.some((error) => error.includes("eventType") && error.includes("not allowed"))).toBe(true); expect(errors.some((error) => error.includes("sourceKey") && error.includes("undeclared log source"))).toBe(true); expect(errors.some((error) => error.includes("permission") && error.includes("declared"))).toBe(true); expect(errors.some((error) => error.includes("schemaRef") && error.includes("missing semantic log event schema"))).toBe(true); expect(errors.some((error) => error.includes("retentionDays"))).toBe(true); expect(errors.some((error) => error.includes("severity"))).toBe(true); }); it("rejects unsafe or unbounded semantic log event schemas", () => { const errors = validateTemporaryLogEventManifest((_manifest, fixtureDir) => { writeFixtureJSON(fixtureDir, "schemas/log-events/login.event.schema.json", bridgeObjectSchema({ hostPath: { type: "string" }, details: { type: "string" }, count: { type: "integer" } }, ["hostPath", "details", "count"])); }); expect(errors.some((error) => error.includes("schemaRef") && error.includes("raw host path"))).toBe(true); expect(errors.some((error) => error.includes("maxLength") && error.includes("bounded event strings"))).toBe(true); expect(errors.some((error) => error.includes("bounded event numbers"))).toBe(true); }); it("aligns the SCUM Client Manager declaration with the real Go bootstrap", () => { const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as { runtimeProfiles?: { clientManagers?: Array<{ key: string; build?: { workspaceRef?: string; entryRef?: string }; configTemplates?: Array<{ key?: string; templateRef?: string; outputRef?: string }>; deployment?: { arguments?: string[] }; health?: { intervalSeconds?: number; degradedAfterSeconds?: number; offlineAfterSeconds?: number }; }> }; }; const manager = manifest.runtimeProfiles?.clientManagers?.find((profile) => profile.key === "scum-client-manager"); expect(manager?.build).toMatchObject({ entryRef: "main.go" }); expect(manager?.build).not.toHaveProperty("workspaceRef"); expect(manager?.configTemplates).toEqual([{ key: "client-config", templateRef: "config.yaml.example", outputRef: "config.yaml" }]); expect(manager?.deployment?.arguments).toBeUndefined(); expect(manager?.health).toMatchObject({ intervalSeconds: 30, degradedAfterSeconds: 90, offlineAfterSeconds: 120 }); }); it("accepts the Minecraft server plugin manifest", () => { expect(validateManifestFile("examples/minecraft-server-plugin/manifest.json")).toEqual([]); const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/minecraft-server-plugin/manifest.json"), "utf8")) as { pages: Array<{ key: string; title: string }>; fileWorkspace?: { files?: Array<{ key: string }> } }; expect(manifest.pages.map((page) => `${page.key}:${page.title}`)).toEqual(["files:文件管理", "mods-market:mod/插件市场"]); expect(manifest.fileWorkspace?.files?.map((file) => file.key)).toEqual(expect.arrayContaining(["server-properties", "ops-json", "whitelist-json", "minecraft-latest-log"])); }); it("validates typed game-client bridge catalogs", () => { const schema = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "manifests/game-plugin.manifest.schema.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/dev-game-plugin/manifest.json"), "utf8")); manifest.permissions = [...manifest.permissions, "server.game-client.command", "server.game-client.read"]; manifest.gameClientBridge = { commands: [{ type: "announcement.send", title: "Send announcement", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/announcement.schema.json", resultSchemaRef: "schemas/bridge/announcement-result.schema.json", timeoutSeconds: 60, maxPayloadBytes: 4096 }], snapshots: [{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.schema.json", keepForSeconds: 3600, maxRecords: 100 }], commandRetentionSeconds: 86400, maxCommands: 1000, pages: [] }; const validate = new Ajv2020({ allErrors: true }).compile(schema); expect(validate(manifest), JSON.stringify(validate.errors)).toBe(true); manifest.gameClientBridge.commands[0].approvalLevel = "automatic"; expect(validate(manifest)).toBe(false); }); it("loads and validates every schema referenced by a safe game-client bridge manifest", () => { expect(validateTemporaryBridgeManifest()).toEqual([]); }); it.each(["sql.execute", "sqlExecute", "database.execute", "database.query"])("rejects arbitrary SQL command type %s independently", (commandType) => { const errors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.commands[0].type = commandType; }); expect(errors.some((error) => error.includes("commands[0].type") && error.includes("arbitrary SQL"))).toBe(true); }); it.each(["shell.execute", "powershell.execute", "script.run", "terminal.execute", "command.run"])("rejects arbitrary shell command type %s independently", (commandType) => { const errors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.commands[0].type = commandType; }); expect(errors.some((error) => error.includes("commands[0].type") && error.includes("arbitrary shell"))).toBe(true); }); it("rejects raw bridge schema paths end to end", () => { const errors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.commands[0].payloadSchemaRef = "/etc/scum-query.json"; }); expect(errors.some((error) => error.includes("payloadSchemaRef") && error.includes("raw host paths"))).toBe(true); }); it("rejects undeclared page command references end to end", () => { const errors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.pages[0].commandTypes = ["undeclared.command"]; }); expect(errors.some((error) => error.includes("undeclared command undeclared.command"))).toBe(true); }); it("rejects duplicate and undeclared page query template keys", () => { const duplicateErrors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.queryTemplates?.push({ ...manifest.gameClientBridge.queryTemplates[0] }); }); expect(duplicateErrors.some((error) => error.includes("duplicate query template player.by-id"))).toBe(true); const pageErrors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.pages[0].queryTemplateKeys = ["undeclared.query"]; }); expect(pageErrors.some((error) => error.includes("undeclared query template undeclared.query"))).toBe(true); }); it("rejects unsafe or unbounded query template declarations", () => { const errors = validateTemporaryBridgeManifest((manifest) => { Object.assign(manifest.gameClientBridge.queryTemplates?.[0] ?? {}, { key: "../raw-query", engine: "mysql", permission: "server.not-declared", parameterSchemaRef: "/etc/query.json", maxRows: 501, timeoutSeconds: 61 }); }); expect(errors.some((error) => error.includes("queryTemplates/0/key") || error.includes("queryTemplates[0].key"))).toBe(true); expect(errors.some((error) => error.includes("engine") && error.includes("sqlite"))).toBe(true); expect(errors.some((error) => error.includes("permission") && error.includes("declared"))).toBe(true); expect(errors.some((error) => error.includes("parameterSchemaRef") && error.includes("raw host paths"))).toBe(true); expect(errors.some((error) => error.includes("maxRows"))).toBe(true); expect(errors.some((error) => error.includes("timeoutSeconds"))).toBe(true); }); it("requires query templates to match a declared sqlite transport target and capability", () => { const targetErrors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.queryTemplates![0].targetKey = "db/other"; }); expect(targetErrors.some((error) => error.includes("targetKey") && error.includes("sqlite transport target"))).toBe(true); const capabilityErrors = validateTemporaryBridgeManifest((manifest) => { manifest.runtimeProfiles!.transportProfiles![0].capabilities = ["remote.run.files.read"]; }); expect(capabilityErrors.some((error) => error.includes("transportKey") && error.includes("remote.run.db.sqlite.query"))).toBe(true); }); it("requires query template pages to declare template permission and remote access", () => { const permissionErrors = validateTemporaryBridgeManifest((manifest) => { const overviewPage = manifest.pages?.find((page) => page.key === "overview"); overviewPage!.permissions = overviewPage!.permissions?.filter((permission) => permission !== "server.game-client.read"); }); expect(permissionErrors.some((error) => error.includes("page must declare query template permission"))).toBe(true); const actionErrors = validateTemporaryBridgeManifest((manifest) => { const overviewPage = manifest.pages?.find((page) => page.key === "overview"); overviewPage!.bridgeActions = overviewPage!.bridgeActions?.filter((action) => action !== "remote.access.request"); }); expect(actionErrors.some((error) => error.includes("page must declare remote.access.request"))).toBe(true); }); it("validates typed operation templates and page operation bindings", () => { expect(validateTemporaryBridgeManifest()).toEqual([]); const unsafeKeyErrors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.operationTemplates![0].key = "raw.sql.execute"; }); expect(unsafeKeyErrors.some((error) => error.includes("operationTemplates") && error.includes("arbitrary SQL"))).toBe(true); const approvalErrors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.operationTemplates![0].approvalLevel = "none"; }); expect(approvalErrors.some((error) => error.includes("approvalLevel") && error.includes("operator"))).toBe(true); const rconTransportErrors = validateTemporaryBridgeManifest((manifest) => { Object.assign(manifest.gameClientBridge.operationTemplates![0], { transportKey: "sqlite-db", targetKey: "db/sqlite" }); }); expect(rconTransportErrors.some((error) => error.includes("rcon operations require"))).toBe(true); const mutationSafetyErrors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.operationTemplates![1].safety = { requiresConfirmation: true }; }); expect(mutationSafetyErrors.some((error) => error.includes("sqlite-mutation operations require before value"))).toBe(true); const pageErrors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.pages[0].operationKeys = ["missing.operation"]; }); expect(pageErrors.some((error) => error.includes("undeclared operation template missing.operation"))).toBe(true); }); it.each(["sqlText", "dsn", "hostPath", "shellCommand", "socketAddress", "accessToken", "credential"])("rejects unsafe query parameter schema field %s", (fieldName) => { const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => { writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.parameters.schema.json", bridgeObjectSchema({ [fieldName]: { type: "string", minLength: 1, maxLength: 120 } }, [fieldName])); }); expect(errors.some((error) => error.includes("queryTemplates[0].parameterSchemaRef") && error.includes("not allowed"))).toBe(true); }); it("rejects SQL text embedded in a query result schema", () => { const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => { writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.result.schema.json", bridgeObjectSchema({ summary: { type: "string", minLength: 1, maxLength: 200, const: "SELECT id FROM players" } }, ["summary"])); }); expect(errors.some((error) => error.includes("queryTemplates[0].resultSchemaRef") && error.includes("arbitrary SQL content"))).toBe(true); }); it("rejects missing bridge schema files end to end", () => { const errors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.commands[0].resultSchemaRef = "schemas/bridge/missing.schema.json"; }); expect(errors.some((error) => error.includes("resultSchemaRef") && error.includes("missing bridge schema file"))).toBe(true); }); it("rejects missing bridge command approval metadata end to end", () => { const errors = validateTemporaryBridgeManifest((manifest) => { delete manifest.gameClientBridge.commands[0].approvalLevel; }); expect(errors.some((error) => error.includes("approvalLevel") && (error.includes("required") || error.includes("approval metadata")))).toBe(true); }); it("rejects unsafe executor capabilities end to end", () => { const errors = validateTemporaryBridgeManifest((manifest) => { manifest.capabilities = [...manifest.capabilities, "shell.exec"]; }); expect(errors.some((error) => error.includes("capabilities") && error.includes("allowed values"))).toBe(true); }); it("rejects invalid bridge schema JSON without throwing", () => { const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => { fs.writeFileSync(path.join(fixtureDir, "schemas/bridge/announcement.schema.json"), "{ invalid", "utf8"); }); expect(errors.some((error) => error.includes("payloadSchemaRef") && error.includes("not valid JSON"))).toBe(true); }); it("rejects dangerous fields and values in payload, result, and snapshot schemas", () => { const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => { writeFixtureJSON(fixtureDir, "schemas/bridge/announcement.schema.json", bridgeObjectSchema({ sqlText: { type: "string" } }, ["sqlText"])); writeFixtureJSON(fixtureDir, "schemas/bridge/announcement-result.schema.json", bridgeObjectSchema({ shellCommand: { type: "string", const: "bash -c whoami" } }, ["shellCommand"])); writeFixtureJSON(fixtureDir, "schemas/bridge/players.schema.json", bridgeObjectSchema({ hostPath: { type: "string" }, mode: { type: "string", const: "run.socket" }, runCapability: { type: "string" } }, ["hostPath", "mode", "runCapability"])); }); expect(errors.some((error) => error.includes("payloadSchemaRef") && error.includes("arbitrary SQL field"))).toBe(true); expect(errors.some((error) => error.includes("resultSchemaRef") && error.includes("arbitrary shell"))).toBe(true); expect(errors.some((error) => error.includes("snapshots[0].schemaRef") && error.includes("raw host path"))).toBe(true); expect(errors.some((error) => error.includes("snapshots[0].schemaRef") && error.includes("unsafe executor capability"))).toBe(true); }); it("requires bounded object schemas for every bridge reference", () => { const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => { writeFixtureJSON(fixtureDir, "schemas/bridge/announcement.schema.json", { type: "object", properties: { message: { type: "string" } } }); }); expect(errors.some((error) => error.includes("payloadSchemaRef") && error.includes("additionalProperties to false"))).toBe(true); }); it("rejects a manifest with an invalid create form schema", () => { const errors = validateManifestFile("tests/fixtures/invalid-create-form-manifest.json"); expect(errors.some((error) => error.includes("createForm") && error.includes("label"))).toBe(true); }); it("rejects unsafe direct run and raw AI key requests", () => { const errors = validateManifestFile("tests/fixtures/unsafe-manifest.json"); expect(errors.some((error) => error.includes("direct run access"))).toBe(true); expect(errors.some((error) => error.includes("raw credential or AI/provider key"))).toBe(true); }); it("rejects unsafe runtime profile values", () => { const errors = validateManifestFile("tests/fixtures/unsafe-runtime-profile-manifest.json"); expect(errors.some((error) => error.includes("raw credential or AI/provider key"))).toBe(true); expect(errors.some((error) => error.includes("raw host path"))).toBe(true); expect(errors.some((error) => error.includes("arbitrary shell"))).toBe(true); expect(errors.some((error) => error.includes("not approved for dependency download"))).toBe(true); expect(errors.some((error) => error.includes("client-manager.deploy is required"))).toBe(true); expect(errors.some((error) => error.includes("offline threshold"))).toBe(true); expect(errors.some((error) => error.includes("profile version is below minimumVersion"))).toBe(true); }); it("validates typed lifecycle declarations and rejects shell/path escapes", () => { const declaration: PluginLifecycleActionDeclaration = { version: 1, action: "start", mode: "supervised", executableKey: "bin/game-server", arguments: ["--foreground"] }; expect(declaration.action).toBe("start"); const errors = validateLifecycleActionFile("tests/fixtures/unsafe-lifecycle-action.json", "start"); expect(errors.some((error) => error.includes("pattern") || error.includes("arbitrary shell"))).toBe(true); expect(errors.some((error) => error.includes("raw credential"))).toBe(true); }); }); describe("plugin SDK", () => { it("types generic runtime semantic log event declarations", () => { const declaration: RuntimeLogEventDeclaration = { key: "scum-performance", title: "SCUM server performance", sourceKey: "scum-performance-events", eventType: "scum.performance", permission: "server.logs.read", schemaRef: "schemas/log-events/performance.event.schema.json", retentionDays: 30, severity: "info" }; expect(declaration).toMatchObject({ eventType: "scum.performance", permission: "server.logs.read", severity: "info" }); }); it("types read-only SQLite query template declarations", () => { const declaration: GameClientBridgeQueryTemplateDeclaration = { key: "scum.player.by-id", title: "Find SCUM player by ID", permission: "server.game-client.read", engine: "sqlite", transportKey: "sqlite-db", targetKey: "db/sqlite", parameterSchemaRef: "schemas/bridge/queries/player-by-id.parameters.schema.json", resultSchemaRef: "schemas/bridge/queries/player-by-id.result.schema.json", maxRows: 1, timeoutSeconds: 10 }; expect(declaration).toMatchObject({ engine: "sqlite", transportKey: "sqlite-db", targetKey: "db/sqlite", maxRows: 1 }); expect(JSON.stringify(declaration).toLowerCase()).not.toMatch(/sqltext|dsn|hostpath|socket|credential/); }); it("types controlled operation template declarations", () => { const declaration: GameClientBridgeOperationTemplateDeclaration = { key: "player.attribute.855.set", title: "Set player attribute 855", permission: "server.game-client.maintenance", approvalLevel: "platform-admin", kind: "sqlite-mutation", transportKey: "scum-mutation-db", targetKey: "scum-mutation-db", payloadSchemaRef: "schemas/bridge/operations/player-attribute-855-set.payload.schema.json", resultSchemaRef: "schemas/bridge/operations/player-attribute-855-set.result.schema.json", confirmationSchemaRef: "schemas/bridge/operations/player-attribute-855-set.confirmation.schema.json", timeoutSeconds: 120, maxPayloadBytes: 4096, maxRowsAffected: 1, mutation: { fieldKey: "855", tableKey: "prisoner", identityKey: "user_profile_id", valueKey: "value", confirmationQueryKey: "player.lookup", allowedValueType: "integer", minValue: 0, maxValue: 100000 }, safety: { requiresApproval: true, requiresOfflinePlayer: true, requiresBeforeValue: true, requiresConfirmation: true, backupRequired: true } }; expect(declaration).toMatchObject({ kind: "sqlite-mutation", approvalLevel: "platform-admin", maxRowsAffected: 1 }); expect(JSON.stringify(declaration).toLowerCase()).not.toMatch(/sqltext|dsn|hostpath|socket|credential|password/); }); it("builds safe game-client bridge requests without component transport material", () => { const request = createGameClientBridgeQueueRequest({ profileKey: "scum-client", commandType: "announcement.send", payload: { message: "hello" }, idempotencyKey: "announcement-1", expiresAt: "2026-07-20T12:00:00Z" }); expect(request.commandType).toBe("announcement.send"); expect(request).not.toHaveProperty("sessionToken"); expect(request).not.toHaveProperty("componentKey"); expect(request).not.toHaveProperty("runEndpoint"); expect(request).not.toHaveProperty("hostPath"); expect(request).not.toHaveProperty("dsn"); }); it("types protected request declarations while retaining text redaction boundaries", () => { const declaration: GameClientBridgeProtectedRequestDeclaration = { kind: "sql", transportKey: "scum-database", targetKey: "scum-database", textField: "requestText", maxTextBytes: 4096 }; expect(declaration).toMatchObject({ kind: "sql", textField: "requestText" }); expect(JSON.stringify(declaration).toLowerCase()).not.toMatch(/dsn|hostpath|socket|credential|password/); const errors = validateTemporaryBridgeManifest((manifest) => { manifest.gameClientBridge.commands[0].type = "database.request"; manifest.gameClientBridge.commands[0].protectedRequest = { kind: "sql", transportKey: "missing", targetKey: "missing", textField: "requestText", maxTextBytes: 512 }; }); expect(errors.some((error) => error.includes("protectedRequest.transportKey"))).toBe(true); }); it("checks declared bridge permissions", () => { const context: PluginBridgeContext = { pluginId: "game.example", routeKey: "logs", permissions: ["server.read", "server.logs.read", "server.artifacts.read"] }; expect(hasPluginPermission(context, "server.logs.read")).toBe(true); expect(hasPluginPermission(context, "server.artifacts.read")).toBe(true); expect(hasPluginPermission(context, "ai.invoke")).toBe(false); }); it("builds typed bridge request envelopes without owning transport", () => { const context: PluginBridgeContext = { pluginId: "game.example", routeKey: "logs", serverInstanceId: "server-1", permissions: ["server.read", "server.logs.read"] }; const request = createBridgeRequest({ id: "request-1", context, action: "logs.query", payload: { streamKey: "stdout", limit: 100 } }); expect(request).toEqual({ id: "request-1", pluginId: "game.example", routeKey: "logs", serverInstanceId: "server-1", action: "logs.query", payload: { streamKey: "stdout", limit: 100 } }); }); it("checks bridge action permissions and AI purposes locally", () => { const context: PluginBridgeContext = { pluginId: "game.example", routeKey: "logs", permissions: ["server.read", "server.logs.read", "ai.invoke"], aiPurposes: ["logs.diagnose"] }; expect(canRequestBridgeAction(context, "logs.query")).toBe(true); expect(canRequestBridgeAction(context, "files.request")).toBe(false); expect(canRequestBridgeAction(context, "ai.invoke", { aiPurpose: "logs.diagnose" })).toBe(true); expect(canRequestBridgeAction(context, "ai.invoke", { aiPurpose: "config.suggest" })).toBe(false); }); it("returns safe bridge errors without credential or transport fields", () => { const error = bridgeError("missing_permission", "Permission is required", ["server.files.read"]); expect(error).toEqual({ code: "missing_permission", message: "Permission is required", details: ["server.files.read"] }); expect(error).not.toHaveProperty("apiKey"); expect(error).not.toHaveProperty("runSocket"); expect(error).not.toHaveProperty("hostPath"); }); it("builds and parses execution envelopes without owning transport", () => { const context: PluginBridgeContext = { pluginId: "game.example", routeKey: "logs", serverInstanceId: "server-1", permissions: ["server.read", "server.logs.read"] }; const request = createBridgeExecutionRequest({ requestId: "exec-1", context, action: "logs.query", payload: { logStreamId: "log-1", limit: "100" } }); expect(request).toEqual({ requestId: "exec-1", pluginId: "game.example", routeKey: "logs", serverInstanceId: "server-1", action: "logs.query", aiPurpose: undefined, payload: { logStreamId: "log-1", limit: "100" } }); expect(request).not.toHaveProperty("fetch"); expect(request).not.toHaveProperty("authorization"); const parsed = parseBridgeExecutionResponse({ requestId: "exec-1", pluginId: "game.example", routeKey: "logs", serverInstanceId: "server-1", action: "logs.query", status: "ok", result: { entryCount: "0" } }); expect(parsed).toMatchObject({ status: "ok", result: { entryCount: "0" } }); expect(parsed).not.toHaveProperty("apiKey"); expect(parsed).not.toHaveProperty("runSocket"); }); it("builds mediated AI requests and parses redacted responses", () => { const context: PluginBridgeContext = { pluginId: "game.example", routeKey: "logs", serverInstanceId: "server-1", permissions: ["ai.invoke"], aiPurposes: ["logs.diagnose"] }; const request = createAIInvocationRequest({ requestId: "ai-1", context, purpose: "logs.diagnose", prompt: "Summarize warnings", contextRefs: { server: "server://server-1" } }); expect(request).toMatchObject({ pluginId: "game.example", purpose: "logs.diagnose", prompt: "Summarize warnings" }); expect(request).not.toHaveProperty("apiKeyRef"); expect(request).not.toHaveProperty("providerBaseUrl"); const parsed = parseAIInvocationResponse({ requestId: "ai-1", purpose: "logs.diagnose", status: "ok", recommendation: "Review the warning trend.", usage: { model: "mock", mocked: true, inputTokens: 5, outputTokens: 6 } }); expect(parsed).toMatchObject({ status: "ok", usage: { mocked: true } }); expect(JSON.stringify(parsed)).not.toContain("sk-"); expect(JSON.stringify(parsed)).not.toContain("apiKeyRef"); }); it("builds artifact open envelopes and parses safe platform references", () => { const context: PluginBridgeContext = { pluginId: "game.example", routeKey: "logs", serverInstanceId: "server-1", permissions: ["server.artifacts.read"] }; expect(canRequestBridgeAction(context, "artifacts.open")).toBe(true); expect( createArtifactOpenRequest({ requestId: "artifact-open-1", context, artifactId: "artifact-1" }) ).toEqual({ requestId: "artifact-open-1", pluginId: "game.example", routeKey: "logs", serverInstanceId: "server-1", action: "artifacts.open", aiPurpose: undefined, payload: { artifactId: "artifact-1" } }); const reference = parseArtifactReference({ artifactId: "artifact-1", filename: "artifact-1.bin", contentType: "application/octet-stream", sizeBytes: "64", checksum: "sha256:abc", downloadUrl: "/api/v1/artifacts/artifact-1/content", expiresAt: "2026-07-03T00:15:00Z", rangeSupported: "true", chunkSizeBytes: "1048576", storageBehavior: "platform-memory-transfer-session" }); expect(reference).toMatchObject({ artifactId: "artifact-1", rangeSupported: true }); expect(JSON.stringify(reference)).not.toContain("/Users/"); expect(JSON.stringify(reference)).not.toContain("storage://"); expect(JSON.stringify(reference)).not.toContain("Bearer "); expect( parseArtifactReference({ artifactId: "artifact-1", filename: "artifact.bin", contentType: "application/octet-stream", sizeBytes: "64", checksum: "sha256:abc", downloadUrl: "storage://bucket/artifact-1", expiresAt: "2026-07-03T00:15:00Z", rangeSupported: "true", chunkSizeBytes: "1048576" }) ).toBeUndefined(); }); it("builds lifecycle dispatch envelopes without direct run transport", () => { const context: PluginBridgeContext = { pluginId: "game.example", routeKey: "overview", serverInstanceId: "server-1", permissions: ["server.lifecycle"] }; expect(canRequestBridgeAction(context, "jobs.dispatch")).toBe(true); const request = createLifecycleDispatchRequest({ requestId: "lifecycle-start-1", context, action: "start", expectedConfigVersion: 2, idempotencyKey: "idem-lifecycle-start" }); expect(request).toEqual({ requestId: "lifecycle-start-1", pluginId: "game.example", routeKey: "overview", serverInstanceId: "server-1", action: "jobs.dispatch", aiPurpose: undefined, payload: { lifecycleAction: "start", capability: "process.start", expectedConfigVersion: "2", idempotencyKey: "idem-lifecycle-start" } }); expect(JSON.stringify(request)).not.toContain("http://"); expect(JSON.stringify(request)).not.toContain("unix://"); expect(JSON.stringify(request)).not.toContain("/Users/"); expect(JSON.stringify(request)).not.toContain("Bearer "); expect(JSON.stringify(request)).not.toContain("sk-"); }); it("builds remote access request envelopes without direct transport secrets", () => { const context: PluginBridgeContext = { pluginId: "game.minecraft", routeKey: "rcon", serverInstanceId: "server-1", permissions: ["server.remote.access"] }; expect(canRequestBridgeAction(context, "remote.access.request")).toBe(true); const request = createRemoteAccessRequest({ requestId: "remote-rcon-1", context, capability: "remote.run.rcon.command", targetKey: "rcon/command", inputRef: "input://server-1/rcon/command/1", inputs: { playerId: "steam-123", limit: "25" }, idempotencyKey: "idem-remote-rcon" }); expect(request).toEqual({ requestId: "remote-rcon-1", pluginId: "game.minecraft", routeKey: "rcon", serverInstanceId: "server-1", action: "remote.access.request", aiPurpose: undefined, payload: { capability: "remote.run.rcon.command", targetKey: "rcon/command", inputRef: "input://server-1/rcon/command/1", "input.playerId": "steam-123", "input.limit": "25", idempotencyKey: "idem-remote-rcon" } }); expect(JSON.stringify(request)).not.toContain("tcp://"); expect(JSON.stringify(request)).not.toContain("password="); expect(JSON.stringify(request)).not.toContain("/Users/"); }); it("types runtime profile declarations without raw credentials", () => { const clientManager: RuntimeClientManagerProfile = { key: "safe-client-manager", version: "1.2.3", repository: { url: "https://github.com/example/safe-client.git", revisionPolicy: "pinned", revision: "0123456789abcdef" }, supportedTargets: [{ os: "linux", arch: "amd64" }], build: { system: "go", entryRef: "cmd/client/main.go" }, outputArtifacts: ["safe-client"], deployment: { mode: "run-supervised", executableRef: "safe-client", arguments: ["--config", "config.json"], requiredRunCapabilities: ["client-manager.deploy", "client-manager.control", "client-manager.update", "client-manager.rollback", "client-manager.uninstall"] }, lifecycle: { actions: ["start", "stop", "restart", "status", "update", "rollback", "uninstall"], startupTimeoutSeconds: 30, stopTimeoutSeconds: 15 }, health: { mode: "component-heartbeat", intervalSeconds: 15, degradedAfterSeconds: 45, offlineAfterSeconds: 120, requiredCapabilities: ["component.register", "component.heartbeat", "component.health"] }, compatibility: { minimumVersion: "1.0.0", allowDowngrade: false }, updatePolicy: { strategy: "manual-staged", requireApproval: true, healthConfirmationSeconds: 60, retainPrevious: true } }; const manifest: GamePluginManifest = { id: "game.runtime", name: "Runtime Fixture", version: "0.1.0", kind: "game-plugin", server: { type: "runtime", displayName: "Runtime Fixture", createFormSchema: "schemas/create-form.schema.json" }, capabilities: ["process.start", "process.stop", "logs.read"], permissions: ["server.read", "server.lifecycle", "server.logs.read"], productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required", approvalRequired: ["disable", "rollback", "retire"] }, runtimeProfiles: { discovery: [{ key: "java", kind: "command.version", targetKey: "java", required: true }], dependencyProbes: [{ key: "java-21", kind: "java.version", targetKey: "java", minimumVersion: "21" }], logSources: [{ key: "console", kind: "process.stdout", streamKey: "console", cursorKind: "sequence" }], transportProfiles: [{ key: "files", kind: "file", capabilities: ["files.read"] }], clientManagers: [clientManager] } }; expect(manifest.runtimeProfiles?.discovery?.[0].targetKey).toBe("java"); expect(manifest.runtimeProfiles?.clientManagers?.[0].deployment?.requiredRunCapabilities).toContain("client-manager.deploy"); expect(JSON.stringify(manifest)).not.toContain("password="); }); it("builds run distribution, dependency, log backfill, and client-manager envelopes", () => { const context: PluginBridgeContext = { pluginId: "game.scum", routeKey: "remote", serverInstanceId: "server-1", permissions: ["server.run.distribution", "server.dependencies.manage", "server.logs.read", "server.client-manager.manage"] }; expect(canRequestBridgeAction(context, "run.distribution.request")).toBe(true); expect(createRunDistributionRequest({ requestId: "run-gen-1", context, operation: "generate", targetOS: "windows", targetArch: "amd64", idempotencyKey: "idem-run" })).toMatchObject({ action: "run.distribution.request", payload: { operation: "generate", targetOS: "windows", targetArch: "amd64", idempotencyKey: "idem-run" } }); expect(createDependencyActionRequest({ requestId: "dep-1", context, operation: "check", probeKey: "steamcmd", idempotencyKey: "idem-dep" })).toMatchObject({ action: "dependencies.request", payload: { operation: "check", probeKey: "steamcmd" } }); expect(createDependencyActionRequest({ requestId: "dep-2", context, operation: "install", probeKey: "steamcmd", planKey: "install-steamcmd-linux", planDigest: `sha256:${"a".repeat(64)}`, idempotencyKey: "idem-dep-install" })).toMatchObject({ action: "dependencies.request", payload: { operation: "install", probeKey: "steamcmd", planKey: "install-steamcmd-linux", planDigest: `sha256:${"a".repeat(64)}` } }); expect(() => createDependencyActionRequest({ requestId: "dep-unsafe", context, operation: "install", probeKey: "steamcmd", planKey: "install-steamcmd-linux", idempotencyKey: "idem-dep-unsafe" })).toThrow(/reviewed plan SHA-256 digest/); expect(createLogBackfillRequest({ requestId: "logs-1", context, sourceKey: "chat-log", limit: 500, idempotencyKey: "idem-logs" })).toMatchObject({ action: "logs.backfill.request", payload: { sourceKey: "chat-log", limit: "500" } }); expect(createClientManagerRequest({ requestId: "client-1", context, operation: "generate", profileKey: "scum-client-manager", targetOS: "windows", targetArch: "amd64", idempotencyKey: "idem-client" })).toMatchObject({ action: "client-manager.request", payload: { operation: "generate", profileKey: "scum-client-manager" } }); expect(JSON.stringify(createClientManagerRequest({ requestId: "client-2", context, operation: "reset-key", profileKey: "scum-client-manager", idempotencyKey: "idem-reset" }))).not.toContain("secret"); expect(createClientManagerRequest({ requestId: "client-3", context, operation: "deploy", profileKey: "scum-client-manager", installationId: "cm-install-1", artifactId: "artifact-1", expectedDeploymentGeneration: 2, idempotencyKey: "idem-deploy" })).toMatchObject({ action: "client-manager.request", payload: { operation: "deploy", installationId: "cm-install-1", artifactId: "artifact-1", expectedDeploymentGeneration: "2" } }); expect(parseClientManagerLifecycleStatus({ installationId: "cm-install-1", profileKey: "scum-client-manager", status: "online", phase: "healthy", targetOS: "windows", targetArch: "amd64", version: "1.0.0", artifactId: "artifact-1", deploymentGeneration: "2", health: "healthy", actions: "stop,restart,update,uninstall" })).toMatchObject({ installationId: "cm-install-1", deploymentGeneration: 2, actions: ["stop", "restart", "update", "uninstall"] }); expect(parseClientManagerLifecycleStatus({ installationId: "cm-install-1", profileKey: "scum-client-manager", status: "online", deploymentGeneration: "2", actions: "stop", healthReason: "Bearer stolen-session" })).toBeUndefined(); }); it("builds full plugin lifecycle envelopes through Platform only", () => { const context: PluginBridgeContext = { pluginId: "game.scum", routeKey: "overview", serverInstanceId: "server-1", permissions: ["server.lifecycle"] }; const request = createProductionPluginLifecycleRequest({ requestId: "plugin-upgrade-1", context, operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "plugin-upgrade-v1" }); expect(request).toMatchObject({ action: "plugin-lifecycle.request", payload: { operation: "upgrade", targetVersion: "1.2.0", confirmed: "false" } }); expect(JSON.stringify(request)).not.toMatch(/apiKey|providerBaseUrl|runSocket|runEndpoint|hostPath|credential/i); expect(() => createProductionPluginLifecycleRequest({ requestId: "plugin-retire-1", context, operation: "retire", idempotencyKey: "plugin-retire-v1" })).toThrow(/confirmation/); }); });