~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
261 lines · 23.0 KB Raw History
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:<lxm>?aud=<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);
});