Align SCUM adapter release gates
This commit is contained in:
@@ -1123,7 +1123,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
if (gate.gate === "enabled") {
|
||||
if (gate.evidenceStatus !== "compatible") errors.push(`${gateLocation}.evidenceStatus: enabled gates require compatible evidence`);
|
||||
if (!/^(sha256:)?[a-fA-F0-9]{16,128}$/.test(gate.requiredSchemaFingerprint ?? "")) errors.push(`${gateLocation}.requiredSchemaFingerprint: enabled gates require a schema fingerprint`);
|
||||
if (!Array.isArray(gate.requiredAssetDigests) || gate.requiredAssetDigests.length === 0) errors.push(`${gateLocation}.requiredAssetDigests: enabled gates require immutable asset digests`);
|
||||
if (capability !== "schema-probe" && (!Array.isArray(gate.requiredAssetDigests) || gate.requiredAssetDigests.length === 0)) errors.push(`${gateLocation}.requiredAssetDigests: enabled gates require immutable asset digests`);
|
||||
}
|
||||
if (gate.gate === "disabled" && gate.evidenceStatus === "compatible") errors.push(`${gateLocation}.evidenceStatus: disabled gates must not claim compatible evidence`);
|
||||
}
|
||||
@@ -1136,6 +1136,21 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
if (!digestPattern.test(asset.digest ?? "")) errors.push(`${assetLocation}.digest: must be a sha256 digest`);
|
||||
if (asset.assetPath && asset.digest) validateSCUMAssetDigest(asset.assetPath, asset.digest, `${assetLocation}.digest`);
|
||||
};
|
||||
const gateByCapability = new Map(gates.map((gate) => [gate.capability ?? "", gate]));
|
||||
const requireCapabilityGateMatch = (capability: string | undefined, adapterVersion: string | undefined, schemaFingerprint: string | undefined, digests: Array<string | undefined>, itemLocation: string): void => {
|
||||
const safeCapability = capability ?? "";
|
||||
const gate = gateByCapability.get(safeCapability);
|
||||
if (!gate) {
|
||||
errors.push(`${itemLocation}: capability ${safeCapability} must have a release gate`);
|
||||
return;
|
||||
}
|
||||
if (gate.adapterVersion !== adapterVersion) errors.push(`${itemLocation}: capability gate adapterVersion must match the asset adapterVersion`);
|
||||
if (schemaFingerprint && gate.requiredSchemaFingerprint !== schemaFingerprint) errors.push(`${itemLocation}: capability gate schema fingerprint must match the asset requirement`);
|
||||
const gateDigests = new Set(gate.requiredAssetDigests ?? []);
|
||||
for (const digest of digests) {
|
||||
if (digestPattern.test(digest ?? "") && !gateDigests.has(digest ?? "")) errors.push(`${itemLocation}: capability gate must include asset digest ${digest}`);
|
||||
}
|
||||
};
|
||||
const validateSCUMAssetDigest = (assetPath: string, expectedDigest: string, digestLocation: string): void => {
|
||||
if (!manifestDir || !isSafeRelativePathRef(assetPath) || !digestPattern.test(expectedDigest)) return;
|
||||
const absoluteAssetPath = path.resolve(manifestDir, assetPath);
|
||||
@@ -1265,6 +1280,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
requireAsset(query, itemLocation);
|
||||
if (!readCapabilities.has(query.capability ?? "")) errors.push(`${itemLocation}.capability: must be a read capability`);
|
||||
if (!fingerprintPattern.test(query.requiredSchemaFingerprint ?? "")) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
|
||||
requireCapabilityGateMatch(query.capability, query.adapterVersion, query.requiredSchemaFingerprint, [query.digest], itemLocation);
|
||||
requireRefs(query as Record<string, unknown>, itemLocation, ["parameterSchemaRef", "resultSchemaRef"]);
|
||||
if (!Number.isInteger(query.maxRows) || (query.maxRows ?? 0) < 1 || (query.maxRows ?? 0) > 500) errors.push(`${itemLocation}.maxRows: must be between 1 and 500`);
|
||||
if (!Number.isInteger(query.timeoutMs) || (query.timeoutMs ?? 0) < 1 || (query.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
|
||||
@@ -1288,6 +1304,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
requireAsset(template, itemLocation);
|
||||
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`);
|
||||
requireCapabilityGateMatch(template.capability, template.adapterVersion, template.requiredSchemaFingerprint, [template.digest], itemLocation);
|
||||
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", "confirmationSchemaRef"]);
|
||||
if (!Number.isInteger(template.timeoutMs) || (template.timeoutMs ?? 0) < 1 || (template.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
|
||||
@@ -1302,6 +1319,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
requireAsset(mutation, itemLocation);
|
||||
if (mutation.capability !== "profile-xml.write") errors.push(`${itemLocation}.capability: must be profile-xml.write`);
|
||||
if (!fingerprintPattern.test(mutation.requiredSchemaFingerprint ?? "")) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
|
||||
requireCapabilityGateMatch(mutation.capability, mutation.adapterVersion, mutation.requiredSchemaFingerprint, [mutation.digest], itemLocation);
|
||||
if (mutation.permission !== "server.game-client.maintenance" || !declaredPermissions.has("server.game-client.maintenance")) errors.push(`${itemLocation}.permission: must require declared server.game-client.maintenance`);
|
||||
requireRefs(mutation as Record<string, unknown>, itemLocation, ["payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef"]);
|
||||
if (!Number.isInteger(mutation.timeoutMs) || (mutation.timeoutMs ?? 0) < 1 || (mutation.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
|
||||
@@ -1317,6 +1335,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
const itemLocation = `${location}.mapAssets[${index}]`;
|
||||
requireAsset(mapAsset, itemLocation);
|
||||
if (!fingerprintPattern.test(mapAsset.requiredSchemaFingerprint ?? "")) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
|
||||
requireCapabilityGateMatch("positions.read", mapAsset.adapterVersion, mapAsset.requiredSchemaFingerprint, [mapAsset.digest, mapAsset.transformDigest], itemLocation);
|
||||
requireRefs(mapAsset as Record<string, unknown>, itemLocation, ["metadataSchemaRef"]);
|
||||
if (!mapAsset.transformAssetPath || !isSafeRelativePathRef(mapAsset.transformAssetPath)) errors.push(`${itemLocation}.transformAssetPath: must be a contained package-relative path`);
|
||||
if (mapAsset.transformAssetPath && !assetFiles.has(mapAsset.transformAssetPath)) errors.push(`${itemLocation}.transformAssetPath: must be declared in manifest.assetFiles`);
|
||||
@@ -1332,6 +1351,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
||||
const itemLocation = `${location}.giftCatalogs[${index}]`;
|
||||
requireAsset(catalog, itemLocation);
|
||||
if (!adapterPattern.test(catalog.catalogVersion ?? "")) errors.push(`${itemLocation}.catalogVersion: must be a bounded catalog version`);
|
||||
requireCapabilityGateMatch("gift-command.write", catalog.adapterVersion, undefined, [catalog.digest], itemLocation);
|
||||
requireRefs(catalog as Record<string, unknown>, itemLocation, ["itemSchemaRef"]);
|
||||
if (!Array.isArray(catalog.transportTemplateKeys) || catalog.transportTemplateKeys.length === 0) errors.push(`${itemLocation}.transportTemplateKeys: must reference at least one typed RCON transport template`);
|
||||
for (const templateKey of catalog.transportTemplateKeys ?? []) {
|
||||
|
||||
Reference in New Issue
Block a user