diff --git a/.changeset/attest-launch-for-pod.md b/.changeset/attest-launch-for-pod.md new file mode 100644 index 00000000..d93f06ad --- /dev/null +++ b/.changeset/attest-launch-for-pod.md @@ -0,0 +1,11 @@ +--- +--- + +Add `room/launch-attest.ts` — the L2 launch-attestation orchestration step: +`podLaunchManifest(pod)` (the pod's resolved door grants = authority held) + +`attestLaunchForPod(pod)` which attests the manifest via the keeper door +(`runKeeperDoorAttestLaunch`) and stores the signed L2 content-addressed +(`storeLaunchAttestation`), returning the `l2LaunchDigest` the box's keeper push +links. Effects are injected seams (offline-testable). The live `playPod` flow +calls this once the keeper door is up, then projects `l2LaunchDigest` into the box +env. No release. diff --git a/packages/prx/src/room/launch-attest.ts b/packages/prx/src/room/launch-attest.ts new file mode 100644 index 00000000..6a29ca92 Binary files /dev/null and b/packages/prx/src/room/launch-attest.ts differ diff --git a/packages/prx/test/room/launch-attest.test.ts b/packages/prx/test/room/launch-attest.test.ts new file mode 100644 index 00000000..e375fd9b --- /dev/null +++ b/packages/prx/test/room/launch-attest.test.ts @@ -0,0 +1,48 @@ +// The launch-attestation orchestration step: manifest = the pod's held doors, +// attested via the keeper door + stored; returns the l2LaunchDigest. +import { describe, test, expect } from "bun:test"; + +import { perRepoPod } from "../../src/room/per-repo-pod.ts"; +import { attestLaunchForPod, podLaunchManifest } from "../../src/room/launch-attest.ts"; + +describe("podLaunchManifest", () => { + test("is the pod's resolved door grants, deterministically ordered", () => { + const m = podLaunchManifest(perRepoPod); + expect(m.pod).toBe(perRepoPod.name); + expect(Array.isArray(m.doors)).toBe(true); + // each door names the authority the box holds + for (const d of m.doors) { + expect(typeof d.door).toBe("string"); + expect(typeof d.capability).toBe("string"); + expect(typeof d.consumer).toBe("string"); + } + // deterministic (same input → same manifest) + expect(podLaunchManifest(perRepoPod)).toEqual(m); + }); +}); + +describe("attestLaunchForPod", () => { + test("attests the manifest via the keeper door, stores the L2, returns the digest", async () => { + let sent: { subject: string; manifest: unknown } | undefined; + let storedL2: unknown; + const digest = await attestLaunchForPod(perRepoPod, { + attestLaunch: async (opts) => { + sent = opts; + return { + subject: opts.subject, + manifestDigest: "m".repeat(64), + l2LaunchDigest: "l".repeat(64), + attestation: { statement: { x: 1 }, signature: "sig" }, + }; + }, + store: async (l2) => { + storedL2 = l2; + return "stored-digest"; + }, + }); + expect(sent?.subject).toBe(perRepoPod.name); + expect(sent?.manifest).toEqual(podLaunchManifest(perRepoPod)); + expect(storedL2).toEqual({ statement: { x: 1 }, signature: "sig" }); + expect(digest).toBe("stored-digest"); + }); +});