Integrate SCUM real ops workflows

This commit is contained in:
npc0-hue
2026-08-10 21:12:53 +08:00
parent 1063330710
commit a770bc6250
88 changed files with 6375 additions and 2719 deletions
+137 -3
View File
@@ -167,6 +167,12 @@ function unsafeGameClientBridgeCommandTypeReason(value: string): string | undefi
return undefined;
}
function unsafeGameClientBridgePayloadKey(value: string): boolean {
const tokens = identifierTokens(value);
const compact = tokens.join("");
return ["sql", "rawsql", "sqltext", "sqlstatement", "dsn", "hostpath", "socket", "credential", "accesstoken"].includes(compact);
}
function unsafeBridgeSchemaFieldReason(fieldName: string): string | undefined {
const tokens = identifierTokens(fieldName);
const compact = tokens.join("");
@@ -669,7 +675,25 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
maxRows?: number;
timeoutSeconds?: number;
};
type BridgePage = { pageKey?: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] };
type BridgeOperationSafety = { requiresApproval?: boolean; requiresOfflinePlayer?: boolean; requiresMaintenanceWindow?: boolean; requiresBeforeValue?: boolean; requiresConfirmation?: boolean; backupRequired?: boolean };
type BridgeOperationMutation = { fieldKey?: string; tableKey?: string; identityKey?: string; valueKey?: string; confirmationQueryKey?: string; allowedValueType?: string; minValue?: number; maxValue?: number };
type BridgeOperationTemplate = {
key?: string;
permission?: string;
approvalLevel?: string;
kind?: string;
transportKey?: string;
targetKey?: string;
payloadSchemaRef?: string;
resultSchemaRef?: string;
confirmationSchemaRef?: string;
timeoutSeconds?: number;
maxPayloadBytes?: number;
maxRowsAffected?: number;
mutation?: BridgeOperationMutation;
safety?: BridgeOperationSafety;
};
type BridgePage = { pageKey?: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[]; operationKeys?: string[] };
type BridgeCompanion = {
profileKey?: string;
configTemplateKey?: string;
@@ -692,7 +716,7 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[] };
pages?: PluginPage[];
runtimeProfiles?: { transportProfiles?: RuntimeTransportProfile[]; clientManagers?: RuntimeClientManager[] };
gameClientBridge?: { commands?: BridgeCommand[]; snapshots?: Array<{ type?: string }>; queryTemplates?: BridgeQueryTemplate[]; pages?: BridgePage[]; companion?: BridgeCompanion };
gameClientBridge?: { commands?: BridgeCommand[]; snapshots?: Array<{ type?: string }>; queryTemplates?: BridgeQueryTemplate[]; operationTemplates?: BridgeOperationTemplate[]; pages?: BridgePage[]; companion?: BridgeCompanion };
};
const bridge = declaration.gameClientBridge;
if (!bridge) {
@@ -702,6 +726,7 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
const commands = new Set<string>();
const snapshots = new Set((bridge.snapshots ?? []).map((snapshot) => snapshot.type ?? ""));
const queryTemplates = new Map<string, BridgeQueryTemplate>();
const operationTemplates = new Map<string, BridgeOperationTemplate>();
const declaredPermissions = new Set(declaration.permissions ?? []);
const declaredCapabilities = new Set(declaration.capabilities ?? []);
const remoteCapabilities = new Set(declaration.remoteAccess?.runCapabilities ?? []);
@@ -839,6 +864,92 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
errors.push(`${location}: sqlite query templates require the plugin and remote-access sqlite query capability`);
}
}
for (const [index, operationTemplate] of (bridge.operationTemplates ?? []).entries()) {
const location = `manifest.gameClientBridge.operationTemplates[${index}]`;
const key = operationTemplate.key ?? "";
const unsafeReason = unsafeGameClientBridgeCommandTypeReason(key);
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/.test(key) || unsafeReason) {
errors.push(`${location}.key: ${unsafeReason ?? "operation template key is unsafe"}`);
}
if (operationTemplates.has(key)) {
errors.push(`${location}.key: duplicate operation template ${key}`);
}
operationTemplates.set(key, operationTemplate);
if (!operationTemplate.permission || !declaredPermissions.has(operationTemplate.permission)) {
errors.push(`${location}.permission: permission must be declared by the plugin manifest`);
}
if (!new Set(["operator", "platform-admin"]).has(operationTemplate.approvalLevel ?? "")) {
errors.push(`${location}.approvalLevel: must require operator or platform-admin approval`);
}
if (!new Set(["rcon", "sqlite-mutation"]).has(operationTemplate.kind ?? "")) {
errors.push(`${location}.kind: must be rcon or sqlite-mutation`);
}
for (const [field, ref] of [["payloadSchemaRef", operationTemplate.payloadSchemaRef], ["resultSchemaRef", operationTemplate.resultSchemaRef], ["confirmationSchemaRef", operationTemplate.confirmationSchemaRef]] as const) {
if ((field === "payloadSchemaRef" && !ref) || (ref && !isSafeRelativeJsonRef(ref))) {
errors.push(`${location}.${field}: raw host paths and unsafe schema references are not allowed`);
}
}
if (!Number.isInteger(operationTemplate.timeoutSeconds) || (operationTemplate.timeoutSeconds ?? 0) < 1 || (operationTemplate.timeoutSeconds ?? 0) > 3600) {
errors.push(`${location}.timeoutSeconds: must be an integer between 1 and 3600`);
}
if (!Number.isInteger(operationTemplate.maxPayloadBytes) || (operationTemplate.maxPayloadBytes ?? 0) < 1 || (operationTemplate.maxPayloadBytes ?? 0) > 65536) {
errors.push(`${location}.maxPayloadBytes: must be an integer between 1 and 65536`);
}
const transport = transportProfiles.find((profile) => profile.key === operationTemplate.transportKey);
if (!transport) {
errors.push(`${location}.transportKey: undeclared transport profile ${operationTemplate.transportKey ?? ""}`);
continue;
}
if (!operationTemplate.targetKey || transport.targetKey !== operationTemplate.targetKey) {
errors.push(`${location}.targetKey: must match the declared runtime transport target`);
}
if (operationTemplate.kind === "rcon") {
if (transport.kind !== "rcon" || !transport.capabilities?.includes("remote.run.protected.rcon")) {
errors.push(`${location}.transportKey: rcon operations require remote.run.protected.rcon transport`);
}
if (operationTemplate.maxRowsAffected !== undefined) {
errors.push(`${location}.maxRowsAffected: only sqlite-mutation operations may declare affected row bounds`);
}
if (operationTemplate.mutation !== undefined) {
errors.push(`${location}.mutation: only sqlite-mutation operations may declare mutation metadata`);
}
}
if (operationTemplate.kind === "sqlite-mutation") {
if (transport.kind !== "sqlite" || !transport.capabilities?.includes("remote.run.protected.sql")) {
errors.push(`${location}.transportKey: sqlite-mutation operations require sqlite remote.run.protected.sql transport`);
}
if (operationTemplate.approvalLevel !== "platform-admin") {
errors.push(`${location}.approvalLevel: sqlite-mutation operations require platform-admin approval`);
}
if (!Number.isInteger(operationTemplate.maxRowsAffected) || (operationTemplate.maxRowsAffected ?? 0) < 1 || (operationTemplate.maxRowsAffected ?? 0) > 10) {
errors.push(`${location}.maxRowsAffected: must be an integer between 1 and 10`);
}
const safety = operationTemplate.safety;
if (!safety?.requiresBeforeValue || !safety.requiresConfirmation || (!safety.requiresOfflinePlayer && !safety.requiresMaintenanceWindow)) {
errors.push(`${location}.safety: sqlite-mutation operations require before value, confirmation, and offline or maintenance protection`);
}
const mutation = operationTemplate.mutation;
if (!mutation) {
errors.push(`${location}.mutation: sqlite-mutation operations require field/table/identity metadata`);
} else {
for (const field of ["fieldKey", "tableKey", "identityKey", "valueKey", "confirmationQueryKey"] as const) {
const value = mutation[field] ?? "";
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/.test(value) || unsafeGameClientBridgePayloadKey(value)) {
errors.push(`${location}.mutation.${field}: must be a safe logical key`);
}
}
if (!new Set(["integer", "number", "string", "boolean"]).has(mutation.allowedValueType ?? "")) {
errors.push(`${location}.mutation.allowedValueType: must be integer, number, string, or boolean`);
}
if (mutation.minValue !== undefined && mutation.maxValue !== undefined && mutation.minValue > mutation.maxValue) {
errors.push(`${location}.mutation: minValue must not exceed maxValue`);
}
if (mutation.confirmationQueryKey && !queryTemplates.has(mutation.confirmationQueryKey)) {
errors.push(`${location}.mutation.confirmationQueryKey: must reference a declared query template`);
}
}
}
}
for (const [index, page] of (bridge.pages ?? []).entries()) {
for (const commandType of page.commandTypes ?? []) {
if (!commands.has(commandType)) {
@@ -864,6 +975,17 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
errors.push(`manifest.gameClientBridge.pages[${index}].queryTemplateKeys: page must declare remote.access.request`);
}
}
for (const operationKey of page.operationKeys ?? []) {
const operationTemplate = operationTemplates.get(operationKey);
if (!operationTemplate) {
errors.push(`manifest.gameClientBridge.pages[${index}].operationKeys: undeclared operation template ${operationKey}`);
continue;
}
const pluginPage = declaration.pages?.find((candidate) => candidate.key === page.pageKey);
if (!pluginPage?.permissions?.includes(operationTemplate.permission ?? "")) {
errors.push(`manifest.gameClientBridge.pages[${index}].operationKeys: page must declare operation template permission ${operationTemplate.permission ?? ""}`);
}
}
}
return errors;
}
@@ -944,7 +1066,8 @@ function referencedGameClientBridgeSchemas(manifest: unknown): GameClientBridgeS
type BridgeCommand = { payloadSchemaRef?: string; resultSchemaRef?: string };
type BridgeSnapshot = { schemaRef?: string };
type BridgeQueryTemplate = { parameterSchemaRef?: string; resultSchemaRef?: string };
const bridge = (manifest as { gameClientBridge?: { commands?: BridgeCommand[]; snapshots?: BridgeSnapshot[]; queryTemplates?: BridgeQueryTemplate[] } }).gameClientBridge;
type BridgeOperationTemplate = { payloadSchemaRef?: string; resultSchemaRef?: string; confirmationSchemaRef?: string };
const bridge = (manifest as { gameClientBridge?: { commands?: BridgeCommand[]; snapshots?: BridgeSnapshot[]; queryTemplates?: BridgeQueryTemplate[]; operationTemplates?: BridgeOperationTemplate[] } }).gameClientBridge;
if (!bridge) {
return [];
}
@@ -970,6 +1093,17 @@ function referencedGameClientBridgeSchemas(manifest: unknown): GameClientBridgeS
refs.push({ location: `manifest.gameClientBridge.queryTemplates[${index}].resultSchemaRef`, ref: queryTemplate.resultSchemaRef });
}
}
for (const [index, operationTemplate] of (bridge.operationTemplates ?? []).entries()) {
if (operationTemplate.payloadSchemaRef) {
refs.push({ location: `manifest.gameClientBridge.operationTemplates[${index}].payloadSchemaRef`, ref: operationTemplate.payloadSchemaRef });
}
if (operationTemplate.resultSchemaRef) {
refs.push({ location: `manifest.gameClientBridge.operationTemplates[${index}].resultSchemaRef`, ref: operationTemplate.resultSchemaRef });
}
if (operationTemplate.confirmationSchemaRef) {
refs.push({ location: `manifest.gameClientBridge.operationTemplates[${index}].confirmationSchemaRef`, ref: operationTemplate.confirmationSchemaRef });
}
}
return refs;
}