import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdtemp, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; import { atlibs } from "../../../rust/host/conformance.mjs"; import { appRepo, appRepoAnswer, appRepoHandler, clientId, clientMetadata, clientMetadataHandler, composeRecord, pageClientId, enabledFeatures, grantedSessions, policyCall, requestScope, scopeAllows } from "./index.js"; const T = "com.publicdomainrelay.temp"; const ATIRC = "did:web:irc.fedfork.com#atirc"; // Session records: requester and atirc as small inline fixtures (repo writes with scope groups; rpc through a // proxy), fedproxy is this repo's statusphere record. const fixture = (name, groups) => ({ $type: "com.fedfork.exec.oauth.clientMetadata", name, clientMetadata: { client_id: `https://${name}.example/oauth-client-metadata.json`, client_name: name, client_uri: `https://${name}.example`, redirect_uris: [`https://${name}.example`], scope: ["atproto", ...Object.values(groups).flat()].join(" ") }, scopeGroups: Object.entries(groups).map(([key, scopes]) => ({ key, label: key, scopes })), createdAt: "2026-11-08T00:00:00.000Z", }); const records = { requester: fixture("requester", { associate: [`repo:${T}.badgeBlueKeys?action=create`], market: [`repo:${T}.market.rfp?action=create&action=delete`, `rpc:${T}.market.submitRfp?aud=*`], signal: [`repo:${T}.compute.events.guest.signalWebRTC?action=create&action=delete`], }), atirc: fixture("atirc", { read: [`rpc:chat.atirc.list?aud=did:web:irc.fedfork.com%23atirc`], post: [`rpc:chat.atirc.post?aud=did:web:irc.fedfork.com%23atirc`] }), fedproxy: JSON.parse(await readFile(new URL("../../../records/statusphere.json", import.meta.url), "utf8")), }; const create = (collection) => ["com.atproto.repo.createRecord", { input: { repo: "did:plc:me", collection, record: {} } }]; test("composeRecord unions scopes and keys scope groups by session", () => { const composed = composeRecord({ requester: records.requester, atirc: records.atirc }, { name: "ui", clientId: "https://ui.example/oauth/ui/client-metadata.json", createdAt: "2026-11-08T00:00:00.000Z" }); const scopes = composed.clientMetadata.scope.split(" "); assert.equal(scopes[0], "atproto"); assert.equal(scopes.filter((s) => s === "atproto").length, 1); assert.ok(scopes.includes(`rpc:chat.atirc.post?aud=did:web:irc.fedfork.com%23atirc`)); assert.ok(scopes.includes(`repo:${T}.market.rfp?action=create&action=delete`)); assert.deepEqual(composed.scopeGroups.map((g) => g.key), ["requester.associate", "requester.market", "requester.signal", "atirc.read", "atirc.post"]); assert.deepEqual(composed.clientMetadata.redirect_uris, ["https://ui.example"]); }); test("clientMetadata rebases the template onto the site and forces a public DPoP client", () => { const doc = clientMetadata(records.fedproxy, { clientId: "https://site.example/oauth/fedproxy/client-metadata.json", redirectUri: "https://site.example/" }); assert.equal(doc.client_uri, "https://site.example"); assert.equal(doc.client_id, "https://site.example/oauth/fedproxy/client-metadata.json"); assert.deepEqual(doc.redirect_uris, ["https://site.example/"]); assert.equal(doc.token_endpoint_auth_method, "none"); assert.equal(doc.dpop_bound_access_tokens, true); assert.throws(() => clientMetadata({ name: "x", clientMetadata: { client_id: "https://x.example/c.json", scope: "repo:a.b.c" } }, { clientId: "https://s.example/c", redirectUri: "https://s.example/" }), /atproto/); }); test("clientId is the staged id online and the loopback client on localhost", () => { const client = { clientId: "https://ui.example/oauth/ui/client-metadata.json", scope: "atproto rpc:chat.atirc.list?aud=*" }; assert.equal(clientId(client, new URL("https://ui.example/")), client.clientId); const loopback = new URL(clientId(client, new URL("http://localhost:8000/ui/index.html"))); assert.equal(loopback.origin, "http://localhost"); assert.equal(loopback.searchParams.get("scope"), client.scope); assert.equal(loopback.searchParams.get("redirect_uri"), "http://127.0.0.1:8000/ui/"); }); test("scopeAllows maps calls to repo and rpc permissions", () => { const scope = records.requester.clientMetadata.scope; assert.ok(scopeAllows(scope, ...create(`${T}.market.rfp`))); assert.ok(!scopeAllows(scope, ...create("com.fedproxy.sshPublicKey"))); assert.ok(scopeAllows(scope, "com.atproto.repo.deleteRecord", { input: { collection: `${T}.market.rfp` } }), "the contract cleans up its records"); assert.ok(!scopeAllows(scope, "com.atproto.repo.deleteRecord", { input: { collection: `${T}.market.bid` } })); assert.ok(!scopeAllows(scope, "com.atproto.repo.putRecord", { input: { collection: `${T}.market.rfp` } }), "put needs create and update"); assert.ok(scopeAllows(`atproto repo:${T}.market.rfp`, "com.atproto.repo.putRecord", { input: { collection: `${T}.market.rfp` } })); assert.ok(scopeAllows(scope, `${T}.market.submitRfp`, { input: {}, service: "https://bidder.example:8443" })); assert.ok(scopeAllows("atproto", "com.atproto.repo.listRecords", { params: {} }), "reads need no scope"); assert.ok(scopeAllows("atproto", "com.atproto.repo.getRecord", { params: {}, service: "did:plc:bidder" })); assert.ok(scopeAllows("atproto", "com.fedfork.exec.attest", { input: {} })); // getServiceAuth needs rpc:?aud= (the PDS's rule for an OAuth session) const cocore = "atproto rpc:dev.cocore.inference.dispatch?aud=did:web:cocore.dev"; const mint = (aud, lxm) => ["com.atproto.server.getServiceAuth", { params: { aud, lxm } }]; assert.ok(scopeAllows(cocore, ...mint("did:web:cocore.dev", "dev.cocore.inference.dispatch"))); assert.ok(!scopeAllows(cocore, ...mint("did:web:evil.example", "dev.cocore.inference.dispatch"))); assert.ok(!scopeAllows(cocore, ...mint("did:web:cocore.dev", "x.y.z"))); assert.ok(!scopeAllows("atproto rpc:com.atproto.server.getServiceAuth?aud=*", ...mint("did:web:cocore.dev", "x.y.z"))); assert.ok(scopeAllows("atproto", "com.fedfork.exec.webrtc.connect", { input: {} }), "host pseudo-calls need no scope"); const atirc = records.atirc.clientMetadata.scope; assert.ok(scopeAllows(atirc, "chat.atirc.post", { input: {}, proxy: ATIRC })); assert.ok(!scopeAllows(atirc, "chat.atirc.post", { input: {}, proxy: "did:web:evil.example#atirc" })); assert.ok(!scopeAllows(atirc, ...create(`${T}.market.rfp`))); assert.ok(scopeAllows(`atproto rpc?lxm=a.b.c&lxm=d.e.f&aud=*`, "d.e.f", { proxy: "did:web:x.example#svc" })); assert.ok(scopeAllows("atproto repo?collection=a.b.c&action=delete", "com.atproto.repo.applyWrites", { input: { writes: [{ $type: "com.atproto.repo.applyWrites#delete", collection: "a.b.c" }] } })); assert.ok(scopeAllows("atproto transition:generic", ...create("any.thing.here"))); assert.ok(!scopeAllows("atproto transition:generic", "chat.bsky.convo.sendMessage", { input: {} })); // a public subscribe mints no token; one with serviceAuth needs the rpc scope for its service assert.ok(scopeAllows("atproto", "com.atproto.sync.subscribeRepos", { service: "https://relay.example", subscribe: true })); assert.ok(!scopeAllows("atproto", "a.b.watch", { service: "https://svc.example", subscribe: true, serviceAuth: {} })); assert.ok(scopeAllows("atproto rpc:a.b.watch?aud=did:web:svc.example", "a.b.watch", { service: "https://svc.example", subscribe: true, serviceAuth: {} })); assert.ok(scopeAllows("atproto blob:*/*", "com.atproto.repo.uploadBlob", { input: {} }) && !scopeAllows("atproto", "com.atproto.repo.uploadBlob", { input: {} })); }); const policy = { sessions: { requester: { scope: records.requester.clientMetadata.scope }, atirc: { scope: records.atirc.clientMetadata.scope } }, modules: { requester: { session: "requester" }, atirc: { session: "atirc" } }, features: { agents: { requires: ["requester", "atirc"], grants: { agent: ["requester", "atirc"] } } }, }; test("one sign-in: requestScope asks for some sessions, grantedSessions reads them back", () => { const scope = requestScope(policy, ["atirc"]); assert.equal(scope, records.atirc.clientMetadata.scope); assert.deepEqual(grantedSessions(policy, scope), ["atirc"]); // a session's optional scope groups (stage.json required): one signed in before them stays connected const later = { sessions: { requester: { scope: "atproto repo:a?action=create repo:b?action=create", required: "atproto repo:a?action=create" } } }; assert.deepEqual(grantedSessions(later, "atproto repo:a?action=create"), ["requester"]); assert.deepEqual(grantedSessions(later, "atproto"), []); assert.deepEqual(grantedSessions(policy, requestScope(policy, ["atirc", "requester"])).sort(), ["atirc", "requester"]); assert.deepEqual(enabledFeatures(policy, ["atirc"]), []); assert.deepEqual(enabledFeatures(policy, ["atirc", "requester"]), ["agents"]); }); test("policyCall keeps each module in its own session until a feature grants more", async () => { const made = []; const session = (name) => async (nsid, request) => (made.push([name, nsid, request.proxy]), { ok: name }); const both = { requester: session("requester"), atirc: session("atirc") }; assert.deepEqual(await policyCall(policy, "atirc", both)("chat.atirc.post", { input: {}, proxy: ATIRC }), { ok: "atirc" }); await assert.rejects(policyCall(policy, "atirc", both)(...create(`${T}.market.rfp`)), /atirc may not call com.atproto.repo.createRecord with sessions atirc/); assert.deepEqual(await policyCall(policy, "requester", both)(...create(`${T}.market.rfp`)), { ok: "requester" }); // The agent module has no session of its own: with both connected the agents feature lends it both. const agent = policyCall(policy, "agent", both); assert.deepEqual(await agent(...create(`${T}.market.rfp`)), { ok: "requester" }); assert.deepEqual(await agent("chat.atirc.list", { params: {}, proxy: ATIRC }), { ok: "atirc" }); // With atirc alone the feature is off, so the agent has nothing. await assert.rejects(policyCall(policy, "agent", { atirc: both.atirc })("chat.atirc.list", { proxy: ATIRC }), /no connected session/); // A session that is not connected is never used, even for the module that owns it. await assert.rejects(policyCall(policy, "requester", { atirc: both.atirc })(...create(`${T}.market.rfp`)), /no connected session/); // Calls that need no scope go through any connected session, for a module with no session of its own too. assert.deepEqual(await policyCall(policy, "agent", { atirc: both.atirc })("com.atproto.repo.getRecord", { params: {} }), { ok: "atirc" }); await assert.rejects(policyCall(policy, "agent", {})("com.atproto.repo.getRecord", { params: {} }), /agent may not call com.atproto.repo.getRecord with no connected session/); assert.equal(made.length, 5); }); test("atlibs stage writes local modules and their client metadata at known paths", async () => { const out = await mkdtemp(join(tmpdir(), "atlibs-stage-")); const record = join(out, "ui.record.json"); const { stdout } = await atlibs(["stage", "--out", out, "--origin", "https://ui.example/app", "--name", "ui", "--record", record, "statusphere", "vibescrobble"]); const manifest = JSON.parse(stdout); const stagedRecord = JSON.parse(await readFile(new URL("../../../records/vibescrobble.json", import.meta.url), "utf8")); assert.deepEqual(JSON.parse(await readFile(join(out, "stage.json"), "utf8")), manifest); assert.equal(manifest.clientId, "https://ui.example/app/oauth/ui/client-metadata.json"); assert.deepEqual(Object.keys(manifest.sessions), ["statusphere", "vibescrobble"]); const doc = JSON.parse(await readFile(join(out, "oauth/ui/client-metadata.json"), "utf8")); assert.equal(doc.client_id, manifest.clientId); assert.equal(doc.scope, manifest.scope); assert.deepEqual(doc.redirect_uris, ["https://ui.example/app/"]); const vibescrobble = JSON.parse(await readFile(join(out, "oauth/vibescrobble/client-metadata.json"), "utf8")); assert.equal(vibescrobble.client_id, manifest.sessions.vibescrobble.clientId); assert.deepEqual(vibescrobble, clientMetadata(stagedRecord, { clientId: vibescrobble.client_id, redirectUri: "https://ui.example/app/" }), "the Rust twin of clientMetadata"); const wasm = await readFile(join(out, manifest.modules.vibescrobble.wasm)); assert.equal(createHash("sha256").update(wasm).digest("hex"), manifest.modules.vibescrobble.sha256); assert.deepEqual(JSON.parse(await readFile(join(out, "modules/vibescrobble.json"), "utf8")), JSON.parse(await readFile(new URL("../../../examples/rust/vibescrobble/module.json", import.meta.url), "utf8"))); assert.equal(JSON.parse(await readFile(record, "utf8")).name, "ui"); await assert.rejects(atlibs(["stage", "--out", out, "--origin", "http://ui.example", "vibescrobble"]), /must be https/); // --oauth-path moves the clients; --target server stages the records for clientMetadataHandler instead const moved = JSON.parse((await atlibs(["stage", "--out", out, "--origin", "https://ui.example/", "--oauth-path", "auth/clients/", "vibescrobble"])).stdout); assert.equal(moved.clientId, "https://ui.example/auth/clients/vibescrobble/client-metadata.json"); const server = JSON.parse((await atlibs(["stage", "--target", "server", "--out", join(out, "srv"), "vibescrobble"])).stdout); assert.deepEqual(server.sessions.vibescrobble.record, stagedRecord); }); test("clientMetadataHandler serves each session's metadata, and the composite, on origin (or a loopback request's)", async () => { const handler = clientMetadataHandler({ sessions: { atirc: { record: records.atirc }, fedproxy: { record: records.fedproxy } } }, { name: "ui" }); const get = (url, headers) => handler(new Request(url, { headers })); const doc = await get("http://127.0.0.1:8787/oauth/atirc/client-metadata.json").json(); assert.equal(doc.client_id, "http://127.0.0.1:8787/oauth/atirc/client-metadata.json"); assert.deepEqual(doc.redirect_uris, ["http://127.0.0.1:8787/"]); assert.equal(doc.scope, records.atirc.clientMetadata.scope); // a public Host (or X-Forwarded-*) is never trusted without origin assert.equal(get("http://site.example/oauth/fedproxy/client-metadata.json", { "x-forwarded-proto": "https" }).status, 500); const ui = await get("http://localhost:8787/oauth/ui/client-metadata.json").json(); assert.equal(ui.scope, composeRecord({ atirc: records.atirc, fedproxy: records.fedproxy }, { name: "ui", clientId: ui.client_id }).clientMetadata.scope); assert.equal(get("http://127.0.0.1/oauth/nope/client-metadata.json").status, 404); assert.equal(get("https://site.example/elsewhere"), undefined); assert.equal(handler(new Request("https://site.example/oauth/atirc/client-metadata.json", { method: "POST" })), undefined); const fixed = clientMetadataHandler({ atirc: records.atirc }, { path: "auth", origin: "https://fixed.example/" }); assert.equal((await fixed(new Request("http://x/auth/atirc/client-metadata.json")).json()).client_id, "https://fixed.example/auth/atirc/client-metadata.json"); }); test("pageClientId: loopback on localhost, the staged client when deployed, else the fallback", async () => { const at = (href) => Object.assign(new URL(href), { href }); const staged = async (url) => (String(url) === "https://site.example/app/stage.json" ? Response.json({ clientId: "https://site.example/app/oauth/ui/client-metadata.json" }) : new Response("", { status: 404 })); const local = await pageClientId("atproto repo:a", { location: at("http://localhost:8000/app/index.html"), fetch: staged }); assert.equal(new URL(local).searchParams.get("redirect_uri"), "http://127.0.0.1:8000/app/"); assert.equal(await pageClientId("atproto", { location: at("https://site.example/app/index.html"), fetch: staged }), "https://site.example/app/oauth/ui/client-metadata.json"); assert.equal(await pageClientId("atproto", { location: at("https://other.example/x/"), fetch: staged, fallback: "https://other.example/oauth-client-metadata.json" }), "https://other.example/oauth-client-metadata.json"); await assert.rejects(pageClientId("atproto", { location: at("https://other.example/x/"), fetch: staged }), /atlibs stage --target spa/); const template = async () => Response.json({ clientId: "https://atlibs.invalid/oauth/ui/client-metadata.json" }); await assert.rejects(pageClientId("atproto", { location: at("https://site.example/app/"), fetch: template }), /is a template/); }); // An app record: co/core's registration on the template origin (the record's client_id), served on another. const REGISTRATION = { $type: "dev.cocore.app.registration", name: "App", website: "https://app.example", returnUrls: ["https://app.example/done"], createdAt: "2026-11-10T00:00:00.000Z" }; const appRecord = { $type: "com.fedfork.exec.oauth.clientMetadata", name: "app", clientMetadata: { client_id: "https://app.example/oauth/app/client-metadata.json", scope: "atproto" }, createdAt: "2026-11-10T00:00:00.000Z", app: { records: [{ collection: "dev.cocore.app.registration", rkey: "self", value: REGISTRATION }], wellKnown: { "cocore-app.json": { did: "did:web:app.example" } } } }; test("appRepo: the app's did:web, its records rebased onto the origin with their CIDs, the well-known files; read-only", async () => { const repo = await appRepo({ sessions: { app: { record: appRecord }, other: { record: records.atirc } } }, { origin: "https://deploy.example/page", publicKeyMultibase: "zDnaeKey" }); assert.equal(repo.did, "did:web:deploy.example"); assert.deepEqual(repo.document.verificationMethod, [{ id: "did:web:deploy.example#atproto", type: "Multikey", controller: "did:web:deploy.example", publicKeyMultibase: "zDnaeKey" }]); assert.deepEqual(repo.document.service, [{ id: "#atproto_pds", type: "AtprotoPersonalDataServer", serviceEndpoint: "https://deploy.example" }]); const value = { ...REGISTRATION, website: "https://deploy.example", returnUrls: ["https://deploy.example/done"] }; // the same CID as rust/host oauth::AppRepo's test assert.deepEqual(repo.records, [{ uri: "at://did:web:deploy.example/dev.cocore.app.registration/self", cid: "bafyreiact3y3dp5yjllsptqtj57iv4kuzvo4rtxvpzakl7el5qgx6fp6nm", value, collection: "dev.cocore.app.registration", rkey: "self" }]); const answer = (path) => { const [status, type, body] = appRepoAnswer(repo, path); return [status, type, type === "text/plain" ? body : JSON.parse(body)]; }; assert.deepEqual(answer("/.well-known/cocore-app.json"), [200, "application/json", { did: "did:web:deploy.example" }]); assert.deepEqual(answer("/.well-known/atproto-did"), [200, "text/plain", "did:web:deploy.example"]); assert.deepEqual(answer("/xrpc/com.atproto.repo.getRecord?repo=did:web:deploy.example&collection=dev.cocore.app.registration&rkey=self")[2].value, value); assert.deepEqual(answer("/xrpc/com.atproto.repo.getRecord?repo=deploy.example&collection=dev.cocore.app.registration&rkey=nope"), [400, "application/json", { error: "RecordNotFound", message: "Could not locate record: at://did:web:deploy.example/dev.cocore.app.registration/nope" }]); assert.equal(answer("/xrpc/com.atproto.repo.listRecords?repo=did:plc:other&collection=x")[2].error, "RepoNotFound"); assert.deepEqual(answer("/xrpc/com.atproto.repo.describeRepo?repo=did:web:deploy.example")[2].collections, ["dev.cocore.app.registration"]); for (const path of ["/xrpc/com.atproto.server.getServiceAuth?aud=x", "/xrpc/com.atproto.server.createAccount", "/xrpc/com.atproto.sync.getRepo?did=did:web:deploy.example", "/.well-known/other.json", "/"]) assert.equal(appRepoAnswer(repo, path), undefined, path); const handler = appRepoHandler(repo); const res = handler(new Request("https://deploy.example/.well-known/did.json")); assert.equal(res.headers.get("access-control-allow-origin"), "*"); assert.equal((await res.json()).id, "did:web:deploy.example"); assert.equal(handler(new Request("https://elsewhere.example/.well-known/did.json")), undefined, "another Host is not this identity"); assert.equal(handler(new Request("https://deploy.example/.well-known/did.json", { method: "POST" })), undefined); await assert.rejects(appRepo({ app: appRecord }, { origin: "http://127.0.0.1:8787", publicKeyMultibase: "z" }), /public https origin/); const clash = { ...appRecord, clientMetadata: { ...appRecord.clientMetadata, client_id: "https://other.example/c.json" } }; await assert.rejects(appRepo({ app: appRecord, clash }, { origin: "https://deploy.example", publicKeyMultibase: "z" }), /declared twice, differently/); await assert.rejects(appRepo({ bad: { ...appRecord, app: { wellKnown: { "did.json": {} } } } }, { origin: "https://deploy.example", publicKeyMultibase: "z" }), /not a file name it may serve/); }); test("atlibs stage --origin writes the app identity as files a static host serves (one record: getRecord too)", async () => { const dir = await mkdtemp(join(tmpdir(), "atlibs-app-")); const { cp, mkdir, writeFile } = await import("node:fs/promises"); const root = new URL("../../../", import.meta.url).pathname; await mkdir(join(dir, "statusphere"), { recursive: true }); await cp(join(root, "examples/rust/statusphere/module.json"), join(dir, "statusphere/module.json")); await cp(join(root, "target/wasm32-unknown-unknown/release/statusphere.wasm"), join(dir, "target/wasm32-unknown-unknown/release/statusphere.wasm"), { recursive: true }); const session = JSON.parse(await readFile(join(dir, "statusphere/module.json"), "utf8")).session; await mkdir(join(dir, "records")); await writeFile(join(dir, `records/${session}.json`), JSON.stringify({ ...appRecord, name: session })); const out = join(dir, "out"); const manifest = JSON.parse((await atlibs(["stage", "--out", out, "--origin", "https://deploy.example/", join(dir, "statusphere")])).stdout); assert.equal(manifest.app.did, "did:web:deploy.example"); const file = async (path) => readFile(join(out, path), "utf8"); const doc = JSON.parse(await file(".well-known/did.json")); // the Rust twin of appRepo: the same files from the same key const repo = await appRepo({ [session]: appRecord }, { origin: "https://deploy.example", publicKeyMultibase: doc.verificationMethod[0].publicKeyMultibase }); assert.match(doc.verificationMethod[0].publicKeyMultibase, /^zDn/, "a P-256 did:key the host keeps"); for (const [path, query] of [[".well-known/did.json", ""], [".well-known/atproto-did", ""], [".well-known/cocore-app.json", ""], ["xrpc/com.atproto.repo.getRecord", "?repo=did:web:deploy.example&collection=dev.cocore.app.registration&rkey=self"], ["xrpc/com.atproto.repo.listRecords", "?repo=did:web:deploy.example&collection=dev.cocore.app.registration"], ["xrpc/com.atproto.repo.describeRepo", "?repo=did:web:deploy.example"]]) { const [, type, body] = appRepoAnswer(repo, `/${path}${query}`); assert.deepEqual(type === "text/plain" ? await file(path) : JSON.parse(await file(path)), type === "text/plain" ? body : JSON.parse(body), path); } assert.deepEqual(manifest.app.files.sort(), [".well-known/atproto-did", ".well-known/cocore-app.json", ".well-known/did.json", "xrpc/com.atproto.repo.describeRepo", "xrpc/com.atproto.repo.getRecord", "xrpc/com.atproto.repo.listRecords"]); // the key is kept: staging again names the same identity key const again = JSON.parse((await atlibs(["stage", "--out", join(dir, "out2"), "--origin", "https://deploy.example/", join(dir, "statusphere")])).stdout); assert.equal(again.app.did, manifest.app.did); assert.equal(JSON.parse(await readFile(join(dir, "out2/.well-known/did.json"), "utf8")).verificationMethod[0].publicKeyMultibase, doc.verificationMethod[0].publicKeyMultibase); });