import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import worker, { type Env } from "./index"; import { DEVICE_TOKEN_TTL_SEC, mintDeviceToken } from "./device-token"; import { clearMetadataCache } from "./metadata"; const SECRET = "integration-secret-at-least-32-chars"; const APP = "https://your-app.example"; const OTHER_APP = "https://other-app.example"; const CALLBACK = "example.your-app://loggedin"; const DID = "did:plc:z72i7hdynmk6r22z27h6tvur"; type Row = { did: string; origin: string }; /** In-memory stand-in for the Durable Object, keyed by session id. */ function makeEnv(): Env & { rows: Map } { const rows = new Map(); const stubFor = (name: string) => ({ async list() { const byDid = new Map(); for (const row of rows.get(name) ?? []) { const origins = byDid.get(row.did) ?? []; if (!origins.includes(row.origin)) origins.push(row.origin); byDid.set(row.did, origins); } return { accounts: [...byDid].map(([did, origins]) => ({ did, origins })) }; }, async loggedIn(did: string, origin: string) { const existing = rows.get(name) ?? []; if (!existing.some((row) => row.did === did && row.origin === origin)) { existing.push({ did, origin }); } rows.set(name, existing); return { ok: true }; }, async logout(did: string, origin: string) { rows.set( name, (rows.get(name) ?? []).filter((row) => !(row.did === did && row.origin === origin)), ); return { ok: true }; }, }); return { rows, DEVICE_TOKEN_SECRET: SECRET, SESSIONS: { idFromName: (name: string) => name, get: (id: string) => stubFor(id), }, } as unknown as Env & { rows: Map }; } function metadataFor(origin: string, redirectUris: string[]): Response { return new Response( JSON.stringify({ client_id: `${origin}/oauth-client-metadata.json`, client_name: "Your App", client_uri: origin, redirect_uris: redirectUris, }), { status: 200, headers: { "content-type": "application/json" } }, ); } beforeEach(() => { clearMetadataCache(); vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url === `${APP}/oauth-client-metadata.json`) { return metadataFor(APP, [CALLBACK, `${APP}/oauth/callback`]); } if (url === `${OTHER_APP}/oauth-client-metadata.json`) { return metadataFor(OTHER_APP, [`${OTHER_APP}/oauth/callback`]); } return new Response("not found", { status: 404 }); }), ); }); afterEach(() => { vi.unstubAllGlobals(); }); function startUrl(params: Record): string { const url = new URL("https://loggedin.fyi/app/start"); for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value); return url.toString(); } async function freshToken(sid: string, origin = APP): Promise { return mintDeviceToken(SECRET, { sid, origin, expiresAt: Math.floor(Date.now() / 1000) + DEVICE_TOKEN_TTL_SEC, }); } function tokenRequest(path: string, token: string, body?: unknown): Request { return new Request(`https://loggedin.fyi${path}`, { method: body ? "POST" : "GET", headers: { "X-Loggedin-Token": token, ...(body ? { "content-type": "application/json" } : {}), }, body: body ? JSON.stringify(body) : undefined, }); } describe("GET /app/start", () => { it("mints a token bound to the cookie session and embeds it in the page", async () => { const env = makeEnv(); const sid = "ab".repeat(16); const res = await worker.fetch( new Request(startUrl({ origin: APP, callback: CALLBACK, state: "xyz" }), { headers: { Cookie: `loggedin_session=${sid}` }, }), env, ); expect(res.status).toBe(200); const body = await res.text(); expect(body).toContain(CALLBACK); expect(body).toContain("xyz"); const token = /"(v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)"/.exec(body)?.[1]; expect(token).toBeTruthy(); // The token must address the same Durable Object the browser cookie does. const write = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.loggedIn", token!, { did: DID }), env, ); expect(write.status).toBe(200); expect(env.rows.get(sid)).toEqual([{ did: DID, origin: APP }]); }); it("does not let a crafted state break out of the inline script", async () => { const res = await worker.fetch( new Request( startUrl({ origin: APP, callback: CALLBACK, state: "", }), ), makeEnv(), ); expect(res.status).toBe(200); const body = await res.text(); expect(body).not.toContain("