From bad09df3e23429f508a3cfabab0a7308a6b67532 Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Thu, 13 Aug 2026 08:27:22 +0800 Subject: [PATCH] Add SCUM player query assets --- .../tasks.md | 2 +- .../queries/player-details-read.json | 27 +++++++ .../queries/player-economy-read.json | 29 +++++++ .../player-session-enrichment-read.json | 27 +++++++ .../scum-live/queries/players-read.json | 28 +++++++ .../examples/scum-server-plugin/manifest.json | 78 +++++++++++++++++++ ...player-details-read.parameters.schema.json | 10 +++ .../player-details-read.result.schema.json | 34 ++++++++ ...player-economy-read.parameters.schema.json | 11 +++ .../player-economy-read.result.schema.json | 26 +++++++ ...ion-enrichment-read.parameters.schema.json | 10 +++ ...session-enrichment-read.result.schema.json | 30 +++++++ .../players-read.parameters.schema.json | 11 +++ .../scum-live/players-read.result.schema.json | 31 ++++++++ plugins/tests/manifest-validation.test.ts | 28 ++++++- 15 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 plugins/examples/scum-server-plugin/assets/scum-live/queries/player-details-read.json create mode 100644 plugins/examples/scum-server-plugin/assets/scum-live/queries/player-economy-read.json create mode 100644 plugins/examples/scum-server-plugin/assets/scum-live/queries/player-session-enrichment-read.json create mode 100644 plugins/examples/scum-server-plugin/assets/scum-live/queries/players-read.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/player-details-read.parameters.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/player-details-read.result.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/player-economy-read.parameters.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/player-economy-read.result.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/player-session-enrichment-read.parameters.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/player-session-enrichment-read.result.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/players-read.parameters.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/players-read.result.schema.json diff --git a/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md b/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md index d27d1de..0763eaa 100644 --- a/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md +++ b/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md @@ -59,7 +59,7 @@ - [x] 3.2 Extend plugin validation to require asset digests, contained package paths, unique template keys, bounded parameters/results, compatible adapter versions, and explicit permission bindings, and to reject raw caller-supplied SQL, RCON, XML, paths, or undeclared parameters. - [x] 3.3 Capture sanitized authentic login-log fixtures from the active service and bind their expected events to server, Run binding, plugin version, parser version/digest, a transport cursor `(source identity, stream generation, sequence)`, and a separate privacy-safe logical event identity stable across rotation overlap. - [x] 3.4 Implement the versioned SCUM login/logout parser and tests for successful login/logout, failed login, partial/undecodable/oversized/malformed lines, copy-truncate/rotation overlap under a new generation, Run restart/resume, duplicate delivery, and out-of-order delivery while discarding IP/network material before storage or logical fingerprinting. -- [ ] 3.5 Add parameterized, read-only player identity/detail/economy/session-enrichment query assets and exact result schemas only for joins and fields proven by the probe. +- [x] 3.5 Add parameterized, read-only player identity/detail/economy/session-enrichment query assets and exact result schemas only for joins and fields proven by the probe. - [ ] 3.6 Add parameterized squad/member, vehicle, flag/territory, and position query assets and exact result schemas, keeping ambiguous ranks, ownership, coordinates, and missing numeric values null. - [ ] 3.7 Add query-asset tests for single SELECT/CTE or approved introspection boundaries, parameter binding, pagination/cursors, timeout/row/byte limits, schema-version matching, and rejection of DDL, mutation, `ATTACH`, extension loading, write PRAGMAs, and multi-statement input. - [ ] 3.8 Package the authorized SCUM map asset, identity/version, verified world bounds, layer metadata, and coordinate transform, with fixture tests for known points, out-of-bounds/non-finite coordinates, and adapter incompatibility. diff --git a/plugins/examples/scum-server-plugin/assets/scum-live/queries/player-details-read.json b/plugins/examples/scum-server-plugin/assets/scum-live/queries/player-details-read.json new file mode 100644 index 0000000..0bcbbd8 --- /dev/null +++ b/plugins/examples/scum-server-plugin/assets/scum-live/queries/player-details-read.json @@ -0,0 +1,27 @@ +{ + "key": "scum-player-details-read-v1", + "adapterVersion": "scum-live-data-v1", + "queryVersion": "scum-sqlite-query-v1", + "capability": "player-details.read", + "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", + "statementType": "single-select-or-cte", + "statement": "SELECT u.id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, u.creation_time AS userCreationTime, u.last_login_time AS userLastLoginTime, u.is_banned AS isBanned, up.id AS profileId, up.name AS displayName, up.type AS profileType, p.id AS prisonerId, p.user_profile_id AS prisonerUserProfileId, p.last_save_time AS prisonerLastSaveTime, pe.entity_id AS entityId, CASE WHEN up.template_xml IS NULL THEN NULL ELSE length(up.template_xml) END AS profileXmlLength FROM \"user\" u LEFT JOIN user_profile up ON up.user_id = u.id AND up.type = 1 LEFT JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id LEFT JOIN prisoner_entity pe ON pe.prisoner_id = p.id WHERE u.id = :externalPlayerId LIMIT 1", + "parameters": [ + { "name": "externalPlayerId", "binding": ":externalPlayerId", "type": "string", "required": true, "minLength": 1, "maxLength": 96 } + ], + "safety": { + "queryOnly": true, + "readOnlyConnection": true, + "forbidMultipleStatements": true, + "forbidAttach": true, + "forbidWritePragmas": true, + "forbidExtensionLoading": true, + "maxRows": 1, + "timeoutMs": 2000, + "maxResultBytes": 32768 + }, + "evidence": { + "source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md", + "provenFields": ["user.id", "user.id_type", "user.provider", "user.creation_time", "user.last_login_time", "user.is_banned", "user_profile.id", "user_profile.name", "user_profile.type", "user_profile.template_xml", "prisoner.id", "prisoner.user_profile_id", "prisoner.last_save_time", "prisoner_entity.entity_id"] + } +} diff --git a/plugins/examples/scum-server-plugin/assets/scum-live/queries/player-economy-read.json b/plugins/examples/scum-server-plugin/assets/scum-live/queries/player-economy-read.json new file mode 100644 index 0000000..82cfe17 --- /dev/null +++ b/plugins/examples/scum-server-plugin/assets/scum-live/queries/player-economy-read.json @@ -0,0 +1,29 @@ +{ + "key": "scum-player-economy-read-v1", + "adapterVersion": "scum-live-data-v1", + "queryVersion": "scum-sqlite-query-v1", + "capability": "player-details.read", + "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", + "statementType": "single-select-or-cte", + "statement": "SELECT u.id AS externalPlayerId, up.id AS profileId, bar.id AS bankAccountId, bac.id AS currencyRowId, bac.currency_type AS currencyType, bac.account_balance AS accountBalance FROM \"user\" u JOIN user_profile up ON up.user_id = u.id AND up.type = 1 JOIN bank_account_registry bar ON bar.account_owner_user_profile_id = up.id JOIN bank_account_registry_currencies bac ON bac.bank_account_id = bar.id WHERE u.id = :externalPlayerId ORDER BY bac.currency_type ASC, bac.id ASC LIMIT :limit", + "parameters": [ + { "name": "externalPlayerId", "binding": ":externalPlayerId", "type": "string", "required": true, "minLength": 1, "maxLength": 96 }, + { "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 16 } + ], + "safety": { + "queryOnly": true, + "readOnlyConnection": true, + "forbidMultipleStatements": true, + "forbidAttach": true, + "forbidWritePragmas": true, + "forbidExtensionLoading": true, + "maxRows": 16, + "timeoutMs": 2000, + "maxResultBytes": 32768 + }, + "evidence": { + "source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md", + "provenJoins": ["bank_account_registry.account_owner_user_profile_id -> user_profile.id", "bank_account_registry_currencies.bank_account_id -> bank_account_registry.id"], + "ambiguity": "Currency labels, units, command execution, and readback confirmation are unverified; currencyType remains numeric and version-scoped." + } +} diff --git a/plugins/examples/scum-server-plugin/assets/scum-live/queries/player-session-enrichment-read.json b/plugins/examples/scum-server-plugin/assets/scum-live/queries/player-session-enrichment-read.json new file mode 100644 index 0000000..f96c763 --- /dev/null +++ b/plugins/examples/scum-server-plugin/assets/scum-live/queries/player-session-enrichment-read.json @@ -0,0 +1,27 @@ +{ + "key": "scum-player-session-enrichment-read-v1", + "adapterVersion": "scum-live-data-v1", + "queryVersion": "scum-sqlite-query-v1", + "capability": "players.read", + "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", + "statementType": "single-select-or-cte", + "statement": "SELECT u.id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, u.creation_time AS userCreationTime, u.last_login_time AS userLastLoginTime, u.is_banned AS isBanned, up.id AS profileId, up.name AS displayName, up.type AS profileType, p.id AS prisonerId FROM \"user\" u LEFT JOIN user_profile up ON up.user_id = u.id AND up.type = 1 LEFT JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id WHERE u.id = :externalPlayerId LIMIT 1", + "parameters": [ + { "name": "externalPlayerId", "binding": ":externalPlayerId", "type": "string", "required": true, "minLength": 1, "maxLength": 96 } + ], + "safety": { + "queryOnly": true, + "readOnlyConnection": true, + "forbidMultipleStatements": true, + "forbidAttach": true, + "forbidWritePragmas": true, + "forbidExtensionLoading": true, + "maxRows": 1, + "timeoutMs": 2000, + "maxResultBytes": 32768 + }, + "evidence": { + "source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md", + "sessionBoundary": "Online state and local sessions are created from authenticated login/logout events; this query only enriches a login-created identity with proven database facts." + } +} diff --git a/plugins/examples/scum-server-plugin/assets/scum-live/queries/players-read.json b/plugins/examples/scum-server-plugin/assets/scum-live/queries/players-read.json new file mode 100644 index 0000000..f524a74 --- /dev/null +++ b/plugins/examples/scum-server-plugin/assets/scum-live/queries/players-read.json @@ -0,0 +1,28 @@ +{ + "key": "scum-players-read-v1", + "adapterVersion": "scum-live-data-v1", + "queryVersion": "scum-sqlite-query-v1", + "capability": "players.read", + "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", + "statementType": "single-select-or-cte", + "statement": "WITH player_rows AS (SELECT u.id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, u.creation_time AS userCreationTime, u.last_login_time AS userLastLoginTime, u.is_banned AS isBanned, up.id AS profileId, up.name AS displayName, up.type AS profileType, p.id AS prisonerId, pe.entity_id AS entityId FROM \"user\" u LEFT JOIN user_profile up ON up.user_id = u.id AND up.type = 1 LEFT JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id LEFT JOIN prisoner_entity pe ON pe.prisoner_id = p.id ORDER BY u.last_login_time DESC, u.id ASC LIMIT :limit OFFSET :offset) SELECT externalPlayerId, idType, provider, userCreationTime, userLastLoginTime, isBanned, profileId, displayName, profileType, prisonerId, entityId FROM player_rows", + "parameters": [ + { "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 100 }, + { "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 } + ], + "safety": { + "queryOnly": true, + "readOnlyConnection": true, + "forbidMultipleStatements": true, + "forbidAttach": true, + "forbidWritePragmas": true, + "forbidExtensionLoading": true, + "maxRows": 100, + "timeoutMs": 2000, + "maxResultBytes": 65536 + }, + "evidence": { + "source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md", + "provenJoins": ["user_profile.user_id -> user.id", "user_profile.prisoner_id -> prisoner.id", "prisoner.user_profile_id -> user_profile.id", "prisoner_entity.prisoner_id -> prisoner.id"] + } +} diff --git a/plugins/examples/scum-server-plugin/manifest.json b/plugins/examples/scum-server-plugin/manifest.json index a6ce8ba..6276048 100644 --- a/plugins/examples/scum-server-plugin/manifest.json +++ b/plugins/examples/scum-server-plugin/manifest.json @@ -421,6 +421,68 @@ "cursorPolicy": "source-generation-sequence", "privacy": { "stripNetworkIdentifiers": true, "logicalEventIdentity": "native-or-sanitized-fields" } } + ], + "sqliteQueries": [ + { + "key": "scum-players-read", + "adapterVersion": "scum-live-data-v1", + "assetPath": "assets/scum-live/queries/players-read.json", + "digest": "sha256:b3359f3c5939a410a98ddfccb920791044df76da4f8e5df4e726a9fe58423520", + "capability": "players.read", + "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", + "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": 2000, + "maxResultBytes": 65536 + }, + { + "key": "scum-player-details-read", + "adapterVersion": "scum-live-data-v1", + "assetPath": "assets/scum-live/queries/player-details-read.json", + "digest": "sha256:c980807b736f8988cd7c37ecb97c72e5e707050450ebc8c539dccec06c60e1f8", + "capability": "player-details.read", + "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", + "transportKey": "scum-database", + "targetKey": "scum-database", + "parameterSchemaRef": "schemas/scum-live/player-details-read.parameters.schema.json", + "resultSchemaRef": "schemas/scum-live/player-details-read.result.schema.json", + "maxRows": 1, + "timeoutMs": 2000, + "maxResultBytes": 32768 + }, + { + "key": "scum-player-economy-read", + "adapterVersion": "scum-live-data-v1", + "assetPath": "assets/scum-live/queries/player-economy-read.json", + "digest": "sha256:919c5f68f74fdb0743fa5d9b59aeecd003dcdac5bc51e52d3a486ed1045c9a48", + "capability": "player-details.read", + "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", + "transportKey": "scum-database", + "targetKey": "scum-database", + "parameterSchemaRef": "schemas/scum-live/player-economy-read.parameters.schema.json", + "resultSchemaRef": "schemas/scum-live/player-economy-read.result.schema.json", + "maxRows": 16, + "timeoutMs": 2000, + "maxResultBytes": 32768 + }, + { + "key": "scum-player-session-enrichment-read", + "adapterVersion": "scum-live-data-v1", + "assetPath": "assets/scum-live/queries/player-session-enrichment-read.json", + "digest": "sha256:d9c262d0b4f1b600b608c8279a6dfb052de1a605fe1286ddc15f74892eace4fc", + "capability": "players.read", + "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", + "transportKey": "scum-database", + "targetKey": "scum-database", + "parameterSchemaRef": "schemas/scum-live/player-session-enrichment-read.parameters.schema.json", + "resultSchemaRef": "schemas/scum-live/player-session-enrichment-read.result.schema.json", + "maxRows": 1, + "timeoutMs": 2000, + "maxResultBytes": 32768 + } ] }, "permissions": [ @@ -480,6 +542,22 @@ { "path": "assets/scum-live/login-log-parser.json", "mode": 384 + }, + { + "path": "assets/scum-live/queries/players-read.json", + "mode": 384 + }, + { + "path": "assets/scum-live/queries/player-details-read.json", + "mode": 384 + }, + { + "path": "assets/scum-live/queries/player-economy-read.json", + "mode": 384 + }, + { + "path": "assets/scum-live/queries/player-session-enrichment-read.json", + "mode": 384 } ], "productionLifecycle": { diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/player-details-read.parameters.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/player-details-read.parameters.schema.json new file mode 100644 index 0000000..7ade211 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/player-details-read.parameters.schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SCUMPlayerDetailsReadParameters", + "type": "object", + "additionalProperties": false, + "required": ["externalPlayerId"], + "properties": { + "externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/player-details-read.result.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/player-details-read.result.schema.json new file mode 100644 index 0000000..28a76f2 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/player-details-read.result.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SCUMPlayerDetailsReadResult", + "type": "object", + "additionalProperties": false, + "required": ["rows"], + "properties": { + "rows": { + "type": "array", + "maxItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["externalPlayerId", "idType", "provider", "userCreationTime", "userLastLoginTime", "isBanned", "profileId", "displayName", "profileType", "prisonerId", "prisonerUserProfileId", "prisonerLastSaveTime", "entityId", "profileXmlLength"], + "properties": { + "externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" }, + "idType": { "type": "string", "minLength": 1, "maxLength": 32 }, + "provider": { "type": "string", "minLength": 1, "maxLength": 32 }, + "userCreationTime": { "type": "string", "minLength": 1, "maxLength": 64 }, + "userLastLoginTime": { "type": "string", "minLength": 1, "maxLength": 64 }, + "isBanned": { "type": "integer", "minimum": 0, "maximum": 1 }, + "profileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "displayName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 }, + "profileType": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "prisonerId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "prisonerUserProfileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "prisonerLastSaveTime": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "entityId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "profileXmlLength": { "type": ["integer", "null"], "minimum": 0, "maximum": 1048576 } + } + } + } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/player-economy-read.parameters.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/player-economy-read.parameters.schema.json new file mode 100644 index 0000000..7047138 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/player-economy-read.parameters.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SCUMPlayerEconomyReadParameters", + "type": "object", + "additionalProperties": false, + "required": ["externalPlayerId", "limit"], + "properties": { + "externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" }, + "limit": { "type": "integer", "minimum": 1, "maximum": 16 } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/player-economy-read.result.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/player-economy-read.result.schema.json new file mode 100644 index 0000000..043a1c9 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/player-economy-read.result.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SCUMPlayerEconomyReadResult", + "type": "object", + "additionalProperties": false, + "required": ["rows"], + "properties": { + "rows": { + "type": "array", + "maxItems": 16, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["externalPlayerId", "profileId", "bankAccountId", "currencyRowId", "currencyType", "accountBalance"], + "properties": { + "externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" }, + "profileId": { "type": "integer", "minimum": 0, "maximum": 2147483647 }, + "bankAccountId": { "type": "integer", "minimum": 0, "maximum": 2147483647 }, + "currencyRowId": { "type": "integer", "minimum": 0, "maximum": 2147483647 }, + "currencyType": { "type": "integer", "minimum": 0, "maximum": 2147483647 }, + "accountBalance": { "type": ["integer", "null"], "minimum": -2147483648, "maximum": 2147483647 } + } + } + } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/player-session-enrichment-read.parameters.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/player-session-enrichment-read.parameters.schema.json new file mode 100644 index 0000000..843018f --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/player-session-enrichment-read.parameters.schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SCUMPlayerSessionEnrichmentReadParameters", + "type": "object", + "additionalProperties": false, + "required": ["externalPlayerId"], + "properties": { + "externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/player-session-enrichment-read.result.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/player-session-enrichment-read.result.schema.json new file mode 100644 index 0000000..6061551 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/player-session-enrichment-read.result.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SCUMPlayerSessionEnrichmentReadResult", + "type": "object", + "additionalProperties": false, + "required": ["rows"], + "properties": { + "rows": { + "type": "array", + "maxItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["externalPlayerId", "idType", "provider", "userCreationTime", "userLastLoginTime", "isBanned", "profileId", "displayName", "profileType", "prisonerId"], + "properties": { + "externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" }, + "idType": { "type": "string", "minLength": 1, "maxLength": 32 }, + "provider": { "type": "string", "minLength": 1, "maxLength": 32 }, + "userCreationTime": { "type": "string", "minLength": 1, "maxLength": 64 }, + "userLastLoginTime": { "type": "string", "minLength": 1, "maxLength": 64 }, + "isBanned": { "type": "integer", "minimum": 0, "maximum": 1 }, + "profileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "displayName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 }, + "profileType": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "prisonerId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 } + } + } + } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/players-read.parameters.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/players-read.parameters.schema.json new file mode 100644 index 0000000..cc4dd15 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/players-read.parameters.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SCUMPlayersReadParameters", + "type": "object", + "additionalProperties": false, + "required": ["limit", "offset"], + "properties": { + "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, + "offset": { "type": "integer", "minimum": 0, "maximum": 1000000 } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/players-read.result.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/players-read.result.schema.json new file mode 100644 index 0000000..88e1e41 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/players-read.result.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SCUMPlayersReadResult", + "type": "object", + "additionalProperties": false, + "required": ["rows"], + "properties": { + "rows": { + "type": "array", + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["externalPlayerId", "idType", "provider", "userCreationTime", "userLastLoginTime", "isBanned", "profileId", "displayName", "profileType", "prisonerId", "entityId"], + "properties": { + "externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" }, + "idType": { "type": "string", "minLength": 1, "maxLength": 32 }, + "provider": { "type": "string", "minLength": 1, "maxLength": 32 }, + "userCreationTime": { "type": "string", "minLength": 1, "maxLength": 64 }, + "userLastLoginTime": { "type": "string", "minLength": 1, "maxLength": 64 }, + "isBanned": { "type": "integer", "minimum": 0, "maximum": 1 }, + "profileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "displayName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 }, + "profileType": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "prisonerId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }, + "entityId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 } + } + } + } + } +} diff --git a/plugins/tests/manifest-validation.test.ts b/plugins/tests/manifest-validation.test.ts index 3eff0c0..a17df9f 100644 --- a/plugins/tests/manifest-validation.test.ts +++ b/plugins/tests/manifest-validation.test.ts @@ -218,6 +218,32 @@ describe("plugin manifest validation", () => { expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/); }); + it("packages SCUM player 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 }; + const assetPaths = new Set(manifest.assetFiles?.map((file) => file.path) ?? []); + const queries = manifest.scumLiveData.sqliteQueries ?? []; + expect(queries.map((query) => query.key)).toEqual(["scum-players-read", "scum-player-details-read", "scum-player-economy-read", "scum-player-session-enrichment-read"]); + + for (const query of queries) { + expect(assetPaths.has(query.assetPath)).toBe(true); + const assetPath = path.join(pluginDir, query.assetPath); + const actualDigest = `sha256:${crypto.createHash("sha256").update(fs.readFileSync(assetPath)).digest("hex")}`; + expect(query.digest).toBe(actualDigest); + const asset = JSON.parse(fs.readFileSync(assetPath, "utf8")) as { requiredSchemaFingerprint?: string; statement?: string; safety?: { queryOnly?: boolean; readOnlyConnection?: boolean } }; + expect(asset.requiredSchemaFingerprint).toBe(query.requiredSchemaFingerprint); + expect(asset.safety).toMatchObject({ queryOnly: true, readOnlyConnection: true }); + expect(asset.statement).toMatch(/^(WITH|SELECT)\b/i); + expect(asset.statement).not.toMatch(/;|\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|ATTACH)\b/i); + + for (const schemaRef of [query.parameterSchemaRef, query.resultSchemaRef]) { + expect(fs.existsSync(path.join(pluginDir, schemaRef))).toBe(true); + } + const resultSchema = JSON.parse(fs.readFileSync(path.join(pluginDir, query.resultSchemaRef), "utf8")) as { properties?: { rows?: { maxItems?: number } } }; + expect(resultSchema.properties?.rows?.maxItems).toBeLessThanOrEqual(query.maxRows); + } + }); + it("requires SCUM schema probe targets to have generated Run workspace data targets", () => { const errors = validateTemporaryScumCompanionManifest((manifest) => { manifest.runtimeProfiles.dataTargets = []; @@ -238,7 +264,7 @@ describe("plugin manifest validation", () => { const fingerprint = `sha256:${"f".repeat(64)}`; const assetPaths = [ "assets/scum-live/login-parser.json", - "assets/scum-live/queries/players-read.json", + "assets/scum-live/queries/fixture-players-read.json", "assets/scum-live/rcon/gift-grant.json", "assets/scum-live/mutations/profile-xml-patch.json", "assets/scum-live/map/island.png",