Skip to content

Add pdscheck — pure-client-side PDS conformance verifier#174

Merged
ascorbic merged 10 commits into
mainfrom
add-pdscheck-app
May 24, 2026
Merged

Add pdscheck — pure-client-side PDS conformance verifier#174
ascorbic merged 10 commits into
mainfrom
add-pdscheck-app

Conversation

@ascorbic
Copy link
Copy Markdown
Owner

Summary

  • New app at apps/check/ (deployed at check.cirrus.earth) — a fully client-side AT Protocol PDS conformance verifier
  • Three modes: read-only checks, OAuth-authenticated write lifecycle, and a deep OAuth conformance flow
  • Validates every response against atcute lexicon schemas; every check links to its spec section
  • Firehose sampling uses cursor=0 historical replay with smart termination (cap / timeout / inactivity / diversity-aware exit) so idle PDSes don't drag and busy PDSes give diverse samples

What it tests

Read-only: identity resolution (handle / DID / combined), server endpoints, repo reads (describe / get / list), sync 1.1 firehose (#commit prevData, blocks-as-CAR, ops-have-prev, #sync / #account / #identity event presence), blobs, OAuth discovery (protected-resource + auth-server + JWKS).

Write tests: createRecord / applyWrites / uploadBlob / putRecord / deleteRecord against an earth.cirrus.check.testrecord lexicon (published in this PR). Uses ephemeral OAuth — auto signs out after each run.

OAuth conformance: full PAR + DPoP + PKCE + iss + revocation flow, with isolated PAR probes (unregistered redirect_uri rejection, bogus permission set rejection, advertised include acceptance, and resolution of the published site.standard.authFull permission set lexicon). Post-token: scope-echo, DPoP-bound API call, in-scope and out-of-scope boundary writes, refresh, revocation.

Test plan

  • Run read-only checks against bsky reference PDS — all pass or warn with clear messages
  • Run read-only checks against a small/idle self-hosted PDS — firehose terminates on inactivity, doesn't hang
  • Run write tests against a granular-scope PDS — full lifecycle completes, ephemeral session cleaned up
  • Run OAuth conformance flow against bsky — all PAR probes resolve, including site.standard.authFull
  • Run OAuth conformance flow against a granular-only PDS that doesn't resolve include: — real flow fails at send-par with a clear scope error, isolated probe pinpoints the gap
  • PDS-URL mode (https:// input): identity/repo checks skip cleanly, server/sync/OAuth-discovery still run
  • Findings markdown downloads correctly; "verify a different account" pivots between modes

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.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 24, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
atproto-pds d3dea3b May 24 2026, 07:50 PM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 24, 2026

Open in StackBlitz

npm i https://pkg.pr.new/create-pds@174
npm i https://pkg.pr.new/@getcirrus/oauth-provider@174
npm i https://pkg.pr.new/@getcirrus/pds@174

commit: d3dea3b

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.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 24, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
pdscheck d3dea3b May 24 2026, 07:50 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 24, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
cirrusdocs d3dea3b Commit Preview URL

Branch Preview URL
May 24 2026, 07:50 PM

ascorbic added 8 commits May 24, 2026 19:40
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.
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.
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.
# Conflicts:
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
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.
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.
@ascorbic ascorbic merged commit b2de77a into main May 24, 2026
7 checks passed
@ascorbic ascorbic deleted the add-pdscheck-app branch May 24, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant