openvibers/openvibe.games Commits

Check protocol version before content digest and reject mod-pack maps

OP OpenVibers committed 4 Oct 2026 · 357d7a1092 · 4 files, +20 −6

Requested by finish-f4. Handed to the local harness by the cloud job pr-OpenVibe.Games-4; done by Claude opus.

Changes

ddiff --git a/apps/server/src/game/platformIntegration.test.ts b/apps/server/src/game/platformIntegration.test.ts
iindex c0dc028..f85ca48 100644
---- a/apps/server/src/game/platformIntegration.test.ts
++++ b/apps/server/src/game/platformIntegration.test.ts
@@@ -221,6 +221,15 @@ describe('Games on platform identity, events and mods (real server, restart)', (
})
expect(stale.inbox.some((m) => m.t === 'welcome')).toBe(false)
}
++ // An older bundle sends neither the current version nor a digest; it must
++ // get protocol_mismatch, the only reason the client reloads itself on.
++ const oldBundle = connect(a.game)
++ oldBundle.conn.identity = { guestKeyHash: hashGuestKey('old-bundle') }
++ a.game.onMessage(oldBundle.conn, {
++ t: 'hello', v: PROTOCOL_VERSION - 1, slot: 0, name: 'Old', appearance: defaultAppearance(),
++ })
++ await new Promise((r) => setTimeout(r, 0))
++ expect(oldBundle.inbox).toContainEqual({ t: 'reject', reason: 'protocol_mismatch' })
// The signed-in player is keyed by the canonical subject.
const ana = connect(a.game)
ddiff --git a/apps/server/src/game/systems/sessions.ts b/apps/server/src/game/systems/sessions.ts
iindex 5ed8dc8..cb5144f 100644
---- a/apps/server/src/game/systems/sessions.ts
++++ b/apps/server/src/game/systems/sessions.ts
@@@ -87,6 +87,12 @@ export class SessionsSystem implements System {
private async resolveHello(conn: GameConnection, msg: ClientHello): Promise<void> {
const ctx = this.ctx
const serverDigest = ctx.world.content.digest
++ // Protocol first: a stale bundle predates the digest and only reloads itself on protocol_mismatch.
++ if (msg.v !== PROTOCOL_VERSION) {
++ conn.send(encodeServerMessage({ t: 'reject', reason: 'protocol_mismatch' }))
++ conn.close(4002, 'protocol_mismatch')
++ return
++ }
if (msg.contentDigest !== serverDigest) {
conn.send(encodeServerMessage({
t: 'reject', reason: 'content_mismatch',
@@@ -95,11 +101,6 @@ export class SessionsSystem implements System {
conn.close(4013, 'content_mismatch')
return
}
-- if (msg.v !== PROTOCOL_VERSION) {
-- conn.send(encodeServerMessage({ t: 'reject', reason: 'protocol_mismatch' }))
-- conn.close(4002, 'protocol_mismatch')
-- return
-- }
if (ctx.sessions.size >= ctx.config.maxPlayers) {
conn.send(encodeServerMessage({ t: 'reject', reason: 'server_full' }))
conn.close(4003, 'server_full')
ddiff --git a/apps/server/src/mods/contentPack.test.ts b/apps/server/src/mods/contentPack.test.ts
iindex 197e37d..0937b0c 100644
---- a/apps/server/src/mods/contentPack.test.ts
++++ b/apps/server/src/mods/contentPack.test.ts
@@@ -44,7 +44,9 @@ describe('games-content@2 definitions', () => {
it('recognizes an authored map as a definition-capability section', () => {
const mapPack = { map: SCRAPLANDIA_PACK.map! }
expect(capabilitiesUsedBy(mapPack)).toEqual([CAP_DEFINE])
-- expect(validateContentPack(mapPack, createContent()).ok).toBe(true)
++ const result = validateContentPack(mapPack, createContent())
++ expect(result.ok).toBe(false)
++ expect(!result.ok && result.errors).toContain('map is defined by the built-in pack and cannot be replaced by a mod')
expect(validateContentPack({ map: { id: 'broken' } }, createContent()).ok).toBe(false)
})
it('needs games.def.define for any non-empty defs section', () => {
ddiff --git a/apps/server/src/mods/contentPack.ts b/apps/server/src/mods/contentPack.ts
iindex 4c0f7f7..bfe6062 100644
---- a/apps/server/src/mods/contentPack.ts
++++ b/apps/server/src/mods/contentPack.ts
@@@ -154,6 +154,8 @@ export function validateContentPack(
}
const pack = value as unknown as ContentPack
const errors: string[] = []
++ // The built-in pack always supplies the map, so a mod's map would be skipped at boot as a duplicate.
++ if (pack.map) errors.push('map is defined by the built-in pack and cannot be replaced by a mod')
if (pack.defs || pack.map) {
// The content the pack would be merged onto; its own ids may not already exist.
const merged = mergePackDefs(