fix(multiplayer): align websocket assignment ids

This commit is contained in:
Josh Creek
2026-09-01 22:42:04 +01:00
parent 2bdf876b47
commit e376e1e80d
4 changed files with 8 additions and 3 deletions
+2
View File
@@ -1591,6 +1591,8 @@ Ranked profile projection now enforces the OpenAPI tier enum, rejecting unknown
Assignment projections and assignment-changed events now enforce the published opaque-ID shape for match, server, and player identifiers; short or unsafe IDs fail closed.
The WebSocket contract and Go event hub now enforce opaque match and server IDs on assignment notifications, keeping server publication aligned with the Godot client validator.
Signed MatchNet claims now also require exact JSON string/integer types for every identity, protocol, expiry, slot, team, and generation field; string-number coercion is rejected before canonical signature verification.
Presentation progress: a shared `Game/themes/cosmic_clash_theme.tres` now gives the menu, lobby, matchmaking, and settings surfaces consistent button, input, option, and label styling. The custom-font portion of `TODO.md` remains open until a distributable font asset is selected.
+1 -1
View File
@@ -131,7 +131,7 @@ func validateControlPlaneEvent(event ControlPlaneEvent) error {
return errors.New("invalid proposal-changed event")
}
case "assignment_changed":
if event.MatchID == "" || event.ServerID == "" {
if !controlPlaneResourceIDRE.MatchString(event.MatchID) || !controlPlaneResourceIDRE.MatchString(event.ServerID) {
return errors.New("invalid assignment-changed event")
}
case "error":
+2
View File
@@ -437,6 +437,8 @@ func TestEventHubRejectsEventsOutsideTheV1Vocabulary(t *testing.T) {
{Event: "state_changed", State: "NOT_A_STATE", Revision: base.Revision, ResourceID: base.ResourceID, OccurredAt: base.OccurredAt, PlayerID: base.PlayerID},
{Event: "proposal_changed", State: "LIVE", Revision: base.Revision, ResourceID: base.ResourceID, OccurredAt: base.OccurredAt, PlayerID: base.PlayerID},
{Event: "assignment_changed", Revision: base.Revision, ResourceID: base.ResourceID, OccurredAt: base.OccurredAt, PlayerID: base.PlayerID},
{Event: "assignment_changed", MatchID: "match-1", ServerID: "server-1", Revision: base.Revision, ResourceID: base.ResourceID, OccurredAt: base.OccurredAt, PlayerID: base.PlayerID},
{Event: "assignment_changed", MatchID: "match_1234567890", ServerID: "server/unsafe", Revision: base.Revision, ResourceID: base.ResourceID, OccurredAt: base.OccurredAt, PlayerID: base.PlayerID},
{Event: "error", Code: "SECRET_LEAK", Revision: base.Revision, ResourceID: base.ResourceID, OccurredAt: base.OccurredAt, PlayerID: base.PlayerID},
}
for _, event := range invalid {
+3 -2
View File
@@ -9,10 +9,11 @@
{"$ref": "#/$defs/error"}
],
"$defs": {
"envelope": {"type": "object", "required": ["event", "revision", "resource_id", "occurred_at"], "properties": {"revision": {"type": "integer", "minimum": 0}, "resource_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{16,128}$"}, "occurred_at": {"type": "string", "format": "date-time"}}},
"opaqueId": {"type": "string", "pattern": "^[A-Za-z0-9_-]{16,128}$"},
"envelope": {"type": "object", "required": ["event", "revision", "resource_id", "occurred_at"], "properties": {"revision": {"type": "integer", "minimum": 0}, "resource_id": {"$ref": "#/$defs/opaqueId"}, "occurred_at": {"type": "string", "format": "date-time"}}},
"stateChanged": {"allOf": [{"$ref": "#/$defs/envelope"}, {"type": "object", "properties": {"event": {"const": "state_changed"}, "state": {"type": "string", "enum": ["QUEUED", "PROPOSED", "ACCEPTED", "ALLOCATING", "PROCESS_READY", "ASSIGNMENT_READY", "ASSIGNED", "CONNECTING", "LIVE", "RESULT_PENDING", "COMPLETED", "CANCELLED", "EXPIRED", "FAILED"]}}, "required": ["event", "state"]}]},
"proposalChanged": {"allOf": [{"$ref": "#/$defs/envelope"}, {"type": "object", "properties": {"event": {"const": "proposal_changed"}, "state": {"type": "string", "enum": ["OPEN", "ACCEPTED", "DECLINED", "EXPIRED", "CANCELLED"]}}, "required": ["event", "state"]}]},
"assignmentChanged": {"allOf": [{"$ref": "#/$defs/envelope"}, {"type": "object", "properties": {"event": {"const": "assignment_changed"}, "match_id": {"type": "string"}, "server_id": {"type": "string"}}, "required": ["event", "match_id", "server_id"]}]},
"assignmentChanged": {"allOf": [{"$ref": "#/$defs/envelope"}, {"type": "object", "properties": {"event": {"const": "assignment_changed"}, "match_id": {"$ref": "#/$defs/opaqueId"}, "server_id": {"$ref": "#/$defs/opaqueId"}}, "required": ["event", "match_id", "server_id"]}]},
"error": {"allOf": [{"$ref": "#/$defs/envelope"}, {"type": "object", "properties": {"event": {"const": "error"}, "code": {"type": "string", "enum": ["REVISION_GAP", "NOT_AUTHORISED", "INVALID_STATE", "RATE_LIMITED"]}}, "required": ["event", "code"]}]}
}
}