// node --test web/agents/agents-cubit.test.mjs // The page cubit against a stubbed OAuth client (the session's PDS minting service auth) and a stubbed // agents service that checks each token's aud and lxm and scopes launches to their caller. import assert from "node:assert/strict"; import { registerHooks } from "node:module"; import { test } from "node:test"; registerHooks({ resolve(specifier, context, next) { const lib = specifier.startsWith("atlibs/") && new URL(`../../../atlibs/javascript/src/${specifier.slice("atlibs/".length)}`, import.meta.url).href; return next(lib || specifier, context); }, }); const { AgentsCubit, DEFAULT_SERVICE, NSID, SCOPE } = await import("./agents-cubit.js"); const BOB = "did:plc:ocnuqjlzgohypj3zoznermms"; const SERVICE = "https://did-key-zdnaetest.xrpc.fedproxy.com"; const json = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); function memoryStorage() { const m = new Map(); return { getItem: (k) => (m.has(k) ? m.get(k) : null), setItem: (k, v) => void m.set(k, String(v)), map: m }; } // The PDS: getServiceAuth returns a token naming aud and lxm (the stub service reads them back). function session(did = BOB) { const minted = []; const agent = { did, com: { atproto: { server: { getSession: async () => ({ success: true, data: { handle: "bob.test" } }) } } }, async fetchHandler(path) { const [nsid, query] = path.replace("/xrpc/", "").split("?"); assert.equal(nsid, "com.atproto.server.getServiceAuth"); const { aud, lxm } = Object.fromEntries(new URLSearchParams(query)); minted.push({ aud, lxm }); return json(200, { token: `${did}|${aud}|${lxm}` }); }, }; const client = { init: async () => ({ session: {} }), signIn: async () => undefined, revoke: async () => undefined }; return { agent, client, minted }; } // The service: verifies the token's aud/lxm, records caller, answers only the caller's launches. function service() { const launches = new Map(); const calls = []; let n = 0; const fetch = async (url, init) => { const u = new URL(url); const nsid = u.pathname.replace("/xrpc/", ""); const [caller, aud, lxm] = (init.headers.authorization ?? "").replace("Bearer ", "").split("|"); calls.push({ nsid, method: init.method, caller, body: init.body && JSON.parse(init.body) }); if (aud !== `did:web:${u.hostname}${u.port ? `%3A${u.port}` : ""}` || lxm !== nsid) return json(401, { error: "AuthenticationRequired", message: "bad token" }); const own = (rkey) => { const l = launches.get(rkey); if (!l) return json(400, { error: "InvalidRequest", message: "reading the launch record: RecordNotFound" }); if (l.caller !== caller) return json(403, { error: "Forbidden", message: `launch ${rkey} belongs to another caller` }); }; if (nsid === NSID.launch) { const rkey = `3mzz${++n}`; launches.set(rkey, { caller, status: "running", name: JSON.parse(init.body).name }); return json(200, { rkey, uri: `at://svc/com.fedfork.agents.launch/${rkey}`, status: "launching" }); } if (nsid === NSID.get) { const rkey = u.searchParams.get("rkey"); const denied = own(rkey); if (denied) return denied; return json(200, { rkey, uri: `at://svc/com.fedfork.agents.launch/${rkey}`, record: { ...launches.get(rkey), subhandle: `x-${rkey}.svc.test` } }); } if (nsid === NSID.shutdown) { const { rkey, delete: remove } = JSON.parse(init.body); const denied = own(rkey); if (denied) return denied; if (remove) launches.delete(rkey); else launches.get(rkey).status = "stopped"; return json(200, { rkey, status: "stopped", revoked: true, terminated: true, deleted: remove ? ["a", "b"] : [] }); } return json(404, { error: "MethodNotImplemented" }); }; return { fetch, launches, calls }; } async function page({ storage = memoryStorage(), svc = service(), did = BOB } = {}) { const s = session(did); const cubit = new AgentsCubit({ client: Promise.resolve(s.client), makeAgent: () => s.agent, fetch: svc.fetch, storage, service: SERVICE }); await cubit.init(); return { cubit, svc, storage, minted: s.minted }; } test("the OAuth scope is only service auth for the three methods", () => { assert.equal(SCOPE, "atproto rpc:com.fedfork.agents.launchAgent?aud=* rpc:com.fedfork.agents.getAgent?aud=* rpc:com.fedfork.agents.shutdownAgent?aud=*"); assert.match(DEFAULT_SERVICE, /^https:\/\/did-key-z[a-z0-9]+\.xrpc\.fedproxy\.com$/); }); test("launch through the service with a PDS-minted token; the page lists it, reads it, shuts it down", async () => { const { cubit, svc, minted } = await page(); assert.equal(cubit.state.session, "signed-in"); const launched = await cubit.launch({ bidders: "did:plc:4vcig7s43z3nyqrnepmcfzqt", name: "page", exp_s: "600" }); assert.equal(launched.status, "launching"); assert.deepEqual(svc.calls[0], { nsid: NSID.launch, method: "POST", caller: BOB, body: { bidders: "did:plc:4vcig7s43z3nyqrnepmcfzqt", name: "page", exp_s: 600 } }); assert.deepEqual(minted[0], { aud: "did:web:did-key-zdnaetest.xrpc.fedproxy.com", lxm: NSID.launch }); assert.deepEqual(cubit.state.agents.map((a) => [a.rkey, a.record.status]), [[launched.rkey, "running"]]); assert.equal(cubit.state.busy.launch, null); const status = await cubit.status(launched.rkey); assert.equal(status.record.subhandle, `x-${launched.rkey}.svc.test`); const down = await cubit.shutdown(launched.rkey, true); assert.equal(down.status, "stopped"); assert.deepEqual(cubit.state.agents, [], "deleted on the service: forgotten here"); assert.ok(minted.every(({ aud }) => aud === "did:web:did-key-zdnaetest.xrpc.fedproxy.com")); assert.deepEqual([...new Set(minted.map((m) => m.lxm))].sort(), Object.values(NSID).sort()); }); test("a reload lists what this browser launched for this account; another account's page sees none of it", async () => { const storage = memoryStorage(); const svc = service(); const first = await page({ storage, svc }); const { rkey } = await first.cubit.launch({ name: "keep" }); const again = await page({ storage, svc }); assert.deepEqual(again.cubit.state.agents.map((a) => a.rkey), [rkey]); const other = await page({ storage, svc, did: "did:plc:mallory0000000000000000" }); assert.deepEqual(other.cubit.state.agents, []); const denied = await other.cubit.shutdown(rkey); assert.match(denied.error, /^403 Forbidden: launch 3mzz1 belongs to another caller/); assert.equal(svc.launches.get(rkey).status, "running"); }); test("service errors keep their status; setService needs https and switches the list", async () => { const svc = service(); const { cubit } = await page({ svc }); const bad = await cubit.status("nope"); assert.match(bad.error, /^400 InvalidRequest: reading the launch record/); await assert.rejects(() => cubit.setService("http://insecure.example"), /https origin/); await cubit.launch({ name: "a" }); await cubit.setService("https://did-key-zdnaeother.xrpc.fedproxy.com/xrpc/whatever"); assert.equal(cubit.state.service, "https://did-key-zdnaeother.xrpc.fedproxy.com"); assert.deepEqual(cubit.state.agents, [], "launches are remembered per service"); }); // review 3a L5: the aud was did:web:, the port dropped; serviceAud keeps it (%3A). test("a service on a port gets tokens for did:web:%3A", async () => { const { cubit, minted } = await page(); await cubit.setService("https://svc.example:8443"); const launched = await cubit.launch({ name: "p" }); assert.equal(launched.status, "launching", launched.error); assert.deepEqual(minted.find((m) => m.lxm === NSID.launch && m.aud.startsWith("did:web:svc")), { aud: "did:web:svc.example%3A8443", lxm: NSID.launch }); }); test("signed out: no calls; logout keeps the service setting", async () => { const svc = service(); const cubit = new AgentsCubit({ client: Promise.resolve({ init: async () => undefined }), makeAgent: () => null, fetch: svc.fetch, storage: memoryStorage(), service: SERVICE }); await cubit.init(); assert.equal(cubit.state.session, "signed-out"); assert.match((await cubit.launch({ name: "x" })).error, /needs a signed-in session/); assert.equal(svc.calls.length, 0); const { cubit: signedIn } = await page(); await signedIn.logout(); assert.deepEqual([signedIn.state.session, signedIn.state.service], ["signed-out", SERVICE]); });