Commit b2de77a
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
File tree
- apps/check
- lexicons/earth/cirrus/check
- public
- src
- checks
- components
- lib
- demos/pds
- docs
- packages/pds
- e2e/fixture
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
0 commit comments