feat: bind signed rosters to assignments

This commit is contained in:
Josh Creek
2026-09-01 08:10:05 +01:00
parent 5b40bf9066
commit fec4f91672
3 changed files with 41 additions and 1 deletions
+35
View File
@@ -3,8 +3,13 @@ package store
import (
"context"
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/cosmic-clash/cosmic-clash/server/domain"
)
// DurableAssignment is the persistence form of a verified assignment-ready
@@ -124,6 +129,36 @@ func SaveAssignments(ctx context.Context, db *sql.DB, assignments []DurableAssig
return tx.Commit()
}
// SaveVerifiedAssignmentRoster converts the backend-verified signed roster to
// player-scoped rows. It rechecks the claims at this persistence boundary so a
// caller cannot accidentally publish a token for another match or slot.
func SaveVerifiedAssignmentRoster(ctx context.Context, db *sql.DB, assignment domain.Assignment, roster []domain.SignedJoinAuthorisation) error {
if assignment.Allocation.State != domain.ServerAllocated || len(roster) == 0 {
return fmt.Errorf("invalid verified assignment roster")
}
digest := domain.ManifestDigest(assignment.Manifest)
rows := make([]DurableAssignment, 0, len(roster))
for _, signed := range roster {
auth := signed.Authorisation
if len(signed.Signature) == 0 || auth.MatchID != assignment.Allocation.MatchID || auth.ServerID != assignment.Allocation.ServerID || auth.Protocol != strconv.Itoa(assignment.Allocation.Protocol) || auth.PlayerID == "" || auth.Slot < 0 || auth.Slot > 5 || auth.ExpiresAt.IsZero() {
return fmt.Errorf("invalid signed assignment roster")
}
envelope, err := json.Marshal(signed)
if err != nil {
return fmt.Errorf("encode signed assignment roster: %w", err)
}
rows = append(rows, DurableAssignment{
MatchID: assignment.Allocation.MatchID, PlayerID: auth.PlayerID,
AllocationID: assignment.Allocation.AllocationID, ServerID: assignment.Allocation.ServerID,
Slot: auth.Slot, Region: assignment.Allocation.Region, ClientBuild: assignment.Allocation.Build,
ProtocolVersion: assignment.Allocation.Protocol, Transport: assignment.Allocation.Transport,
Endpoint: assignment.Endpoint, JoinAuthorisation: base64.RawURLEncoding.EncodeToString(envelope),
ManifestDigest: digest[:], ExpiresAt: auth.ExpiresAt, Revision: 1,
})
}
return SaveAssignments(ctx, db, rows)
}
func GetAssignment(ctx context.Context, db *sql.DB, playerID, matchID string, now time.Time) (DurableAssignment, error) {
if db == nil || playerID == "" || matchID == "" || now.IsZero() {
return DurableAssignment{}, fmt.Errorf("invalid assignment recovery arguments")