custom flag (2) (#1303)

## Description:

This PR implements the permission check logic.

Other related parts will be handled in a separate UI update.

## 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 understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors
This commit is contained in:
Aotumuri
2025-07-03 09:24:52 +09:00
committed by GitHub
parent a83c2bda97
commit 4dd6c9bac3
9 changed files with 713 additions and 101 deletions
+398 -90
View File
@@ -123,98 +123,406 @@
},
"flag": {
"layers": {
"a": { "name": "center_circle" },
"b": { "name": "center_hline" },
"c": { "name": "center_vline" },
"d": { "name": "center_star" },
"e": { "name": "center_flower" },
"f": { "name": "flower_tl" },
"g": { "name": "flower_tc" },
"h": { "name": "flower_tr" },
"i": { "name": "diag_br" },
"j": { "name": "diag_bl" },
"k": { "name": "frame" },
"l": { "name": "full" },
"m": { "name": "triangle_tl" },
"n": { "name": "triangle_bl" },
"o": { "name": "triangle_tr" },
"p": { "name": "triangle_br" },
"q": { "name": "half_l" },
"r": { "name": "half_r" },
"s": { "name": "half_t" },
"t": { "name": "half_b" },
"u": { "name": "mini_tr_bl" },
"v": { "name": "mini_tr_br" },
"w": { "name": "mini_tr_tl" },
"x": { "name": "mini_tr_tr" },
"y": { "name": "triangle_t" },
"z": { "name": "triangle_l" },
"aa": { "name": "triangle_b" },
"ab": { "name": "triangle_r" },
"ac": { "name": "tricolor_l" },
"ad": { "name": "tricolor_c" },
"ae": { "name": "tricolor_r" },
"af": { "name": "tricolor_t" },
"ag": { "name": "tricolor_m" },
"ah": { "name": "tricolor_b" },
"ai": { "name": "nato_emblem" },
"aj": { "name": "eu_star" },
"ak": { "name": "laurel_wreath" },
"al": { "name": "ofm_2025" },
"am": { "name": "octagram" },
"an": { "name": "octagram_2" },
"ao": { "name": "og" },
"ap": { "name": "og_plus" },
"aq": { "name": "beta_tester" },
"ar": { "name": "beta_tester_circle" },
"as": { "name": "rocket" },
"at": { "name": "rocket_mini" },
"au": { "name": "translator" },
"av": { "name": "admin_shield" },
"aw": { "name": "admin_shield_r" },
"ax": { "name": "admin_evan" }
"a": {
"name": "center_circle",
"role_group": "donor"
},
"b": {
"name": "center_hline",
"role_group": "donor"
},
"c": {
"name": "center_vline",
"role_group": "donor"
},
"d": {
"name": "center_star",
"role_group": "donor"
},
"e": {
"name": "center_flower",
"role_group": "donor"
},
"f": {
"name": "flower_tl",
"role_group": "donor"
},
"g": {
"name": "flower_tc",
"role_group": "donor"
},
"h": {
"name": "flower_tr",
"role_group": "donor"
},
"i": {
"name": "diag_br",
"role_group": "donor"
},
"j": {
"name": "diag_bl",
"role_group": "donor"
},
"k": {
"name": "frame"
},
"l": {
"name": "full"
},
"m": {
"name": "triangle_tl",
"role_group": "donor"
},
"n": {
"name": "triangle_bl",
"role_group": "donor"
},
"o": {
"name": "triangle_tr",
"role_group": "donor"
},
"p": {
"name": "triangle_br",
"role_group": "donor"
},
"q": {
"name": "half_l",
"role_group": "donor"
},
"r": {
"name": "half_r",
"role_group": "donor"
},
"s": {
"name": "half_t",
"role_group": "donor"
},
"t": {
"name": "half_b",
"role_group": "donor"
},
"u": {
"name": "mini_tr_bl",
"role_group": "donor"
},
"v": {
"name": "mini_tr_br",
"role_group": "donor"
},
"w": {
"name": "mini_tr_tl",
"role_group": "donor"
},
"x": {
"name": "mini_tr_tr",
"role_group": "donor"
},
"y": {
"name": "triangle_t",
"role_group": "donor"
},
"z": {
"name": "triangle_l",
"role_group": "donor"
},
"aa": {
"name": "triangle_b",
"role_group": "donor"
},
"ab": {
"name": "triangle_r",
"role_group": "donor"
},
"ac": {
"name": "tricolor_l",
"role_group": "donor"
},
"ad": {
"name": "tricolor_c",
"role_group": "donor"
},
"ae": {
"name": "tricolor_r",
"role_group": "donor"
},
"af": {
"name": "tricolor_t",
"role_group": "donor"
},
"ag": {
"name": "tricolor_m",
"role_group": "donor"
},
"ah": {
"name": "tricolor_b",
"role_group": "donor"
},
"ai": {
"name": "nato_emblem",
"role_group": "donor"
},
"aj": {
"name": "eu_star",
"role_group": "donor"
},
"ak": {
"name": "laurel_wreath",
"role_group": "donor"
},
"al": {
"name": "ofm_2025",
"role_group": "donor"
},
"am": {
"name": "octagram",
"role_group": "donor"
},
"an": {
"name": "octagram_2",
"role_group": "donor"
},
"ao": {
"name": "og",
"role_group": "donor"
},
"ap": {
"name": "og_plus",
"role_group": "donor"
},
"aq": {
"name": "beta_tester",
"role_group": "donor"
},
"ar": {
"name": "beta_tester_circle",
"role_group": "donor"
},
"as": {
"name": "rocket",
"role_group": "donor"
},
"at": {
"name": "rocket_mini",
"role_group": "donor"
},
"au": {
"name": "translator",
"role_group": "donor"
},
"av": {
"name": "admin_shield",
"role_group": "donor"
},
"aw": {
"name": "admin_shield_r",
"role_group": "donor"
},
"ax": {
"name": "admin_evan",
"role_group": "donor"
}
},
"color": {
"a": { "color": "#ff0000", "name": "red" },
"b": { "color": "#ffa500", "name": "orange" },
"c": { "color": "#ffff00", "name": "yellow" },
"d": { "color": "#008000", "name": "green" },
"e": { "color": "#00ffff", "name": "cyan" },
"f": { "color": "#0000ff", "name": "blue" },
"g": { "color": "#000000", "name": "black" },
"h": { "color": "#ffffff", "name": "white" },
"i": { "color": "#800080", "name": "purple" },
"j": { "color": "#ff69b4", "name": "hotpink" },
"k": { "color": "#a52a2a", "name": "brown" },
"l": { "color": "#808080", "name": "gray" },
"m": { "color": "#20b2aa", "name": "teal" },
"n": { "color": "#ff6347", "name": "tomato" },
"o": { "color": "#4682b4", "name": "steelblue" },
"p": { "color": "#90ee90", "name": "lightgreen" },
"q": { "color": "#8b0000", "name": "darkred" },
"r": { "color": "#191970", "name": "navy" },
"s": { "color": "#ffd700", "name": "gold" },
"t": { "color": "#add8e6", "name": "lightblue" },
"u": { "color": "#f5f5dc", "name": "beige" },
"v": { "color": "#ffb6c1", "name": "lightpink" },
"w": { "color": "#708090", "name": "slategray" },
"x": { "color": "#00ff7f", "name": "springgreen" },
"y": { "color": "#dc143c", "name": "crimson" },
"z": { "color": "#ffbf00", "name": "amber" },
"0": { "color": "#3d9970", "name": "olive_green" },
"1": { "color": "#87ceeb", "name": "sky_blue" },
"2": { "color": "#6a5acd", "name": "slate_blue" },
"3": { "color": "#ff66cc", "name": "rose_pink" },
"4": { "color": "#36454f", "name": "charcoal" },
"5": { "color": "#fffff0", "name": "ivory" },
"A": { "color": "rainbow", "name": "rainbow" },
"B": { "color": "bright-rainbow", "name": "bright_rainbow" },
"C": { "color": "gold-glow", "name": "gold_glow" },
"D": { "color": "silver-glow", "name": "silver_glow" },
"E": { "color": "copper-glow", "name": "copper_glow" },
"F": { "color": "neon", "name": "neon" },
"G": { "color": "lava", "name": "lava" },
"H": { "color": "water", "name": "water" }
"a": {
"color": "#ff0000",
"name": "red",
"role_group": "donor"
},
"b": {
"color": "#ffa500",
"name": "orange",
"role_group": "donor"
},
"c": {
"color": "#ffff00",
"name": "yellow",
"role_group": "donor"
},
"d": {
"color": "#008000",
"name": "green",
"role_group": "donor"
},
"e": {
"color": "#00ffff",
"name": "cyan",
"role_group": "donor"
},
"f": {
"color": "#0000ff",
"name": "blue",
"role_group": "donor"
},
"g": {
"color": "#000000",
"name": "black",
"role_group": "donor"
},
"h": {
"color": "#ffffff",
"name": "white",
"role_group": "donor"
},
"i": {
"color": "#800080",
"name": "purple",
"role_group": "donor"
},
"j": {
"color": "#ff69b4",
"name": "hotpink",
"role_group": "donor"
},
"k": {
"color": "#a52a2a",
"name": "brown",
"role_group": "donor"
},
"l": {
"color": "#808080",
"name": "gray",
"role_group": "donor"
},
"m": {
"color": "#20b2aa",
"name": "teal",
"role_group": "donor"
},
"n": {
"color": "#ff6347",
"name": "tomato",
"role_group": "donor"
},
"o": {
"color": "#4682b4",
"name": "steelblue",
"role_group": "donor"
},
"p": {
"color": "#90ee90",
"name": "lightgreen",
"role_group": "donor"
},
"q": {
"color": "#8b0000",
"name": "darkred",
"role_group": "donor"
},
"r": {
"color": "#191970",
"name": "navy",
"role_group": "donor"
},
"s": {
"color": "#ffd700",
"name": "gold",
"role_group": "donor"
},
"t": {
"color": "#add8e6",
"name": "lightblue",
"role_group": "donor"
},
"u": {
"color": "#f5f5dc",
"name": "beige",
"role_group": "donor"
},
"v": {
"color": "#ffb6c1",
"name": "lightpink",
"role_group": "donor"
},
"w": {
"color": "#708090",
"name": "slategray",
"role_group": "donor"
},
"x": {
"color": "#00ff7f",
"name": "springgreen",
"role_group": "donor"
},
"y": {
"color": "#dc143c",
"name": "crimson",
"role_group": "donor"
},
"z": {
"color": "#ffbf00",
"name": "amber",
"role_group": "donor"
},
"0": {
"color": "#3d9970",
"name": "olive_green",
"role_group": "donor"
},
"1": {
"color": "#87ceeb",
"name": "sky_blue",
"role_group": "donor"
},
"2": {
"color": "#6a5acd",
"name": "slate_blue",
"role_group": "donor"
},
"3": {
"color": "#ff66cc",
"name": "rose_pink",
"role_group": "donor"
},
"4": {
"color": "#36454f",
"name": "charcoal",
"role_group": "donor"
},
"5": {
"color": "#fffff0",
"name": "ivory",
"role_group": "donor"
},
"A": {
"color": "rainbow",
"name": "rainbow",
"role_group": "donor"
},
"B": {
"color": "bright-rainbow",
"name": "bright_rainbow",
"role_group": "donor"
},
"C": {
"color": "gold-glow",
"name": "gold_glow",
"role_group": "donor"
},
"D": {
"color": "silver-glow",
"name": "silver_glow",
"role_group": "donor"
},
"E": {
"color": "copper-glow",
"name": "copper_glow",
"role_group": "donor"
},
"F": {
"color": "neon",
"name": "neon",
"role_group": "donor"
},
"G": {
"color": "lava",
"name": "lava",
"role_group": "donor"
},
"H": {
"color": "water",
"name": "water",
"role_group": "donor"
}
}
}
}
+5 -1
View File
@@ -1,3 +1,4 @@
import { base64url } from "jose";
import type { TemplateResult } from "lit";
import { html, LitElement, render } from "lit";
import { customElement, query, state } from "lit/decorators.js";
@@ -369,7 +370,10 @@ export function generatePreviewDataUrl(
height?: number,
): string {
// Calculate canvas size
const decoder = new PatternDecoder(pattern ?? DEFAULT_PATTERN_B64);
const decoder = new PatternDecoder(
pattern ?? DEFAULT_PATTERN_B64,
base64url.decode,
);
const scaledWidth = decoder.scaledWidth();
const scaledHeight = decoder.scaledHeight();
+4
View File
@@ -17,6 +17,8 @@ export const CosmeticsSchema = z.object({
z.string(),
z.object({
name: z.string(),
role_group: z.string().optional(),
flares: z.array(z.string()).optional(),
}),
),
color: z.record(
@@ -24,6 +26,8 @@ export const CosmeticsSchema = z.object({
z.object({
color: z.string(),
name: z.string(),
role_group: z.string().optional(),
flares: z.array(z.string()).optional(),
}),
),
}),
+5 -4
View File
@@ -1,5 +1,3 @@
import { base64url } from "jose";
export class PatternDecoder {
private bytes: Uint8Array;
@@ -7,8 +5,11 @@ export class PatternDecoder {
readonly width: number;
readonly scale: number;
constructor(base64: string) {
this.bytes = base64url.decode(base64);
constructor(
base64: string,
base64urlDecode: (input: Uint8Array | string) => Uint8Array,
) {
this.bytes = base64urlDecode(base64);
if (this.bytes.length < 3) {
throw new Error(
+2 -1
View File
@@ -1,3 +1,4 @@
import { base64url } from "jose";
import { z } from "zod/v4";
import quickChatData from "../../resources/QuickChat.json" with { type: "json" };
import {
@@ -190,7 +191,7 @@ export const RequiredPatternSchema = z
.refine(
(val) => {
try {
new PatternDecoder(val);
new PatternDecoder(val, base64url.decode);
return true;
} catch (e) {
console.error(JSON.stringify(e.message, null, 2));
+2 -1
View File
@@ -1,3 +1,4 @@
import { base64url } from "jose";
import { Config } from "../configuration/Config";
import { PatternDecoder } from "../PatternDecoder";
import { ClientID, GameID, Player } from "../Schemas";
@@ -163,7 +164,7 @@ export class PlayerView {
this.decoder =
this.cosmetics.pattern === undefined
? undefined
: new PatternDecoder(this.cosmetics.pattern);
: new PatternDecoder(this.cosmetics.pattern, base64url.decode);
}
patternDecoder(): PatternDecoder | undefined {
+95 -2
View File
@@ -2,7 +2,10 @@ import { Cosmetics } from "../core/CosmeticSchemas";
import { PatternDecoder } from "../core/PatternDecoder";
export class PrivilegeChecker {
constructor(private cosmetics: Cosmetics) {}
constructor(
private cosmetics: Cosmetics,
private b64urlDecode: (base64: string) => Uint8Array,
) {}
isPatternAllowed(
base64: string,
@@ -14,7 +17,7 @@ export class PrivilegeChecker {
if (found === undefined) {
try {
// Ensure that the pattern will not throw for clients
new PatternDecoder(base64);
new PatternDecoder(base64, this.b64urlDecode);
} catch (e) {
// Pattern is invalid
return "invalid";
@@ -55,4 +58,94 @@ export class PrivilegeChecker {
return "restricted";
}
isCustomFlagAllowed(
flag: string,
roles: readonly string[] | undefined,
flares: readonly string[] | undefined,
): true | "restricted" | "invalid" {
if (!flag.startsWith("!")) return "invalid";
const code = flag.slice(1);
if (!code) return "invalid";
const segments = code.split("_");
if (segments.length === 0) return "invalid";
const MAX_LAYERS = 6; // Maximum number of layers allowed
if (segments.length > MAX_LAYERS) return "invalid";
const superFlare = flares?.includes("flag:*") ?? false;
for (const segment of segments) {
const [layerKey, colorKey] = segment.split("-");
if (!layerKey || !colorKey) return "invalid";
const layer = this.cosmetics.flag.layers[layerKey];
const color = this.cosmetics.flag.color[colorKey];
if (!layer || !color) return "invalid";
// Super-flare bypasses all restrictions
if (superFlare) {
continue;
}
// Check layer restrictions
const layerSpec = layer;
let layerAllowed = false;
if (!layerSpec.role_group && !layerSpec.flares) {
layerAllowed = true;
} else {
// By role
if (layerSpec.role_group) {
const allowedRoles =
this.cosmetics.role_groups[layerSpec.role_group] || [];
if (roles?.some((r) => allowedRoles.includes(r))) {
layerAllowed = true;
}
}
// By flare
if (
layerSpec.flares &&
flares?.some((f) => layerSpec.flares?.includes(f))
) {
layerAllowed = true;
}
// By named flag:layer:{name}
if (flares?.includes(`flag:layer:${layerSpec.name}`)) {
layerAllowed = true;
}
}
// Check color restrictions
const colorSpec = color;
let colorAllowed = false;
if (!colorSpec.role_group && !colorSpec.flares) {
colorAllowed = true;
} else {
// By role
if (colorSpec.role_group) {
const allowedRoles =
this.cosmetics.role_groups[colorSpec.role_group] || [];
if (roles?.some((r) => allowedRoles.includes(r))) {
colorAllowed = true;
}
}
// By flare
if (
colorSpec.flares &&
flares?.some((f) => colorSpec.flares?.includes(f))
) {
colorAllowed = true;
}
// By named flag:color:{name}
if (flares?.includes(`flag:color:${colorSpec.name}`)) {
colorAllowed = true;
}
}
// If either part is restricted, block
if (!(layerAllowed && colorAllowed)) {
return "restricted";
}
}
return true;
}
}
+14 -2
View File
@@ -2,6 +2,7 @@ import express, { NextFunction, Request, Response } from "express";
import rateLimit from "express-rate-limit";
import http from "http";
import ipAnonymize from "ip-anonymize";
import { base64url } from "jose";
import path from "path";
import { fileURLToPath } from "url";
import { WebSocket, WebSocketServer } from "ws";
@@ -44,7 +45,7 @@ export function startWorker() {
const gm = new GameManager(config, log);
const privilegeChecker = new PrivilegeChecker(COSMETICS);
const privilegeChecker = new PrivilegeChecker(COSMETICS, base64url.decode);
if (config.otelEnabled()) {
initWorkerMetrics(gm);
@@ -369,7 +370,18 @@ export function startWorker() {
// Check if the flag is allowed
if (clientMsg.flag !== undefined) {
// TODO: Implement custom flag validation
if (clientMsg.flag.startsWith("!")) {
const allowed = privilegeChecker.isCustomFlagAllowed(
clientMsg.flag,
roles,
flares,
);
if (allowed !== true) {
log.warn(`Custom flag ${allowed}: ${clientMsg.flag}`);
ws.close(1002, `Custom flag ${allowed}`);
return;
}
}
}
// Check if the pattern is allowed
+188
View File
@@ -0,0 +1,188 @@
import type { Cosmetics } from "../../src/core/CosmeticSchemas";
import { PrivilegeChecker } from "../../src/server/Privilege";
describe("PrivilegeChecker.isCustomFlagAllowed (with mock cosmetics)", () => {
const dummyPatternDecoder = (_base64: string) => {
throw new Error("Method not implemented");
};
const mockCosmetics: Cosmetics = {
role_groups: {
donor: ["role_donor"],
admin: ["role_admin"],
},
patterns: {},
flag: {
layers: {
a: {
name: "chocolate",
role_group: "donor",
flares: ["cosmetic:flags"],
},
b: { name: "center_hline" },
c: { name: "admin_layer", role_group: "admin" },
},
color: {
a: { color: "#ff0000", name: "red", role_group: "admin" },
b: { color: "#00ff00", name: "green" },
c: { color: "#0000ff", name: "blue", flares: ["cosmetic:blue"] },
},
},
};
const checker = new PrivilegeChecker(mockCosmetics, dummyPatternDecoder);
it("allowed: unrestricted layer/color", () => {
expect(checker.isCustomFlagAllowed("!b-b", [], [])).toBe(true);
});
it("restricted: donor layer without role", () => {
expect(checker.isCustomFlagAllowed("!a-b", [], [])).toBe("restricted");
});
it("allowed: donor layer with donor role", () => {
expect(checker.isCustomFlagAllowed("!a-b", ["role_donor"], [])).toBe(true);
});
it("allowed: donor layer with correct flare", () => {
expect(checker.isCustomFlagAllowed("!a-b", [], ["cosmetic:flags"])).toBe(
true,
);
});
it("restricted: admin color without role", () => {
expect(checker.isCustomFlagAllowed("!b-a", [], [])).toBe("restricted");
});
it("allowed: admin color with admin role", () => {
expect(checker.isCustomFlagAllowed("!b-a", ["role_admin"], [])).toBe(true);
});
it("allowed: color with correct flare", () => {
expect(checker.isCustomFlagAllowed("!b-c", [], ["cosmetic:blue"])).toBe(
true,
);
});
it("invalid: non-existent layer", () => {
expect(checker.isCustomFlagAllowed("!zzz-a", ["role_donor"], [])).toBe(
"invalid",
);
});
it("invalid: non-existent color", () => {
expect(checker.isCustomFlagAllowed("!a-zzz", ["role_donor"], [])).toBe(
"invalid",
);
});
it("allowed: superFlare allows all listed", () => {
expect(checker.isCustomFlagAllowed("!a-a", [], ["flag:*"])).toBe(true);
expect(checker.isCustomFlagAllowed("!b-b", [], ["flag:*"])).toBe(true);
expect(checker.isCustomFlagAllowed("!c-a", [], ["flag:*"])).toBe(true);
expect(checker.isCustomFlagAllowed("!a-c", [], ["flag:*"])).toBe(true);
});
it("invalid: superFlare does not allow non-existent", () => {
expect(checker.isCustomFlagAllowed("!zzz-zzz", [], ["flag:*"])).toBe(
"invalid",
);
});
it("allowed: flare flag:layer:chocolate allows chocolate layer", () => {
expect(
checker.isCustomFlagAllowed("!a-b", [], ["flag:layer:chocolate"]),
).toBe(true);
});
it("allowed: flare flag:color:blue allows blue color", () => {
expect(checker.isCustomFlagAllowed("!b-c", [], ["flag:color:blue"])).toBe(
true,
);
});
it("restricted: only color flare, layer still restricted", () => {
expect(checker.isCustomFlagAllowed("!a-c", [], ["cosmetic:blue"])).toBe(
"restricted",
);
});
it("restricted: only layer flare, color still restricted", () => {
expect(checker.isCustomFlagAllowed("!c-a", [], ["cosmetic:flags"])).toBe(
"restricted",
);
});
it("allowed: layer by role, color by flare", () => {
// layer a: role_group donor, color c: flares ["cosmetic:blue"]
expect(
checker.isCustomFlagAllowed("!a-c", ["role_donor"], ["cosmetic:blue"]),
).toBe(true);
});
it("restricted: layer by role, color by flare (missing flare)", () => {
expect(checker.isCustomFlagAllowed("!a-c", ["role_donor"], [])).toBe(
"restricted",
);
});
it("restricted: layer by role, color by flare (missing role)", () => {
expect(checker.isCustomFlagAllowed("!a-c", [], ["cosmetic:blue"])).toBe(
"restricted",
);
});
it("allowed: layer by flare, color by role", () => {
// layer a: flares ["cosmetic:flags"], color a: role_group admin
expect(
checker.isCustomFlagAllowed("!a-a", ["role_admin"], ["cosmetic:flags"]),
).toBe(true);
});
it("restricted: layer by flare, color by role (missing flare)", () => {
expect(checker.isCustomFlagAllowed("!a-a", ["role_admin"], [])).toBe(
"restricted",
);
});
it("restricted: layer by flare, color by role (missing role)", () => {
expect(checker.isCustomFlagAllowed("!a-a", [], ["cosmetic:flags"])).toBe(
"restricted",
);
});
it("allowed: two segments, both unrestricted", () => {
expect(checker.isCustomFlagAllowed("!b-b_b-b", [], [])).toBe(true);
});
it("restricted: two segments, one restricted by layer role", () => {
expect(checker.isCustomFlagAllowed("!a-b_b-b", [], [])).toBe("restricted");
expect(checker.isCustomFlagAllowed("!a-b_b-b", ["role_donor"], [])).toBe(
true,
);
});
it("restricted: two segments, one restricted by color role", () => {
expect(checker.isCustomFlagAllowed("!b-a_b-b", [], [])).toBe("restricted");
expect(checker.isCustomFlagAllowed("!b-a_b-b", ["role_admin"], [])).toBe(
true,
);
});
it("allowed: two segments, one by role, one by flare", () => {
expect(
checker.isCustomFlagAllowed(
"!a-c_b-b",
["role_donor"],
["cosmetic:blue"],
),
).toBe(true);
expect(checker.isCustomFlagAllowed("!a-c_b-b", ["role_donor"], [])).toBe(
"restricted",
);
expect(checker.isCustomFlagAllowed("!a-c_b-b", [], ["cosmetic:blue"])).toBe(
"restricted",
);
});
it("allowed: two segments, both by flare", () => {
expect(
checker.isCustomFlagAllowed(
"!a-c_a-c",
[],
["cosmetic:flags", "cosmetic:blue"],
),
).toBe(true);
expect(
checker.isCustomFlagAllowed("!a-c_a-c", [], ["cosmetic:flags"]),
).toBe("restricted");
expect(checker.isCustomFlagAllowed("!a-c_a-c", [], ["cosmetic:blue"])).toBe(
"restricted",
);
});
});