pike/glyph Public Public: anyone can read this; nobody can change it.
forked from apps/glyph
workbenchthis machine
/events leaks every user's drawings to every signed-in viewer (#4)
Requested-by: benjivers <[email protected]> Co-authored-by: benjivers <[email protected]>
3 files
changed +40
−3
src/actions.ts +1 −1
| @@ -149,7 +149,7 @@ function start( | ||
| 149 | 149 | "INSERT INTO drawing (prompt, kind, author_id, author_name) VALUES (?1, ?2, ?3, ?4) RETURNING *", |
| 150 | 150 | ) |
| 151 | 151 | .get(prompt, kind, who.id, who.name)!; |
| 152 | emit("drawing.added", drawing); | |
| 152 | emit("drawing.added", { id: drawing.id, status: drawing.status }); | |
| 153 | 153 | kick(db, drawing.id, prompt, kind); |
| 154 | 154 | return ok(drawing); |
| 155 | 155 | } |
src/draw.ts +2 −2
| @@ -55,7 +55,7 @@ export const settled = (): Promise<unknown> => Promise.all([...running]); | ||
| 55 | 55 | /** Fill in drawing `id` in the background. */ |
| 56 | 56 | export function kick(db: Database, id: number, prompt: string, kind: Kind): void { |
| 57 | 57 | const job = (async () => { |
| 58 | let row: unknown; | |
| 58 | let row: { id: number; status: string } | null; | |
| 59 | 59 | try { |
| 60 | 60 | const svgs = await drawer(prompt, kind); |
| 61 | 61 | row = db |
| @@ -67,7 +67,7 @@ export function kick(db: Database, id: number, prompt: string, kind: Kind): void | ||
| 67 | 67 | .query("UPDATE drawing SET status = 'failed', error = ?1, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?2 AND status = 'drawing' RETURNING *") |
| 68 | 68 | .get(message.slice(0, 500), id); |
| 69 | 69 | } |
| 70 | if (row !== null) emit("drawing.changed", row); | |
| 70 | if (row !== null) emit("drawing.changed", { id: row.id, status: row.status }); | |
| 71 | 71 | })().catch((error) => console.error("drawing", id, error)); |
| 72 | 72 | running.add(job); |
| 73 | 73 | void job.finally(() => running.delete(job)); |
src/server.test.ts +37
| @@ -394,3 +394,40 @@ test("what the web expects: robots.txt, and security.txt from the forge that run | ||
| 394 | 394 | ); |
| 395 | 395 | } |
| 396 | 396 | }); |
| 397 | ||
| 398 | test("another person's prompt and SVG never reach /events, live or replayed", async () => { | |
| 399 | const secret = "zq-private-prompt-7"; | |
| 400 | const live = await fetch(base + "/events", { headers: as("u2", "bob") }); | |
| 401 | const reader = live.body!.getReader(); | |
| 402 | let heard = ""; | |
| 403 | const pump = (async () => { | |
| 404 | for (;;) { | |
| 405 | const { value, done } = await reader.read(); | |
| 406 | if (done) return; | |
| 407 | heard += new TextDecoder().decode(value); | |
| 408 | } | |
| 409 | })(); | |
| 410 | await api( | |
| 411 | "/api/drawings", | |
| 412 | { method: "POST", ...jsonBody({ prompt: secret }) }, | |
| 413 | as("u1", "alice"), | |
| 414 | ); | |
| 415 | await settled(); | |
| 416 | await Bun.sleep(50); | |
| 417 | const replay = await (async () => { | |
| 418 | const late = await fetch(base + "/events?since=0", { headers: as("u2", "bob") }); | |
| 419 | const late_reader = late.body!.getReader(); | |
| 420 | const text = new TextDecoder().decode((await late_reader.read()).value); | |
| 421 | await late_reader.cancel(); | |
| 422 | return text; | |
| 423 | })(); | |
| 424 | await reader.cancel(); | |
| 425 | await pump; | |
| 426 | expect(heard).toContain("drawing.changed"); | |
| 427 | expect(replay).toContain("drawing.changed"); | |
| 428 | for (const text of [heard, replay]) { | |
| 429 | expect(text).not.toContain(secret); | |
| 430 | expect(text).not.toContain("<svg"); | |
| 431 | expect(text).not.toContain("Test Mark"); | |
| 432 | } | |
| 433 | }); |