// node --test javascript/src/compose/compose.test.mjs (needs `cargo wasm` and `cargo host`; no network) import assert from "node:assert/strict"; import { mkdtemp, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; import { pathToFileURL } from "node:url"; import { atlibs } from "../../../rust/host/conformance.mjs"; import { loadComposition, runCompose, staged, validateComposition } from "./index.js"; const wasm = new Uint8Array(await readFile(new URL("../../../target/wasm32-unknown-unknown/release/compose.wasm", import.meta.url))); const spec = JSON.parse(await readFile(new URL("../../../examples/rust/compose/module.json", import.meta.url), "utf8")); const LOOKUP = "com.example.compose.lookup"; // ctx.wasm.run for any module bytes, in process. const instances = new Map(); const ctx = { wasm: { run: async ({ module, entry, input }) => { if (!instances.has(module)) instances.set(module, new WebAssembly.Instance(new WebAssembly.Module(module), {}).exports); const exports = instances.get(module); const data = new TextEncoder().encode(JSON.stringify(input)); const ptr = exports.alloc(data.length); new Uint8Array(exports.memory.buffer, ptr, data.length).set(data); const packed = BigInt.asUintN(64, exports[entry](ptr, data.length)); return JSON.parse(new TextDecoder().decode(new Uint8Array(exports.memory.buffer, Number(packed >> 32n), Number(packed & 0xffffffffn)))); }, }, }; // The depth and cycle tests route x.y.a, x.y.b, x.y.c, x.y.never between instances: hooks of the module. const hooks = [...spec.hooks, ...["x.y.a", "x.y.b", "x.y.c", "x.y.never"].map((nsid) => ({ nsid, reason: "a test route" }))]; // ...and serve them at caller const implementsAll = [...spec.implements, ...["x.y.a", "x.y.b", "x.y.c", "x.y.never"].map((nsid) => ({ nsid, entry: "caller" }))]; const load = async (key) => ({ ...spec, hooks, implements: implementsAll, module: wasm, ...(key === "compose-net" && { calls: ["com.example.compose.net"] }), ...(key === "compose-http" && { calls: ["com.fedfork.exec.http.fetch"], http: ["https://svc.example"] }) }); const compose = (manifest) => loadComposition(ctx, { name: "test", ...manifest }, { load }); const toy = (name, extra) => ({ name, module: "compose", ...extra }); const edge = (from, nsid, to, extra) => ({ from, nsid, to, reason: `${from} asks ${to}`, ...extra }); test("A calls B over an edge and B calls A's callback; the answer comes back", async () => { const comp = await compose({ instances: [toy("a"), toy("b")], edges: [edge("a", LOOKUP, "b")] }); const ran = await runCompose(ctx, comp, "a", "caller", { nsid: LOOKUP, input: { n: 21 } }); assert.equal(ran.error, undefined); assert.deepEqual(ran.result, { q: { ok: true, output: { doubled: 42, ack: { seen: { step: 1 } }, caller: { instance: "a", nsid: LOOKUP, reason: "a asks b" } } }, progress: [{ step: 1 }], }); assert.deepEqual(ran.graph.map(({ from, nsid, to, ok }) => [from, nsid, to, ok]), [ ["a", "cap", undefined, true], // the reply ["b", "cap", undefined, true], // the progress callback ["a", LOOKUP, "b", true], ]); }); test("a caller handler gets the call, and the module's callback as a function", async () => { let late; const comp = await compose({ instances: [toy("a")], handlers: [{ name: "confirm" }], edges: [{ from: "a", nsid: "com.example.compose.confirm", handler: "confirm", reason: "the user approves" }], }); const seen = []; const ran = await runCompose(ctx, comp, "a", "caller", { nsid: "com.example.compose.confirm", input: { n: 1 } }, { handlers: { confirm: async ({ n, progress }, { caller, reason }) => { seen.push([n, caller, reason]); late = progress; return { approved: true, ack: await progress({ asked: n }) }; }, }, }); assert.deepEqual(seen, [[1, "a", "the user approves"]]); assert.deepEqual(ran.result, { q: { ok: true, output: { approved: true, ack: { seen: { asked: 1 } } } }, progress: [{ asked: 1 }] }); await assert.rejects(late({}), /capability revoked/); }); test("JS functions in the root input are callbacks the root may call; forged tokens are refused", async () => { const comp = await compose({ instances: [toy("b")] }); const ran = await runCompose(ctx, comp, "b", "lookup", { n: 2, progress: async (data) => ({ host: data }) }); assert.deepEqual(ran.result, { doubled: 4, ack: { host: { step: 1 } }, caller: null }); const forged = await runCompose(ctx, comp, "b", "lookup", { n: 2, progress: { $cap: `cap.${"0".repeat(32)}.1` } }); assert.match(forged.result.ack.error, /is not a capability this run holds/); }); test("a policy module allows or denies the call before it is made", async () => { const comp = await compose({ instances: [toy("a"), toy("b"), toy("even")], edges: [edge("a", LOOKUP, "b", { policies: [{ instance: "even", entry: "even" }] })], }); const denied = await runCompose(ctx, comp, "a", "caller", { nsid: LOOKUP, input: { n: 21 } }); assert.deepEqual(denied.result.q, { ok: false, error: "policy even denied: n 21 is not even" }); assert.ok(!denied.graph.some((g) => g.to === "b" && g.ok)); const allowed = await runCompose(ctx, comp, "a", "caller", { nsid: LOOKUP, input: { n: 4 } }); assert.equal(allowed.result.q.output.doubled, 8); assert.deepEqual(allowed.graph.find((g) => g.policy), { from: "a", nsid: LOOKUP, policy: "even", ok: true }); }); test("a policy run does not hold the caller's capabilities", async () => { const comp = await compose({ instances: [toy("a"), toy("b"), toy("spy")], edges: [edge("a", LOOKUP, "b", { policies: [{ instance: "spy", entry: "spy" }] })], }); const invoked = []; const ran = await runCompose(ctx, comp, "a", "caller", { nsid: LOOKUP, input: { n: 4, host: async (data) => (invoked.push(data), "secret") } }); assert.deepEqual(invoked, [], "the policy could not invoke the root's host function"); assert.equal(ran.result.q.output.doubled, 8); assert.deepEqual(ran.graph.find((g) => g.policy), { from: "a", nsid: LOOKUP, policy: "spy", ok: true }); assert.ok(ran.graph.some((g) => g.from === "spy" && g.nsid === "cap" && g.error === "not held")); }); test("the host's budget and limits cap the manifest's", async () => { // limits: the smaller of the host's and the manifest's, whichever sets it lower const deep = await compose({ instances: [toy("a"), toy("b"), toy("c")], edges: [edge("a", "x.y.b", "b"), edge("b", "x.y.c", "c")], limits: { depth: 50 }, }); const capped = await runCompose(ctx, deep, "a", "caller", { nsid: "x.y.b", input: { nsid: "x.y.c", input: { nsid: "x.y.d" } } }, { limits: { depth: 2 } }); assert.deepEqual(capped.result.q.output.q, { ok: false, error: "composition exceeded depth 2" }); const lowered = await runCompose(ctx, { ...deep, manifest: { ...deep.manifest, limits: { depth: 2 } } }, "a", "caller", { nsid: "x.y.b", input: { nsid: "x.y.c", input: { nsid: "x.y.d" } } }, { limits: { depth: 50 } }); assert.deepEqual(lowered.result.q.output.q, { ok: false, error: "composition exceeded depth 2" }); const slow = await compose({ instances: [toy("w", { budget: { wallMs: 1e9 } })], handlers: [{ name: "hang" }], edges: [{ from: "w", nsid: "x.y.never", handler: "hang", reason: "never answers" }], }); const timed = await runCompose(ctx, slow, "w", "caller", { nsid: "x.y.never" }, { budget: { wallMs: 50 }, handlers: { hang: () => new Promise(() => {}) } }); assert.match(String(timed.error), /exceeded 50 ms/, "a manifest budget cannot raise the host's wallMs"); // only positive finite numbers count: a host value that is not one leaves the manifest's (or the default) const own = await compose({ ...slow.manifest, instances: [toy("w", { budget: { wallMs: 50 } })] }); const ownTimed = await runCompose(ctx, own, "w", "caller", { nsid: "x.y.never" }, { budget: { wallMs: "lots", calls: -1 }, handlers: { hang: () => new Promise(() => {}) } }); assert.match(String(ownTimed.error), /exceeded 50 ms/, "with no usable host value the manifest's own applies"); for (const budget of [{ calls: "1" }, { calls: 0 }, { wallMs: Infinity }, { wallMs: "unlimited" }, { nope: 1 }, [1]]) { assert.throws(() => validateComposition({ instances: [toy("a", { budget })] }), /budget must map/, JSON.stringify(budget)); } // unlimited is the host's alone: a manifest cannot say it nor raise past a finite host limit, and under an // unlimited host its own lower limit still holds for (const limits of [{ calls: Infinity }, { calls: "unlimited" }]) { assert.throws(() => validateComposition({ instances: [toy("a")], limits }), /limits must map/, String(limits.calls)); } const chain = { nsid: "x.y.b", input: { nsid: "x.y.c", input: { nsid: "x.y.d" } } }; const raised = await runCompose(ctx, { ...deep, manifest: { ...deep.manifest, limits: { calls: 50 } } }, "a", "caller", chain, { limits: { calls: 1 } }); assert.match(raised.result.q.output.q.error, /composition exceeded 1 calls/); const under = await runCompose(ctx, { ...deep, manifest: { ...deep.manifest, limits: { calls: 1 } } }, "a", "caller", chain, { limits: { calls: Infinity } }); assert.match(under.result.q.output.q.error, /composition exceeded 1 calls/); const free = await runCompose(ctx, deep, "a", "caller", chain, { limits: { calls: Infinity }, budget: { calls: Infinity, wallMs: Infinity } }); assert.equal(free.error, undefined); assert.doesNotMatch(JSON.stringify(free.result), /exceeded/); assert.equal(free.result.q.output.q.output.q.ok, false, "c's call has no edge"); }); test("calls with no edge are refused unless allowlisted; allowlisted ones use the instance's session and policies", async () => { const comp = await compose({ instances: [toy("a"), toy("n", { module: "compose-net", session: "net", policies: [{ instance: "even", entry: "even" }] }), toy("even")], }); const refused = await runCompose(ctx, comp, "a", "caller", { nsid: "com.example.compose.net", input: { n: 2 } }); assert.match(refused.result.q.error, /a may not call com.example.compose.net: no edge/); const sessions = []; const callFor = (inst) => async (nsid, request) => (sessions.push([inst.session, nsid, request.input.n]), { via: inst.session }); const made = await runCompose(ctx, comp, "n", "caller", { nsid: "com.example.compose.net", input: { n: 2 } }, { callFor }); assert.deepEqual(made.result.q, { ok: true, output: { via: "net" } }); assert.deepEqual(sessions, [["net", "com.example.compose.net", 2]]); const odd = await runCompose(ctx, comp, "n", "caller", { nsid: "com.example.compose.net", input: { n: 3 } }, { callFor }); assert.match(odd.result.q.error, /policy even denied/); assert.equal(sessions.length, 1); }); test("the host's http serviceAuth is minted with the instance's session (callFor), not a call the module makes", async () => { const comp = await compose({ instances: [toy("h", { module: "compose-http", session: "s" })] }); const sent = []; const httpCtx = { ...ctx, http: { fetch: async (url, init) => (sent.push([url, init.headers.authorization]), new Response("ok")) } }; const http = { "https://svc.example": { serviceAuth: { aud: "did:web:svc.example", lxm: "x.y.z" } } }; const fetch = { nsid: "com.fedfork.exec.http.fetch", input: { url: "https://svc.example/x" } }; const minted = []; const callFor = (inst) => async (nsid, request) => (minted.push([inst.session, nsid, request.params]), { token: "T" }); const ran = await runCompose(httpCtx, comp, "h", "caller", fetch, { callFor, http }); assert.equal(ran.result.q.ok, true, JSON.stringify(ran.result)); assert.equal(ran.result.q.output.status, 200); // the module lists no getServiceAuth and no edge routes it: the host mints it, as runModule does on its own assert.deepEqual(minted, [["s", "com.atproto.server.getServiceAuth", { aud: "did:web:svc.example", lxm: "x.y.z" }]]); assert.deepEqual(sent, [["https://svc.example/x", "Bearer T"]]); // no session: no token, and nothing sent const none = await runCompose(httpCtx, comp, "h", "caller", fetch, { http }); assert.match(none.result.q.error, /needs a service-auth token \(aud did:web:svc.example, lxm x.y.z\)/); assert.equal(sent.length, 1); }); test("cycles and depth past the limit are refused", async () => { const comp = await compose({ instances: [toy("a"), toy("b"), toy("c")], edges: [edge("a", "x.y.b", "b"), edge("b", "x.y.a", "a"), edge("b", "x.y.c", "c")], }); const cycle = await runCompose(ctx, comp, "a", "caller", { nsid: "x.y.b", input: { nsid: "x.y.a", input: { n: 1 } } }); assert.deepEqual(cycle.result.q.output.q, { ok: false, error: "cycle: a -> b -> a" }); const deep = await runCompose(ctx, comp, "a", "caller", { nsid: "x.y.b", input: { nsid: "x.y.c", input: { nsid: "x.y.d" } } }, { limits: { depth: 2 } }); assert.deepEqual(deep.result.q.output.q, { ok: false, error: "composition exceeded depth 2" }); const few = await runCompose(ctx, comp, "a", "caller", { nsid: "x.y.b", input: { nsid: "x.y.c", input: { nsid: "x.y.d" } } }, { limits: { calls: 1 } }); assert.match(few.result.q.output.q.error, /composition exceeded 1 calls/); }); test("manifests are checked: reasons, declared handlers, routes, pins", async () => { assert.throws(() => validateComposition({ instances: [toy("a")], edges: [{ from: "a", nsid: LOOKUP, to: "a" }] }), /reason is required/); assert.throws(() => validateComposition({ instances: [toy("a")], edges: [{ ...edge("a", LOOKUP, "a"), entry: "lookup" }] }), /edge a com.example.compose.lookup: an edge has no entry: it reaches the entry its target's module implements/); assert.throws(() => validateComposition({ instances: [toy("a")], edges: [edge("a", LOOKUP, "zz")] }), /to is not an instance/); assert.throws(() => validateComposition({ instances: [toy("a")], edges: [{ from: "a", nsid: LOOKUP, handler: "h", reason: "r" }] }), /not declared/); await assert.rejects(compose({ instances: [toy("a", { pin: "0".repeat(64) })] }), /composition: instance a module does not match its pin/); assert.throws(() => validateComposition({ instances: [toy("a", { pin: "deadbeef" })] }), /composition: instance a pin must be 64 lowercase hex characters/); const comp = await compose({ instances: [toy("a")], handlers: [{ name: "confirm" }] }); await assert.rejects(runCompose(ctx, comp, "a", "caller", {}), /handler confirm was not supplied/); }); test("an edge reaches the entry its target implements; a target that does not implement the NSID is refused", async () => { const comp = await compose({ instances: [toy("a"), toy("b")], edges: [edge("a", LOOKUP, "b")] }); assert.deepEqual((await runCompose(ctx, comp, "a", "caller", { nsid: LOOKUP, input: { n: 2 } })).graph.at(-1), { from: "a", nsid: LOOKUP, to: "b", entry: "lookup", reason: "a asks b", ok: true }); const bare = (key) => ({ ...spec, implements: undefined, module: wasm, id: key }); await assert.rejects(loadComposition(ctx, { name: "t", instances: [toy("a"), toy("b")], edges: [edge("a", LOOKUP, "b")] }, { load: bare }), /composition: edge a com.example.compose.lookup: b \(compose\) does not implement com.example.compose.lookup/); }); test("an instance's input is its module's config: keys, types, parameter types, required fields", async () => { const ok = await compose({ parameters: [{ key: "n", type: "integer" }], instances: [toy("a", { input: { n: { $param: "n" } } }), toy("b", { input: { n: 3 } })] }); assert.equal(ok.manifest.instances.length, 2); const refused = async (manifest, re) => assert.rejects(compose(manifest), re); await refused({ instances: [toy("a", { input: { m: 1 } })] }, /composition: instance a: input m is not in compose's config \(n\)/); await refused({ instances: [toy("a", { input: { n: "3" } })] }, /composition: instance a: input n must be of type integer/); await refused({ parameters: [{ key: "n" }], instances: [toy("a", { input: { n: { $param: "n" } } })] }, /composition: instance a: input n is integer, but parameter n is string/); await refused({ instances: [toy("a", { input: [1] })] }, /composition: instance a: input must be an object/); const config = (config) => async (key) => ({ ...(await load(key)), config }); await assert.rejects(loadComposition(ctx, { name: "t", instances: [toy("a", { input: { n: 1 } })] }, { load: config(undefined) }), /composition: instance a: input n is not in compose's config$/); await assert.rejects(loadComposition(ctx, { name: "t", instances: [toy("a")] }, { load: config([{ key: "n", label: "n", required: true }]) }), /composition: instance a: input n is required by compose's config/); await assert.rejects(loadComposition(ctx, { name: "t", instances: [toy("a")] }, { load: config([{ key: "n", type: "date" }]) }), /module config must be a list of \{ key, label, type\?/); }); test("atlibs stage stages a composition; a host loads it back from the staged directory", async () => { const out = await mkdtemp(join(tmpdir(), "atlibs-compose-")); const manifest = new URL("../../../examples/rust/compose/composition.json", import.meta.url).pathname; await atlibs(["stage", "--target", "server", "--out", out, manifest]); const written = JSON.parse(await readFile(join(out, "composition.json"), "utf8")); assert.deepEqual(written.instances.map((i) => [i.module, i.pin.length]), [["compose", 64], ["compose", 64], ["compose", 64]]); assert.equal(written.target, "server"); assert.equal(JSON.parse(await readFile(join(out, "stage.json"), "utf8")).modules.compose.pin, written.instances[0].pin); const fileFetch = async (url) => new Response(await readFile(new URL(url))); const comp = await loadComposition(ctx, written, { load: staged(pathToFileURL(`${out}/`), fileFetch) }); const ran = await runCompose(ctx, comp, "ui", "caller", { nsid: "com.example.compose.lookup", input: { n: 6 } }, { handlers: { confirm: () => true } }); assert.equal(ran.result.q.output.doubled, 12); await assert.rejects(atlibs(["stage", "--target", "edge", "--out", out, manifest]), /--target must be spa or server/); }); test("staged() takes only module record fields: a staged json cannot pick its id or ledger owner", async () => { const files = { "modules/m.json": JSON.stringify({ ...spec, id: "/etc/passwd", owner: "someone-else", uri: "at://did:plc:other/x/y", sha256: "00" }), "modules/m.wasm": wasm }; const base = "https://site.example/app/"; const mod = await staged(base, async (url) => new Response(files[url.href.slice(base.length)]))("m"); assert.equal(mod.id, "https://site.example/app/modules/m.wasm"); assert.equal(mod.owner, undefined); assert.equal(mod.uri, undefined); assert.equal(mod.sha256, undefined); assert.deepEqual(mod.calls, spec.calls); }); // Review 3b #3, #6 and the policy-stdin note, with a stub engine (ctx.wasm.run by entry). const stub = (entries) => ({ wasm: { run: async ({ entry, input }) => entries[entry](input) } }); const ASK = "com.example.ask.x"; test("limits: depth and calls must be positive numbers; a manifest's bad values never disable the cap", async () => { for (const limits of [{ calls: "x" }, { calls: null }, { depth: 0 }, { depth: Infinity }, { other: 1 }, [], "8"]) { assert.throws(() => validateComposition({ instances: [{ name: "a", module: "m" }], limits }), /composition: limits must map depth, calls to positive numbers/, JSON.stringify(limits)); } validateComposition({ instances: [{ name: "a", module: "m" }], limits: { depth: 3, calls: 10 } }); let handled = 0; const go = ({ event }) => (event.type === "start" ? { calls: Array.from({ length: 150 }, (_, i) => ({ id: `c${i}`, nsid: ASK })) } : {}); const manifest = { instances: [{ name: "a", module: "m", budget: { calls: 500 } }], handlers: [{ name: "h" }], edges: [{ from: "a", nsid: ASK, handler: "h", reason: "r" }], limits: { calls: "x" } }; const ran = await runCompose(stub({ go }), { manifest, modules: { a: { module: new Uint8Array([1]) } } }, "a", "go", {}, { handlers: { h: async () => (handled++, 1) } }); assert.equal(handled, 100, "an unvalidated manifest's limits.calls 'x' leaves the default 100"); assert.match(ran.calls.find((c) => !c.ok).error, /composition exceeded 100 calls/); }); test("subscribing to a held capability is refused before it runs", async () => { let invoked = 0; const closed = []; const go = ({ event, ping }) => { if (event.type === "start") return { calls: [{ id: "c", nsid: ping.$cap, subscribe: true, params: { hi: 1 } }] }; if (event.type === "closed" || event.type === "result") closed.push(event.error); return {}; }; const ran = await runCompose(stub({ go }), { manifest: { instances: [{ name: "a", module: "m" }] }, modules: { a: { module: new Uint8Array([1]) } } }, "a", "go", { ping: async () => (invoked++, {}) }); assert.equal(invoked, 0); assert.match(closed[0], /cap\.[0-9a-f]+\.\d+ cannot be subscribed to/); assert.deepEqual(ran.graph, []); }); test("a policy sees the call without its stdin; the handler still gets it", async () => { const seen = {}; const entries = { go: ({ event }) => (event.type === "start" ? { calls: [{ id: "x", nsid: ASK, input: { stdin: "tok", a: 1 } }] } : { result: event, done: true }), evaluate: ({ call }) => ((seen.policy = call.input), { result: { allow: true }, done: true }), }; const manifest = { instances: [{ name: "a", module: "m" }, { name: "p", module: "m" }], handlers: [{ name: "h" }], edges: [{ from: "a", nsid: ASK, handler: "h", reason: "r", policies: [{ instance: "p" }] }] }; const modules = { a: { module: new Uint8Array([1]) }, p: { module: new Uint8Array([1]) } }; const ran = await runCompose(stub(entries), { manifest, modules }, "a", "go", {}, { handlers: { h: async (input) => ((seen.handler = input), "ok") } }); assert.equal(ran.result.ok, true, ran.result.error); assert.deepEqual(seen, { policy: { a: 1 }, handler: { stdin: "tok", a: 1 } }); }); test("the host's held values start the root's run", async () => { const stub = { wasm: { run: async () => ({ result: { t: { $held: "t" } }, done: true }) } }; const comp = await loadComposition(stub, { name: "test", instances: [toy("a")] }, { load }); const ran = await runCompose(stub, comp, "a", "caller", {}, { held: { t: [1] } }); assert.equal(ran.error, undefined); assert.deepEqual(ran.result, { t: [1] }); });