diff --git a/src/server/routes/collab.ts b/src/server/routes/collab.ts index 2eae164..9a58199 100644 --- a/src/server/routes/collab.ts +++ b/src/server/routes/collab.ts @@ -4,7 +4,12 @@ import { getSessionFromCookie } from "../../atproto/session.ts"; import { canEdit, getAccessLevel } from "../../lib/access.ts"; import { resolveHandleToDid } from "../../lib/profile.ts"; import { closeConn, openConn, recvMessage } from "../collab/manager.ts"; -import { getMemberRole, getNoteBySlug, getWiki } from "../db/queries/index.ts"; +import { + getMemberRole, + getNoteBySlug, + getWiki, + isWikiHidden, +} from "../db/queries/index.ts"; /** * WebSocket transport for live collaborative editing. One connection per editor; @@ -51,7 +56,7 @@ export function isSameOrigin(request: Request): boolean { } } -async function resolveCollabAccess( +export async function resolveCollabAccess( handle: string, wikiSlug: string, noteSlug: string, @@ -61,6 +66,9 @@ async function resolveCollabAccess( if (!ownerDid) return null; const wiki = getWiki(ownerDid, wikiSlug); if (!wiki) return null; + // A takedown 410s every HTTP route, so it has to refuse the upgrade too. + // Connect-time only: a socket already open outlives it until it reconnects. + if (isWikiHidden(wiki)) return null; const note = getNoteBySlug(wiki.at_uri, noteSlug); if (!note) return null; const role = getMemberRole(wiki.at_uri, did); diff --git a/tests/integration/moderation.test.ts b/tests/integration/moderation.test.ts index f92731e..bcac79f 100644 --- a/tests/integration/moderation.test.ts +++ b/tests/integration/moderation.test.ts @@ -6,6 +6,7 @@ import { unbanDid, unhideWiki, } from "../../src/server/db/queries/index.ts"; +import { resolveCollabAccess } from "../../src/server/routes/collab.ts"; import { cleanupWikiAndDependents } from "../helpers/cleanup.ts"; import { ALICE, @@ -60,6 +61,19 @@ describe("wiki takedown via hideWiki", () => { expect((await fetch("GET", wikiUrl, { cookie })).status).toBe(410); }); + test("the collab socket refuses a hidden wiki", async () => { + // The editor's transport never reaches resolveRequestContext, so it has + // to re-check moderation itself or it hands out the note body. + const connect = () => + resolveCollabAccess(ALICE.handle, WIKI_SLUG, NOTE_SLUG, ALICE.did); + + unhideWiki(wikiAtUri); + expect(await connect()).not.toBeNull(); + + hideWiki(wikiAtUri, "abuse", "test"); + expect(await connect()).toBeNull(); + }); + test("unhide restores access", async () => { unhideWiki(wikiAtUri); expect((await fetch("GET", wikiUrl)).status).toBe(200);