diff --git a/packages/pds/package.json b/packages/pds/package.json index 3487e5a..75f7bd1 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -64,7 +64,8 @@ "vite": "^6.4.1", "vitest": "4.1.0-beta.1", "wrangler": "^4.93.0", - "ws": "^8.18.3" + "ws": "^8.18.3", + "@getcirrus/space-conformance": "workspace:*" }, "repository": { "type": "git", diff --git a/packages/pds/test/conformance.test.ts b/packages/pds/test/conformance.test.ts new file mode 100644 index 0000000..efa0ed1 --- /dev/null +++ b/packages/pds/test/conformance.test.ts @@ -0,0 +1,79 @@ +/** + * The conformance suite run against the real, integrated PDS worker. + * + * This complements the in-process space-routes run in @getcirrus/spaces: + * here the target is the whole worker (discovery document, blob endpoints, + * space routes together), so the operator-tier checks — including blob + * isolation, which needs com.atproto.repo.uploadBlob and + * com.atproto.sync.getBlob — execute against production code paths. + * + * Foreign harness identities are not resolvable by the worker's real DID + * resolver, so the identity-requiring credential/delegation checks skip + * here; the space-routes run covers those with a wired resolver. + */ + +import { describe, expect, it } from "vitest"; +import { filterCatalog, runChecks } from "@getcirrus/space-conformance"; +import { fullCatalog } from "@getcirrus/space-conformance/full"; +import { env, worker } from "./helpers"; + +describe("conformance suite vs the integrated PDS", () => { + it("passes the operator-tier and discovery checks, blobs included", async () => { + const fetchAdapter: typeof fetch = (input, init) => + worker.fetch(new Request(input as RequestInfo, init), env); + + const catalog = filterCatalog(fullCatalog, { + // A full PDS: operator session, the public blob endpoints, and its + // own getDelegationToken — so the operator can mint a credential for + // its own space (the bulletin self-flow) without a foreign identity. + capabilities: ["operator", "pds-blobs", "pds-delegation"], + destructive: true, + }); + const report = await runChecks({ + catalog, + context: { + target: { + origin: `https://${env.PDS_HOSTNAME}`, + did: env.DID, + implementation: "cirrus-pds", + }, + fetch: fetchAdapter, + operator: { + oauth: false, + async authorize(reqInit) { + reqInit.headers.set("Authorization", `Bearer ${env.AUTH_TOKEN}`); + }, + }, + }, + suiteVersion: "in-process", + alphaBuild: "0.0.0-spaces-alpha-20260818163953", + }); + + const failures = report.results.filter( + (r) => r.status === "fail" || r.status === "error", + ); + if (failures.length > 0) { + throw new Error( + `conformance failures:\n${failures + .map((f) => ` [${f.tier}] ${f.id}: ${f.status} — ${f.detail}`) + .join("\n")}`, + ); + } + + const byId = Object.fromEntries( + report.results.map((r) => [r.id, r.status]), + ); + // Discovery and blob isolation ran against the real worker. + expect(byId["discovery.space-host-service"]).toBe("pass"); + expect(byId["blobs.space-blob-not-public"]).toBe("pass"); + expect(byId["writes.create-and-read"]).toBe("pass"); + expect(byId["sync.getrepo-two-roots"]).toBe("pass"); + // The self-flow credential round-trip runs end-to-end: the worker mints + // its own delegation token, exchanges it for a DPoP-bound credential, + // and serves the read. This is the interop-critical path the reference + // matrix also exercises. + expect(byId["credential.self-round-trip"]).toBe("pass"); + // Identity-requiring checks skip here (no foreign resolution). + expect(byId["credential.round-trip"]).toBe("skipped"); + }); +}); diff --git a/packages/spaces/package.json b/packages/spaces/package.json index 92a62b5..cfcdbb7 100644 --- a/packages/spaces/package.json +++ b/packages/spaces/package.json @@ -1,7 +1,7 @@ { "name": "@getcirrus/spaces", "version": "0.1.0", - "description": "Atproto spaces engine (alpha) for Cloudflare Workers – permissioned repos, space hosting and credentials", + "description": "Atproto spaces engine (alpha) for Cloudflare Workers \u2013 permissioned repos, space hosting and credentials", "type": "module", "main": "dist/index.js", "files": [ @@ -37,7 +37,8 @@ "tsdown": "^0.18.3", "typescript": "^5.9.3", "vitest": "4.1.0-beta.1", - "wrangler": "^4.93.0" + "wrangler": "^4.93.0", + "@getcirrus/space-conformance": "workspace:*" }, "repository": { "type": "git", diff --git a/packages/spaces/test/conformance.test.ts b/packages/spaces/test/conformance.test.ts new file mode 100644 index 0000000..48ffed4 --- /dev/null +++ b/packages/spaces/test/conformance.test.ts @@ -0,0 +1,134 @@ +/** + * The conformance suite, run in-process against Cirrus's own space routes. + * + * This is the runner that exercises the crypto-bound catalog end to end: + * harness reader identities (did:key) are resolved by this fixture's + * `getSigningKey`, so the full delegation → credential → read dance and the + * host-role checks actually execute against the real route handlers. + * + * Blob isolation needs PDS-level endpoints outside the space routes, so it + * declares the `pds-blobs` capability this fixture does not provide and is + * reported as skipped here; the PDS integration test covers it. + */ + +import { env } from "cloudflare:test"; +import { describe, expect, it } from "vitest"; +import { Hono } from "hono"; +import type { Context } from "hono"; +import { Secp256k1Keypair } from "@atproto/crypto"; +import { filterCatalog, runChecks } from "@getcirrus/space-conformance"; +import { + KeypairIdentityProvider, + fullCatalog, +} from "@getcirrus/space-conformance/full"; +import { createSpaceRoutes } from "../src/routes"; +import type { SpaceRoutesHost } from "../src/routes"; +import { TEST_OPERATOR_DID, TEST_SIGNING_KEY } from "./fixtures/spaces-worker/index"; + +const OPERATOR = TEST_OPERATOR_DID; +const ORIGIN = "https://pds.test"; + +const operatorKeypair = await Secp256k1Keypair.import(TEST_SIGNING_KEY); + +function buildTarget() { + const host: SpaceRoutesHost = { + operatorDid: OPERATOR, + publicOrigin: ORIGIN, + // No blobs: the pds-blobs capability is not offered by this fixture. + getKeypair: async () => operatorKeypair, + getSigningKey: async (iss) => { + if (iss === OPERATOR) return operatorKeypair.did(); + // Harness identities are did:key, which self-describe their key. + if (iss.startsWith("did:key:")) return iss; + throw new Error(`unknown issuer: ${iss}`); + }, + resolveServiceEndpoint: async () => null, + resolveAuthorityEndpoint: async () => null, + verifyServiceJwt: async () => { + throw new Error("service auth not exercised by the conformance run"); + }, + authenticate: async (c: Context) => { + if (c.req.header("Authorization") === "Bearer operator-session") { + return { did: OPERATOR, fullTrust: true, allowsSpace: () => true }; + } + return c.json({ error: "AuthMissing", message: "no session" }, 401); + }, + validateRecord: ({ record }) => ({ record, status: "unknown" }), + getSpaceDO: (uri) => env.SPACES.get(env.SPACES.idFromName(uri)) as never, + getIndexDO: () => + env.SPACES_INDEX.get(env.SPACES_INDEX.idFromName("spaces")) as never, + }; + + const spaceApp = createSpaceRoutes(host); + // Wrap the space routes with the discovery document the suite probes. + const app = new Hono(); + app.get("/.well-known/did.json", (c) => + c.json({ + id: OPERATOR, + service: [ + { + id: "#atproto_space_host", + type: "AtprotoSpaceHost", + serviceEndpoint: ORIGIN, + }, + ], + verificationMethod: [{ id: `${OPERATOR}#atproto` }], + }), + ); + app.route("/", spaceApp); + + const fetchAdapter: typeof fetch = (input, init) => + app.fetch(new Request(input as RequestInfo, init)); + return { fetchAdapter }; +} + +describe("conformance suite vs Cirrus space routes", () => { + it("passes every runnable must and should check", async () => { + const { fetchAdapter } = buildTarget(); + const catalog = filterCatalog(fullCatalog, { + capabilities: ["operator", "identities"], + destructive: true, + }); + const report = await runChecks({ + catalog, + context: { + target: { origin: ORIGIN, did: OPERATOR, implementation: "cirrus" }, + fetch: fetchAdapter, + operator: { + oauth: false, + async authorize(reqInit) { + reqInit.headers.set("Authorization", "Bearer operator-session"); + }, + }, + identities: new KeypairIdentityProvider(), + }, + suiteVersion: "in-process", + alphaBuild: "0.0.0-spaces-alpha-20260818163953", + }); + + const failures = report.results.filter( + (r) => r.status === "fail" || r.status === "error", + ); + // Surface every failing check's detail so a regression is legible. + if (failures.length > 0) { + throw new Error( + `conformance failures:\n${failures + .map((f) => ` [${f.tier}] ${f.id}: ${f.status} — ${f.detail}`) + .join("\n")}`, + ); + } + + // A meaningful number of checks actually ran (not all skipped). + const ran = report.results.filter((r) => r.status === "pass"); + expect(ran.length).toBeGreaterThanOrEqual(15); + // The credential dance, delegation refusals and host gating ran here. + const byId = Object.fromEntries(report.results.map((r) => [r.id, r.status])); + expect(byId["credential.round-trip"]).toBe("pass"); + expect(byId["delegation.replay-refused"]).toBe("pass"); + expect(byId["host.member-list-gates"]).toBe("pass"); + expect(byId["sync.oplog-folds-to-commit"]).toBe("pass"); + expect(byId["host.delete-space-tombstone"]).toBe("pass"); + // Blob isolation needs a full PDS — skipped here, covered elsewhere. + expect(byId["blobs.space-blob-not-public"]).toBe("skipped"); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19f3d6e..f1c54e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,6 +289,9 @@ importers: '@cloudflare/workers-types': specifier: ^4.20260524.1 version: 4.20260524.1 + '@getcirrus/space-conformance': + specifier: workspace:* + version: link:../space-conformance '@ipld/car': specifier: ^5.4.2 version: 5.4.2 @@ -402,6 +405,9 @@ importers: '@cloudflare/workers-types': specifier: ^4.20260524.1 version: 4.20260524.1 + '@getcirrus/space-conformance': + specifier: workspace:* + version: link:../space-conformance publint: specifier: ^0.3.16 version: 0.3.17