Gate SCUM typed RCON catalog declarations
This commit is contained in:
@@ -370,7 +370,7 @@
|
||||
"properties": { "capability": { "$ref": "#/$defs/scumReadCapability" }, "intervalSeconds": { "type": "integer", "minimum": 5, "maximum": 86400 }, "jitterPercent": { "type": "integer", "minimum": 0, "maximum": 100 }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxConcurrentPerServer": { "type": "integer", "minimum": 1, "maximum": 16 } }
|
||||
},
|
||||
"scumTypedRconTemplate": {
|
||||
"allOf": [{ "$ref": "#/$defs/scumVersionedAssetBase" }, { "type": "object", "required": ["capability", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "timeoutMs", "maxPayloadBytes"], "additionalProperties": false, "properties": {
|
||||
"allOf": [{ "$ref": "#/$defs/scumVersionedAssetBase" }, { "type": "object", "required": ["capability", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef", "timeoutMs", "maxPayloadBytes"], "additionalProperties": false, "properties": {
|
||||
"key": { "$ref": "#/$defs/logicalKey" }, "adapterVersion": { "$ref": "#/$defs/scumAdapterVersion" }, "assetPath": { "$ref": "#/$defs/relativePathRef" }, "digest": { "$ref": "#/$defs/scumAssetDigest" }, "capability": { "enum": ["economy-command.write", "gift-command.write"] }, "requiredSchemaFingerprint": { "$ref": "#/$defs/scumSchemaFingerprint" }, "transportKey": { "$ref": "#/$defs/logicalKey" }, "targetKey": { "$ref": "#/$defs/logicalKey" }, "permission": { "const": "server.game-client.command" }, "payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "confirmationSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 }
|
||||
} }]
|
||||
},
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"properties": { "capability": { "$ref": "#/$defs/readCapability" }, "intervalSeconds": { "type": "integer", "minimum": 5, "maximum": 86400 }, "jitterPercent": { "type": "integer", "minimum": 0, "maximum": 100 }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxConcurrentPerServer": { "type": "integer", "minimum": 1, "maximum": 16 } }
|
||||
},
|
||||
"typedRconTemplate": {
|
||||
"allOf": [{ "$ref": "#/$defs/assetBase" }, { "type": "object", "required": ["capability", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "timeoutMs", "maxPayloadBytes"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "adapterVersion": { "$ref": "#/$defs/adapterVersion" }, "assetPath": { "$ref": "#/$defs/relativePathRef" }, "digest": { "$ref": "#/$defs/assetDigest" }, "capability": { "enum": ["economy-command.write", "gift-command.write"] }, "requiredSchemaFingerprint": { "$ref": "#/$defs/schemaFingerprint" }, "transportKey": { "$ref": "#/$defs/logicalKey" }, "targetKey": { "$ref": "#/$defs/logicalKey" }, "permission": { "const": "server.game-client.command" }, "payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "confirmationSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 } } }]
|
||||
"allOf": [{ "$ref": "#/$defs/assetBase" }, { "type": "object", "required": ["capability", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef", "timeoutMs", "maxPayloadBytes"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "adapterVersion": { "$ref": "#/$defs/adapterVersion" }, "assetPath": { "$ref": "#/$defs/relativePathRef" }, "digest": { "$ref": "#/$defs/assetDigest" }, "capability": { "enum": ["economy-command.write", "gift-command.write"] }, "requiredSchemaFingerprint": { "$ref": "#/$defs/schemaFingerprint" }, "transportKey": { "$ref": "#/$defs/logicalKey" }, "targetKey": { "$ref": "#/$defs/logicalKey" }, "permission": { "const": "server.game-client.command" }, "payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "confirmationSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 } } }]
|
||||
},
|
||||
"guardedMutation": {
|
||||
"allOf": [{ "$ref": "#/$defs/assetBase" }, { "type": "object", "required": ["capability", "requiredSchemaFingerprint", "transportKey", "targetKey", "permission", "payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef", "timeoutMs", "maxPayloadBytes", "maxRowsAffected", "safety"], "additionalProperties": false, "properties": { "key": { "$ref": "#/$defs/logicalKey" }, "adapterVersion": { "$ref": "#/$defs/adapterVersion" }, "assetPath": { "$ref": "#/$defs/relativePathRef" }, "digest": { "$ref": "#/$defs/assetDigest" }, "capability": { "const": "profile-xml.write" }, "requiredSchemaFingerprint": { "$ref": "#/$defs/schemaFingerprint" }, "transportKey": { "$ref": "#/$defs/logicalKey" }, "targetKey": { "$ref": "#/$defs/logicalKey" }, "permission": { "const": "server.game-client.maintenance" }, "payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "confirmationSchemaRef": { "$ref": "#/$defs/relativeJsonRef" }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 60000 }, "maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 }, "maxRowsAffected": { "const": 1 }, "safety": { "type": "object", "required": ["requiresExpectedChecksum", "requiresBackupEvidence", "requiresOfflineOrMaintenance", "requiresReadAfterWrite"], "additionalProperties": false, "properties": { "requiresExpectedChecksum": { "const": true }, "requiresBackupEvidence": { "const": true }, "requiresOfflineOrMaintenance": { "const": true }, "requiresReadAfterWrite": { "const": true } } } } }]
|
||||
|
||||
@@ -1289,8 +1289,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
if (!["economy-command.write", "gift-command.write"].includes(template.capability ?? "")) errors.push(`${itemLocation}.capability: must be economy-command.write or gift-command.write`);
|
||||
if (template.requiredSchemaFingerprint && !fingerprintPattern.test(template.requiredSchemaFingerprint)) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
|
||||
if (template.permission !== "server.game-client.command" || !declaredPermissions.has("server.game-client.command")) errors.push(`${itemLocation}.permission: must require declared server.game-client.command`);
|
||||
requireRefs(template as Record<string, unknown>, itemLocation, ["payloadSchemaRef", "resultSchemaRef"]);
|
||||
if (template.confirmationSchemaRef && !isSafeRelativeJsonRef(template.confirmationSchemaRef)) errors.push(`${itemLocation}.confirmationSchemaRef: must be a safe relative JSON schema reference`);
|
||||
requireRefs(template as Record<string, unknown>, itemLocation, ["payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef"]);
|
||||
if (!Number.isInteger(template.timeoutMs) || (template.timeoutMs ?? 0) < 1 || (template.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
|
||||
if (!Number.isInteger(template.maxPayloadBytes) || (template.maxPayloadBytes ?? 0) < 1 || (template.maxPayloadBytes ?? 0) > 65536) errors.push(`${itemLocation}.maxPayloadBytes: must be between 1 and 65536`);
|
||||
const rconTransport = transportByKey(template.transportKey);
|
||||
|
||||
@@ -361,7 +361,7 @@ export interface SCUMTypedRCONTemplateDeclaration extends SCUMVersionedAssetDecl
|
||||
permission: Extract<PluginPermission, "server.game-client.command">;
|
||||
payloadSchemaRef: string;
|
||||
resultSchemaRef: string;
|
||||
confirmationSchemaRef?: string;
|
||||
confirmationSchemaRef: string;
|
||||
timeoutMs: number;
|
||||
maxPayloadBytes: number;
|
||||
}
|
||||
|
||||
@@ -305,6 +305,19 @@ describe("plugin manifest validation", () => {
|
||||
expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/);
|
||||
});
|
||||
|
||||
it("omits unverified SCUM typed RCON templates and gift catalogs", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration };
|
||||
const liveData = manifest.scumLiveData;
|
||||
const gates = new Map(liveData.capabilityGates.map((gate) => [gate.capability, gate]));
|
||||
expect(liveData.typedRconTemplates ?? []).toEqual([]);
|
||||
expect(liveData.giftCatalogs ?? []).toEqual([]);
|
||||
expect(gates.get("economy-command.write")).toMatchObject({ gate: "disabled", evidenceStatus: "missing" });
|
||||
expect(gates.get("gift-command.write")).toMatchObject({ gate: "disabled", evidenceStatus: "missing" });
|
||||
expect((manifest.assetFiles ?? []).map((file) => file.path).filter((assetPath) => /assets\/scum-live\/(?:rcon|gifts)\//.test(assetPath))).toEqual([]);
|
||||
expect(JSON.stringify(liveData).toLowerCase()).not.toMatch(/#setcurrency|#setfame|sendchat|commandtext|rawcommand|starter-pack|bandage|water-bottle|improvised-spear/);
|
||||
});
|
||||
|
||||
it("packages SCUM live read query assets with exact bounded result schemas", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration };
|
||||
@@ -531,6 +544,18 @@ describe("plugin manifest validation", () => {
|
||||
expect(errors.some((error) => error.includes("transportTemplateKeys") && error.includes("missing-template"))).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects SCUM typed RCON templates without conclusive confirmation schemas", () => {
|
||||
const errors = validateTemporaryScumCompanionManifest((manifest, fixtureDir) => {
|
||||
const assetPath = "assets/scum-live/rcon/unverified-gift.json";
|
||||
manifest.assetFiles = [...(manifest.assetFiles ?? []), { path: assetPath, mode: 384 }];
|
||||
writeFixtureJSON(fixtureDir, assetPath, { packaged: true });
|
||||
writeFixtureJSON(fixtureDir, "schemas/scum-live/unverified-gift.payload.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"]));
|
||||
writeFixtureJSON(fixtureDir, "schemas/scum-live/unverified-gift.result.schema.json", bridgeObjectSchema({ outcome: { enum: ["succeeded", "failed", "unknown"] } }, ["outcome"]));
|
||||
manifest.scumLiveData.typedRconTemplates = [{ key: "unverified-gift", adapterVersion: "scum-live-data-v1", assetPath, digest: sha256FixtureDigest(fixtureDir, assetPath), capability: "gift-command.write", requiredSchemaFingerprint: currentSCUMSchemaFingerprint, transportKey: "scum-management", targetKey: "scum-management", permission: "server.game-client.command", payloadSchemaRef: "schemas/scum-live/unverified-gift.payload.schema.json", resultSchemaRef: "schemas/scum-live/unverified-gift.result.schema.json", timeoutMs: 5000, maxPayloadBytes: 2048 }];
|
||||
});
|
||||
expect(errors.some((error) => error.includes("confirmationSchemaRef"))).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;
|
||||
|
||||
Reference in New Issue
Block a user