1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// A local fixture registry for the atlibs CLI (no network): a fake PLC, handle resolver and PDS
// (node:http) for alice.test. publish(name) puts examples/rust/<name>'s `cargo wasm` build, its
// module.json and its records/<session>.json as alice.test's records. It also answers an http
// composition handler (POST /handler), app-password sign-in with refresh-token rotation (auth counts
// the calls), and the writes `atlibs publish` makes (listRecords, putRecord/createRecord, uploadBlob).
// `env` points the CLI at it: ATLIBS_PLC_URL, ATLIBS_HANDLE_RESOLVER, ATLIBS_ALLOW_PRIVATE.
//
// const registry = await fixtureRegistry();
// await registry.publish("compose");
// // atlibs add alice.test/compose, with registry.env
// registry.close();
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { modulePin } from "../../javascript/src/run-module/index.js";
const ROOT = new URL("../../", import.meta.url);
export const DID = "did:plc:alicetest";
const sha256 = (bytes) => createHash("sha256").update(bytes).digest();
// CIDv1 base32: raw (blobs) or dag-cbor (records; any stable hash will do for a fake PDS)
export function cid(codec, bytes) {
const raw = [0x01, codec, 0x12, 0x20, ...sha256(bytes)];
let bits = 0, value = 0, out = "b";
for (const byte of raw) {
value = (value << 8) | byte;
bits += 8;
while (bits >= 5) out += "abcdefghijklmnopqrstuvwxyz234567"[(value >>> (bits -= 5)) & 31];
}
return bits ? out + "abcdefghijklmnopqrstuvwxyz234567"[(value << (5 - bits)) & 31] : out;
}
export async function fixtureRegistry({ port = 0 } = {}) {
const records = new Map(); // "collection/rkey" -> { cid, value }
const blobs = new Map();
const auth = { refreshes: 0, creates: 0 };
const seen = []; // the requests' paths
let base;
const put = (collection, rkey, value) => {
const entry = { cid: cid(0x71, Buffer.from(JSON.stringify(value))), value };
records.set(`${collection}/${rkey}`, entry);
return { uri: `at://${DID}/${collection}/${rkey}`, cid: entry.cid };
};
async function publish(name) {
const wasm = await readFile(new URL(`target/wasm32-unknown-unknown/release/${name.replaceAll("-", "_")}.wasm`, ROOT));
const spec = JSON.parse(await readFile(new URL(`examples/rust/${name}/module.json`, ROOT), "utf8"));
const blob = cid(0x55, wasm);
blobs.set(blob, wasm);
const session = spec.session && put("com.fedfork.exec.oauth.clientMetadata", spec.session, JSON.parse(await readFile(new URL(`records/${spec.session}.json`, ROOT), "utf8")));
const record = { $type: "com.fedfork.exec.wasm.module", ...spec, ...(session && { session }), module: { $type: "blob", ref: { $link: blob }, mimeType: "application/wasm", size: wasm.length }, abi: "emdash-wasm-json-v1", actions: spec.actions ?? [], createdAt: "2026-11-08T00:00:00.000Z" };
// as `atlibs publish` writes it: at its name and at its version (<name>.<32 hex of its pin>)
put("com.fedfork.exec.wasm.module", `${name}.${(await modulePin({ ...spec, module: wasm })).slice(0, 32)}`, record);
put("com.fedfork.exec.wasm.module", name, record);
return { wasm, blob };
}
const server = createServer((req, res) => {
const url = new URL(req.url, base);
seen.push(url.pathname);
const json = (status, body) => res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(body));
if (url.pathname === `/plc/${DID}`) return json(200, { id: DID, alsoKnownAs: ["at://alice.test"], service: [{ id: "#atproto_pds", type: "AtprotoPersonalDataServer", serviceEndpoint: base }] });
if (url.pathname === "/xrpc/com.atproto.identity.resolveHandle") return url.searchParams.get("handle") === "alice.test" ? json(200, { did: DID }) : json(400, { error: "InvalidRequest" });
if (url.pathname === "/xrpc/com.atproto.sync.getBlob") return blobs.has(url.searchParams.get("cid")) ? res.end(blobs.get(url.searchParams.get("cid"))) : json(404, { error: "BlobNotFound" });
if (url.pathname === "/xrpc/com.atproto.repo.getRecord") {
const found = records.get(`${url.searchParams.get("collection")}/${url.searchParams.get("rkey")}`);
return found ? json(200, { uri: `at://${DID}/${url.searchParams.get("collection")}/${url.searchParams.get("rkey")}`, ...found }) : json(400, { error: "RecordNotFound", message: "Could not locate record" });
}
// an http handler (atlibs run --handler confirm=http://127.0.0.1/handler): answers with what it got
if (url.pathname === "/handler" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => (body += chunk));
return req.on("end", () => json(200, { approved: true, got: JSON.parse(body), auth: req.headers.authorization ?? null }));
}
// sign-in (the shared-session test): each refresh token is taken once (rotation), a1 is expired, then invalid
const late = (status, body) => setTimeout(() => json(status, body), 10); // concurrent runs overlap
const bearer = req.headers.authorization ?? "";
const didDoc = { service: [{ id: "#atproto_pds", serviceEndpoint: base }] };
if (url.pathname === "/xrpc/com.atproto.server.createSession") return auth.creates++, late(200, { did: DID, handle: "alice.test", accessJwt: "a9", refreshJwt: "r9", didDoc });
if (url.pathname === "/xrpc/com.atproto.server.refreshSession")
return ++auth.refreshes === 1 && bearer === "Bearer r1" ? late(200, { did: DID, handle: "alice.test", accessJwt: "a2", refreshJwt: "r2", didDoc }) : late(400, { error: "ExpiredToken", message: "refresh token already used" });
if (url.pathname === "/xrpc/com.atproto.repo.deleteRecord") return bearer === "Bearer a1" ? (auth.refreshes ? late(401, { error: "InvalidToken" }) : late(400, { error: "ExpiredToken" })) : late(200, {});
// publishing (the `atlibs publish` test): listRecords, putRecord/createRecord, uploadBlob
if (url.pathname === "/xrpc/com.atproto.repo.listRecords") {
const prefix = `${url.searchParams.get("collection")}/`;
return json(200, { records: [...records].filter(([key]) => key.startsWith(prefix)).map(([key, r]) => ({ uri: `at://${DID}/${key}`, ...r })) });
}
if (req.method === "POST" && /^\/xrpc\/com\.atproto\.repo\.(put|create)Record$/.test(url.pathname) || url.pathname === "/xrpc/com.atproto.repo.uploadBlob") {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
return req.on("end", () => {
const body = Buffer.concat(chunks);
if (url.pathname.endsWith("uploadBlob")) {
blobs.set(cid(0x55, body), body);
return json(200, { blob: { $type: "blob", ref: { $link: cid(0x55, body) }, mimeType: req.headers["content-type"], size: body.length } });
}
const { collection, rkey, record } = JSON.parse(body);
json(200, put(collection, rkey, record));
});
}
json(501, { error: "MethodNotImplemented" });
});
await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
base = `http://127.0.0.1:${server.address().port}`;
const env = { ATLIBS_PLC_URL: `${base}/plc`, ATLIBS_HANDLE_RESOLVER: base, ATLIBS_ALLOW_PRIVATE: "1" };
return { base, env, records, blobs, auth, seen, put, publish, close: () => server.close() };
}