store & reference pattern by name (#1766)

## Description:

Store pattern by name instead of value. The worker replaces the pattern
name with it's base64 when joining. This ensures the client & server are
never out of sync after patterns are updated.

* removed resizeObserver on the territory modal, it was causing some
race conditions, and the modal is not resizable so it's unnecessary.

* Moved PatternSchema to CosmeticSchema
## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I have read and accepted the CLA agreement (only required once).

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
evanpelle
2025-08-16 18:08:16 -07:00
committed by GitHub
parent 77fd82b4b4
commit b57a409b8a
12 changed files with 160 additions and 159 deletions
+24 -34
View File
@@ -1,11 +1,16 @@
import { Cosmetics, Pattern } from "../core/CosmeticSchemas";
import { Cosmetics } from "../core/CosmeticSchemas";
import { PatternDecoder } from "../core/PatternDecoder";
type PatternResult =
| { type: "allowed"; pattern: string }
| { type: "unknown" }
| { type: "forbidden"; reason: string };
export interface PrivilegeChecker {
isPatternAllowed(
base64: string,
flares: readonly string[] | undefined,
): true | "restricted" | "unlisted" | "invalid";
): PatternResult;
isCustomFlagAllowed(
flag: string,
flares: readonly string[] | undefined,
@@ -13,49 +18,34 @@ export interface PrivilegeChecker {
}
export class PrivilegeCheckerImpl implements PrivilegeChecker {
private b64ToPattern: Record<string, Pattern> = {};
constructor(
private cosmetics: Cosmetics,
private b64urlDecode: (base64: string) => Uint8Array,
) {
for (const name in this.cosmetics.patterns) {
const pattern = this.cosmetics.patterns[name];
this.b64ToPattern[pattern.pattern] = pattern;
}
}
) {}
isPatternAllowed(
base64: string,
name: string,
flares: readonly string[] | undefined,
): true | "restricted" | "unlisted" | "invalid" {
): PatternResult {
// Look for the pattern in the cosmetics.json config
const found = this.b64ToPattern[base64];
if (found === undefined) {
try {
// Ensure that the pattern will not throw for clients
new PatternDecoder(base64, this.b64urlDecode);
} catch (e) {
// Pattern is invalid
return "invalid";
}
// Pattern is unlisted
if (flares !== undefined && flares.includes("pattern:*")) {
// Player has the super-flare
return true;
}
return "unlisted";
const found = this.cosmetics.patterns[name];
if (!found) return { type: "forbidden", reason: "pattern not found" };
try {
new PatternDecoder(found.pattern, this.b64urlDecode);
} catch (e) {
return { type: "forbidden", reason: "invalid pattern" };
}
if (
flares !== undefined &&
(flares.includes(`pattern:${found.name}`) || flares.includes("pattern:*"))
flares?.includes(`pattern:${found.name}`) ||
flares?.includes("pattern:*")
) {
// Player has a flare for this pattern
return true;
return { type: "allowed", pattern: found.pattern };
} else {
return { type: "forbidden", reason: "no flares for pattern" };
}
return "restricted";
}
isCustomFlagAllowed(
@@ -136,8 +126,8 @@ export class FailOpenPrivilegeChecker implements PrivilegeChecker {
isPatternAllowed(
name: string,
flares: readonly string[] | undefined,
): true | "restricted" | "unlisted" | "invalid" {
return true;
): PatternResult {
return { type: "unknown" };
}
isCustomFlagAllowed(
+23 -8
View File
@@ -24,6 +24,7 @@ import { gatekeeper, LimiterType } from "./Gatekeeper";
import { getUserMe, verifyClientToken } from "./jwt";
import { logger } from "./Logger";
import { assertNever } from "../core/Util";
import { PrivilegeRefresher } from "./PrivilegeRefresher";
import { initWorkerMetrics } from "./WorkerMetrics";
@@ -410,15 +411,29 @@ export async function startWorker() {
}
}
let pattern: string | undefined;
// Check if the pattern is allowed
if (clientMsg.pattern !== undefined) {
const allowed = privilegeRefresher
if (clientMsg.patternName !== undefined) {
const result = privilegeRefresher
.get()
.isPatternAllowed(clientMsg.pattern, flares);
if (allowed !== true) {
log.warn(`Pattern ${allowed}: ${clientMsg.pattern}`);
ws.close(1002, `Pattern ${allowed}`);
return;
.isPatternAllowed(clientMsg.patternName, flares);
switch (result.type) {
case "allowed":
pattern = result.pattern;
break;
case "unknown":
// Api could be down, so allow player to join but disable pattern.
log.warn(`Pattern ${clientMsg.patternName} unknown`);
break;
case "forbidden":
log.warn(`Pattern ${clientMsg.patternName}: ${result.reason}`);
ws.close(
1002,
`Pattern ${clientMsg.patternName}: ${result.reason}`,
);
return;
default:
assertNever(result);
}
}
@@ -433,7 +448,7 @@ export async function startWorker() {
clientMsg.username,
ws,
clientMsg.flag,
clientMsg.pattern,
pattern,
);
const wasFound = gm.addClient(