Skip to content

Commit b2de77a

Browse files
authored
Add pdscheck — pure-client-side PDS conformance verifier (#174)
* feat(check): add pdscheck — a pure-client-side PDS conformance verifier New app at apps/check/ (deployed to check.cirrus.earth). Solid + Vite + Tailwind v4, served as static assets via Wrangler. Tests AT Protocol PDS conformance across three modes: - Read-only: identity resolution, server endpoints, repo reads, sync 1.1 firehose, blobs, OAuth discovery - Write tests: lifecycle of createRecord / applyWrites / uploadBlob / putRecord / deleteRecord (OAuth-authenticated, ephemeral session) - OAuth conformance: full PAR + DPoP + PKCE + iss + revocation flow, with isolated probes for unregistered redirect_uri rejection, permission-set resolution (bogus, advertised, and published site.standard.authFull), and post-token scope/boundary enforcement Firehose sampling uses cursor=0 historical replay with cap (200) / timeout (8s) / inactivity (1.5s) / diversity-aware exit (creates + updates/deletes seen). Reports termination reason so users can tell "hit the cap" from "PDS went idle". Validates every response against atcute lexicon schemas and links every check to its spec section. Designed to be a one-stop conformance check for anyone running a PDS implementation. * fix(check): handle permission-set expansion in scope-echoed check Permission sets get resolved by the AS into either an echoed include: literal (the grant preserves the reference; AS recomputes at refresh) or expanded resource scopes (a snapshot of the computed permissions). Both are spec-valid — atproto.com/specs/permission doesn't mandate either representation in the token's scope claim. Previously the check flagged the expanded form as a "narrowing" because the include: token was dropped and replaced with repo?collection=X&... tokens. Now it recognizes pure expansion (only include: dropped, scopes added) as a pass with the expansion surfaced as evidence. Also teach scopeGrantsWriteTo about the multi-collection token form (repo?collection=X&collection=Y) that expansion produces, alongside the existing single-collection form (repo:X). Without this, boundary write checks falsely reported no write coverage after a permission set expanded. * fix(check): align granular-scope discovery checks with actual spec Two of the three granular-scope advertisement checks were checking for syntax that pre-dated the final atproto.com/specs/permission spec and flagged spec-conformant ASes as warnings: - scope-phase2-granular looked for repo:read, repo:write:<nsid>, account.*, pds.* — none of these are in the actual spec. The real grammar uses bare resource-type tokens (repo, rpc, blob, account, identity) with parameters appended at request time by the client. Removed entirely; it was redundant with scope-resource-buckets. - scope-permission-sets looked for include:<nsid> strings in scopes_supported, but specific NSIDs are dynamically resolved at PAR time via lexicon resolution — the AS only advertises bare `include` as a resource type. Updated to check for that. - scope-resource-buckets reworked to expect all five resource tokens (repo, rpc, blob, identity, account) and warn on partial coverage. Spec URLs now point at atproto.com/specs/permission instead of the obsolete proposal discussion thread. * feat(check): pivot to other check modes from OAuth result page The OAuth conformance result only offered "verify a different account", forcing users back to the landing page when they wanted to also run read-only or write tests against the same target. Add the same pivot buttons RunView already shows. The handlers exit the flow first (clearing flow state and signing out), then start the requested run mode against the OAuth target. * fix(check): drop grade letter, keep score * feat(check): sample live firehose during write probe The anonymous firehose sample is historical (cursor=0), so on long-lived PDSes every #commit frame can predate the Sync 1.1 upgrade — producing false fails on `commit-has-prevdata` and `commit-ops-have-prev`. Two changes: - Anonymous: downgrade the strict pass/fail to a tri-state. Any sampled frame with prevData → pass. None at all → warn (ambiguous: PDS may not support Sync 1.1, or the sample may predate the upgrade). Same shape for ops[].prev on update/delete. - Authenticated write probe: subscribe to the firehose with no cursor before the create/applyWrites/uploadBlob/delete run, then close + validate after. Fresh frames go through the same validators in strict mode, giving a definitive Sync 1.1 verdict. * Fix docs builds * chore(check): drop empty test script The package has no test files; vitest run was failing CI with "No test files found" plus a jsdom env-detection complaint. Removing the script lets pnpm's filtered test runner skip the package cleanly. * chore(check): drop unused deps + knip ignores worktrees apps/check was pulling six runtime deps and vitest with no consumers: @atcute/bluesky, @atcute/tid, @kobalte/core, @solid-primitives/storage, jose, multiformats. Drop them from package.json. Also un-export two oauth-flow helpers that are only used within the same module. Ignore .claude/** in knip — local subagent worktrees pollute results without affecting CI.
1 parent 79200c1 commit b2de77a

47 files changed

Lines changed: 10548 additions & 378 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/check/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules
2+
dist
3+
.wrangler
4+
.dev.vars*
5+
!.dev.vars.example
6+
.env*
7+
!.env.example
8+
*.tsbuildinfo
9+
.DS_Store
10+
*.log

apps/check/index.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6+
<meta name="color-scheme" content="light dark" />
7+
<meta name="description" content="Verify your AT Protocol PDS." />
8+
<title>☁️ check</title>
9+
<link
10+
rel="icon"
11+
href="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%3E%3Ctext%20y='.9em'%20font-size='90'%3E%E2%98%81%EF%B8%8F%3C/text%3E%3C/svg%3E"
12+
/>
13+
</head>
14+
<body>
15+
<div id="root"></div>
16+
<script type="module" src="/src/main.tsx"></script>
17+
</body>
18+
</html>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"lexicon": 1,
3+
"id": "earth.cirrus.check.testrecord",
4+
"description": "Ephemeral test record created by pdscheck during PDS write verification. Records of this type are created and deleted within a single verification run; any leftover record can be safely deleted by hand. The namespace does not federate to app.bsky.* services.",
5+
"defs": {
6+
"main": {
7+
"type": "record",
8+
"description": "A disposable verification artifact.",
9+
"key": "tid",
10+
"record": {
11+
"type": "object",
12+
"required": ["createdAt"],
13+
"properties": {
14+
"createdAt": {
15+
"type": "string",
16+
"format": "datetime",
17+
"description": "When this record was created."
18+
},
19+
"message": {
20+
"type": "string",
21+
"description": "Human-readable identifier — typically a fixed string flagging that the record is safe to delete.",
22+
"maxLength": 256
23+
},
24+
"verifier": {
25+
"type": "string",
26+
"format": "uri",
27+
"description": "Origin of the pdscheck instance that created the record, for tracing leftovers back to a specific verifier deployment.",
28+
"maxLength": 256
29+
},
30+
"runId": {
31+
"type": "string",
32+
"description": "Identifier of the verification run that created the record. Useful for correlating leftovers with a specific test execution.",
33+
"maxLength": 64
34+
},
35+
"blob": {
36+
"type": "blob",
37+
"description": "Optional blob attachment used to exercise uploadBlob and blob references.",
38+
"accept": ["*/*"],
39+
"maxSize": 1048576
40+
}
41+
}
42+
}
43+
}
44+
}
45+
}

apps/check/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@getcirrus/check",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"description": "Web-based PDS verifier for AT Protocol",
7+
"scripts": {
8+
"dev": "vite",
9+
"build": "vite build",
10+
"preview": "vite preview",
11+
"wrangler:dev": "wrangler dev",
12+
"deploy": "vite build && wrangler deploy",
13+
"check": "tsc --noEmit"
14+
},
15+
"dependencies": {
16+
"@atcute/atproto": "^4.0.0",
17+
"@atcute/cbor": "^2.3.3",
18+
"@atcute/cid": "^2.4.1",
19+
"@atcute/client": "^5.0.0",
20+
"@atcute/identity": "^2.0.0",
21+
"@atcute/identity-resolver": "^2.0.0",
22+
"@atcute/lexicons": "^2.0.0",
23+
"@atcute/oauth-browser-client": "^4.0.0",
24+
"@ipld/car": "^5.4.6",
25+
"idb-keyval": "^6.2.4",
26+
"oauth4webapi": "^3.8.6",
27+
"solid-js": "^1.9.13"
28+
},
29+
"devDependencies": {
30+
"@tailwindcss/vite": "^4.3.0",
31+
"tailwindcss": "^4.3.0",
32+
"typescript": "^6.0.3",
33+
"vite": "^8.0.14",
34+
"vite-plugin-solid": "^2.11.12",
35+
"wrangler": "^4.94.0"
36+
}
37+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"client_id": "https://check.cirrus.earth/client-metadata.json",
3+
"client_name": "check · a PDS validator",
4+
"client_uri": "https://check.cirrus.earth",
5+
"redirect_uris": [
6+
"https://check.cirrus.earth/oauth/callback",
7+
"https://check.cirrus.earth/oauth/flow-callback"
8+
],
9+
"scope": "atproto transition:generic repo:earth.cirrus.check.testrecord include:site.standard.authFull",
10+
"grant_types": ["authorization_code", "refresh_token"],
11+
"response_types": ["code"],
12+
"application_type": "web",
13+
"token_endpoint_auth_method": "none",
14+
"dpop_bound_access_tokens": true
15+
}

0 commit comments

Comments
 (0)