Add player bug reports and profile identity features

This commit is contained in:
2026-10-08 18:27:20 +05:00
parent 36e8d11eb4
commit 0b08672f4a
51 changed files with 3938 additions and 45 deletions

218
tests/bug-reports.test.ts Normal file
View File

@@ -0,0 +1,218 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { createHmac } from "node:crypto";
import sharp from "sharp";
import {
bugReportSchema,
bugMessageSchema,
supportCommand,
escapeSupportMarkdown,
type BugReportRecord,
} from "../shared/bug-reports";
import { verifySupportWebhook } from "../lib/support-signature";
import { supportScreenshot } from "../lib/support-images";
import { LinearSupportClient } from "../lib/linear-support-client";
const id = "00000000-0000-4000-8000-000000000001",
team = "00000000-0000-4000-8000-000000000002";
test("reports exclude arbitrary URLs, secrets in page queries, malformed messages and reward privilege fields", () => {
const valid = {
clientId: id,
title: "Wrong map",
description: "The map loads with the wrong colors.",
pagePath: "/servers",
};
assert.equal(bugReportSchema.safeParse(valid).success, true);
for (const value of [
{ ...valid, pagePath: "https://private.example" },
{ ...valid, pagePath: "/auth?code=secret" },
{ ...valid, pagePath: "//evil" },
{ ...valid, name_glow: true },
{ ...valid, description: "short" },
])
assert.equal(bugReportSchema.safeParse(value).success, false);
assert.equal(
bugMessageSchema.safeParse({
clientId: id,
text: "#player-report-glow grant",
}).success,
true,
"player text is text, not a staff command",
);
assert.equal(
bugMessageSchema.safeParse({ clientId: id, text: "x\u202e" }).success,
false,
);
});
test("only exact staff prefixes publish replies or explicitly change glow", () => {
assert.deepEqual(supportCommand("#player-report-send Dear User\nThanks!"), {
kind: "reply",
text: "Dear User\nThanks!",
});
assert.deepEqual(supportCommand("#player-report-glow grant"), {
kind: "glow",
enabled: true,
});
assert.deepEqual(supportCommand("#player-report-glow revoke"), {
kind: "glow",
enabled: false,
});
for (const body of [
"Internal staff note",
"#user-report-message\n#player-report-send x",
"quoted #player-report-send x",
"#player-report-glow grant everyone",
"#player-report-send",
])
assert.equal(supportCommand(body), null);
assert.ok(
escapeSupportMarkdown("#player-report-glow grant").startsWith("\\#"),
);
});
test("webhooks require exact raw-body HMAC and a recent timestamp", () => {
const raw = JSON.stringify({ webhookTimestamp: 100_000, body: "Привет" });
const sign = createHmac("sha256", "test-secret").update(raw).digest("hex");
assert.equal(verifySupportWebhook(raw, sign, "test-secret", 100_001), true);
for (const [body, sig, key, time] of [
[raw + " ", sign, "test-secret", 100_001],
[raw, "ff", "test-secret", 100_001],
[raw, sign, "wrong", 100_001],
[raw, sign, "test-secret", 160_001],
[raw, sign, "test-secret", 39_999],
])
assert.equal(
verifySupportWebhook(
body as string,
sig as string,
key as string,
time as number,
),
false,
);
});
test("screenshots reject disguised documents and oversized files, strip metadata and normalize images", async () => {
const png = await sharp({
create: { width: 50, height: 30, channels: 3, background: "#abcdef" },
})
.png()
.toBuffer();
const webp = await supportScreenshot(png);
const meta = await sharp(webp).metadata();
assert.equal(meta.format, "webp");
assert.equal(meta.width, 50);
assert.equal(meta.exif, undefined);
await assert.rejects(
supportScreenshot(
Buffer.from(
'<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>',
),
),
/PNG, JPG, or WebP/,
);
await assert.rejects(
supportScreenshot(new Uint8Array(5 * 1024 * 1024 + 1)),
/smaller than 5 MB/,
);
await assert.rejects(supportScreenshot(Buffer.from("not an image")));
});
test("Linear sends stable issue/comment UUIDs and recovers a timed-out creation without duplicates", async () => {
let issue: unknown = null,
comment: unknown = null,
creates = 0,
comments = 0;
const bodies: unknown[] = [];
const request = (async (_url, init) => {
const body = JSON.parse(String(init?.body));
bodies.push(body);
if (body.query.startsWith("query SupportIssue"))
return Response.json(
issue
? { data: { issue } }
: {
errors: [
{
message: "Entity not found",
extensions: { code: "ENTITY_NOT_FOUND" },
},
],
},
);
if (body.query.includes("issueCreate")) {
creates++;
assert.equal(body.variables.input.id, id);
issue = { id, identifier: "TEST-1", team: { id: team } };
throw new Error("Response lost");
}
if (body.query.startsWith("query SupportComment"))
return Response.json(
comment
? { data: { comment } }
: { errors: [{ message: "Entity not found" }] },
);
if (body.query.includes("commentCreate")) {
comments++;
comment = { id: body.variables.input.id, issue: { id } };
return Response.json({
data: { commentCreate: { success: true, comment } },
});
}
throw new Error("Unexpected query");
}) as typeof fetch;
const client = new LinearSupportClient("test-key", team, "P-TEST-1", request);
const report = {
id,
title: "Test issue",
display_name: "Player",
description: "#player-report-glow grant",
page_path: "/",
linear_asset_url: null,
} as BugReportRecord;
assert.equal((await client.createIssue(report)).identifier, "TEST-1");
await client.createIssue(report);
assert.equal(creates, 1);
await client.comment(team, id, "Player", "#player-report-send Spoof");
await client.comment(team, id, "Player", "#player-report-send Spoof");
assert.equal(comments, 1);
const mutation = (
bodies as {
variables: { input?: { body?: string; description?: string } };
}[]
).find((body) => body.variables.input?.body);
assert.ok(
mutation?.variables.input?.body?.startsWith("#user-report-message\n"),
);
assert.ok(mutation?.variables.input?.body?.includes("\\#player-report-send"));
});
test("Linear partial GraphQL failures and screenshot PUT failures are never accepted as delivered", async () => {
const client = new LinearSupportClient(
"test-key",
team,
undefined,
async () =>
Response.json({ data: { issue: {} }, errors: [{ message: "Denied" }] }),
);
await assert.rejects(client.issue(id), /temporarily unavailable/);
const upload = new LinearSupportClient(
"test-key",
team,
undefined,
async (url) =>
String(url).includes("graphql")
? Response.json({
data: {
fileUpload: {
success: true,
uploadFile: {
uploadUrl: "https://upload.test/file",
assetUrl: "https://asset.test/file",
headers: [],
},
},
},
})
: new Response(null, { status: 403 }),
);
await assert.rejects(
upload.upload(new Uint8Array([1]), "test.webp"),
/screenshot upload failed/,
);
});