From 290060577da5dfa99e2fa861f47c19befc0573f0 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Wed, 27 May 2026 16:58:15 -0500 Subject: [PATCH] docs: align implementation notes --- AGENTS.md | 7 +++++-- README.md | 6 +++++- bench/README.md | 28 +++++++++++---------------- docs/architecture.md | 27 +++++++++++++++++++++----- docs/benchmarking.md | 12 +++++++----- docs/development.md | 25 +++++++++++++++++++++++- docs/operations.md | 19 +++++++++++++++--- docs/passkeys.md | 46 ++++++++++++++++++++++++++++++++++++++++++++ docs/references.md | 3 +++ 9 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 docs/passkeys.md diff --git a/AGENTS.md b/AGENTS.md index 3cdaef6..d322751 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,8 +30,8 @@ Do not edit `zat` from this repo unless explicitly asked. - `atproto/proxy`: generic service proxying through `atproto-proxy` - `storage`: SQLite state, repo commits, MST updates, blobstore metadata - `internal`: ZDS-local utilities that keep feature code small; use this for - reusable glue such as CLI parsing, sharded locks, and JOSE compatibility - helpers before deciding whether a primitive belongs in `zat`. + reusable glue such as CLI parsing, sharded locks, passkey adapters, and JOSE + compatibility helpers before deciding whether a primitive belongs in `zat`. - `bench`, `tools`, `justfile`: local smoke tests, admin operations, and benchmark entry points. Prefer Just targets for repeatable workflows. @@ -46,6 +46,9 @@ Do not edit `zat` from this repo unless explicitly asked. `validationStatus: "valid"` without validation. - Invite-required deployments must advertise `inviteCodeRequired: true` and consume invite codes during account creation. +- Passkeys are stored as account credentials: credential ID, public key, + signature counter, friendly name, and timestamps. Password login remains + available unless an explicit account policy changes that. - OAuth should follow the ATProto OAuth profile and JOSE behavior without relaxing stricter repo-signature verification semantics. - Appview behavior belongs behind the generic proxy path, not in local diff --git a/README.md b/README.md index 9c7447d..dbfcf9c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ an at protocol personal data server. `zds` stores atproto accounts, repos, records, blobs, sessions, oauth state, and identity state. it uses [`zat`](../zat) for protocol primitives: syntax, tids, did resolution, jwt helpers, dag-cbor, car, mst, repo verification, and key -encoding. +encoding. WebAuthn support comes from +[`webauthn`](https://tangled.org/zzstoatzz.io/webauthn). ## docs @@ -15,6 +16,7 @@ encoding. - [development](docs/development.md) - [operations](docs/operations.md) - [invite codes](docs/invite-codes.md) +- [passkeys](docs/passkeys.md) - [references](docs/references.md) - [benchmarks](bench/README.md) @@ -100,6 +102,8 @@ just docker-publish-current - Invite codes use the official PDS table shape: code metadata plus recorded uses. When invites are required, `createAccount` rejects missing, disabled, or exhausted codes. +- Passkeys are optional account credentials for the OAuth login page. ZDS stores + credential IDs, public keys, counters, names, and last-use timestamps. ## references diff --git a/bench/README.md b/bench/README.md index b7f5a59..d86439b 100644 --- a/bench/README.md +++ b/bench/README.md @@ -38,8 +38,7 @@ just bench run --scenario write --records 10000 - `repo`: full repo CAR materialization through `writeRepoCar`. - `blob`: blob write/read against disk blobstore plus SQLite metadata. - `metastore`: Tranquil-shaped apply/get/list benchmark with caller counts and - latency percentiles. Its get/list rows are ZDS-local probes, not all direct - Tranquil comparisons. + latency percentiles. - `get-cid`: CID-only record lookup, matching Tranquil's `get_record_cid` metastore benchmark. - `get-block`: record index lookup plus `repo_blocks` byte fetch. @@ -110,13 +109,10 @@ just bench get-cid 10 1000 just bench get-cid 100 200 ``` -This matrix only includes operation/count pairs measured with the same unit of -work. Tranquil's `get_record_cid` and `list_records` are record-index -operations: `list_records` returns `rkey + record_cid`, not record body JSON. -The official PDS probe measures its actor-store read paths with Vitest. Its -single-operation rows are apples-to-apples for the internal read boundaries. Its -batched CID rows are useful throughput shape checks, but their p99 values are -batch latencies, not per-operation latencies from the ZDS/Tranquil harness. +This matrix includes only operation/count pairs measured with the same unit of +work. Tranquil's `get_record_cid` is a record-index operation. The official PDS +probe measures its actor-store read paths with Vitest; its single-operation rows +are included for matching internal read/write boundaries. Summary: ZDS is fastest in the single-caller read probes. Tranquil is fastest in the direct 10/100 caller CID lookup probes. The official PDS is slower in these @@ -130,8 +126,8 @@ than Tranquil at 100 callers but with a worse p99/max tail. | apply one record commit | 1 | 642 ops/s, p95 2.9 ms | 287 ops/s, p95 4.1 ms | 448 ops/s, p99 4.0 ms | | current record CID lookup | 1 | 414k ops/s, p95 4 us | 96.8k ops/s, p95 15 us | 3.1k ops/s, p99 511 us | -The direct caller-count concurrency matrix is only ZDS and Tranquil. The -official-PDS rows below it use a batched Vitest probe and are kept separate. +The direct caller-count concurrency matrix is ZDS and Tranquil only. Official +PDS concurrency probes are kept separate until they use the same caller model. | operation | callers | ops | zds | tranquil | |---|---:|---:|---:|---:| @@ -148,9 +144,9 @@ Official PDS batched CID lookup: | current record CID lookup | 100 callers x 20 lookups | 3.2k lookups/s | 630 ms | Tranquil's metastore `list_records` benchmark is not in this table because the -current ZDS list probe follows the official Bluesky PDS read shape: join the -record index to `repo_blocks`, decode DAG-CBOR, and return record values. An -index-only list probe should be reported separately when it exists. +ZDS row reports the official-PDS-shaped `listRecords`: record index lookup, +`repo_blocks` fetch, DAG-CBOR decode, and JSON materialization. Add an +index-only ZDS list probe before comparing that row. ## official PDS probe @@ -171,9 +167,7 @@ then measures actor-store reads directly: | full getRecord materialization | 3.0k ops/s, p99 525 us | | full listRecords, limit 50 | 635 ops/s, p99 2.4 ms | -These are useful for the official-PDS column, but they should not be mixed with -ZDS/Tranquil direct caller-count latency rows. The matching ZDS single-caller -rows are: +The matching ZDS single-caller rows are: | operation | zds | |---|---:| diff --git a/docs/architecture.md b/docs/architecture.md index 7eaeb01..d99aa96 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -13,16 +13,16 @@ path when clients send the proxy header. helpers, DAG-CBOR, CAR, MST, repo verification, OAuth helpers, and streaming clients. - `zds` owns PDS policy and persistence: accounts, repo writes, sessions, - storage, server routes, sync production, migration, and compatibility - behavior. + storage, server routes, sync production, account migration, and the local + login/security surface. - SQLite stores account, repo, commit, token, OAuth, blob metadata, and identity state. - Invite codes follow the official PDS split between code metadata and recorded uses; account creation consumes a code while holding the store lock. - Blob bytes live in the disk blobstore rooted at `ZDS_BLOBSTORE_PATH`. -- JOSE compatibility helpers live under `internal` until they are clean enough - to upstream to `zat`; OAuth JWT verification must accept normal JOSE ECDSA - signatures without relaxing repo signature verification. +- Passkey storage follows Tranquil's core shape: credential ID, public key, + signature counter, friendly name, and timestamps. Passkeys are outside the + official PDS surface ZDS tracks for compatibility. ## repo write invariants @@ -47,3 +47,20 @@ preparation and Tranquil's validation/conformance model. visibility depends on valid repo commits, coherent event frames, and crawl requests. `ZDS_CRAWLERS` should include both `https://bsky.network` and `https://vsky.network` for current deployment coverage. + +## auth and login + +OAuth follows the ATProto OAuth profile: PAR is required, redirect URIs are +validated against client metadata, permission-set includes must resolve, tokens +enforce granular repo/blob/rpc/account/identity scopes, and revocation affects +resource-server checks. + +Password sessions and app passwords match the reference PDS account model. +Passkeys are an additional OAuth login credential inspired by Tranquil, backed +by the local `webauthn` dependency and stored in SQLite. Discoverable passkey +login is supported so an account can be chosen by the authenticator during the +OAuth flow. + +PDS-owned `app.bsky.actor` preferences are handled locally because the official +PDS implements those routes. Appview reads and writes remain behind the generic +`atproto-proxy` path. diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 6770586..f3049c5 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -13,21 +13,23 @@ operation. ## current matrix -The active three-way matrix is: +The active apples-to-apples matrix is intentionally small: - apply one record commit - current record CID lookup + +Adjacent probes are tracked separately: + - record-index list - full record materialization - blob/block put and get - repo export - sync event delivery -ZDS, Tranquil, and the official PDS now have matching single-caller numbers for +ZDS, Tranquil, and the official PDS have matching single-caller numbers for apply-one-record and current-CID lookup. ZDS and Tranquil also have direct -10/100 caller concurrency curves. The official probe has a batched CID lookup -shape check, but its latency values are batch latencies from Vitest, not per-op -latencies from the same harness as the ZDS and Tranquil rows. +10/100 caller concurrency curves. Official-PDS batch probes stay outside the +main matrix until they are measured through the same unit of work. ## read path diff --git a/docs/development.md b/docs/development.md index 9d4f0e4..5b1e859 100644 --- a/docs/development.md +++ b/docs/development.md @@ -17,7 +17,7 @@ quickly. blob metadata. Runtime code reads record bodies from `repo_blocks`, not from cached JSON columns. - `src/internal` is ZDS-local reusable glue. Good candidates are CLI parsing, - sharded synchronization, JOSE compatibility, and narrow encoding adapters. + sharded synchronization, passkey adapters, and narrow encoding adapters. Protocol primitives still belong in `zat` once they are general enough. - `bench` and `tools` contain repeatable local probes. Prefer Just targets over one-off command lines. @@ -33,6 +33,11 @@ When ZDS needs a generally useful primitive that `zat` does not expose yet, build the smallest local version under `src/internal`, use it from ZDS, and document why it is likely upstream material. +For generated JSON, prefer typed structs plus `std.json.Stringify.valueAlloc` +or a narrow helper over hand-written object strings. `std.json.fmt` is still +useful at stream boundaries, but new response code should avoid assembling JSON +objects with format strings when a structured value is straightforward. + ## lessons from history - Browser behavior wins over assumptions. If a client reports a failure, check @@ -43,6 +48,9 @@ document why it is likely upstream material. `validationStatus: "valid"` without validation. - OAuth client assertions are JOSE signatures. Accept standard raw ECDSA JWS signatures for OAuth without weakening stricter repo-signature verification. +- Passkey support should stay close to Tranquil's WebAuthn model: account-owned + credential rows plus challenge rows, with browser prompts opened directly from + user gestures. - Storage shape follows the official PDS SQLite model where practical: record index rows identify current CIDs, and canonical record content is stored as DAG-CBOR blocks. @@ -54,3 +62,18 @@ document why it is likely upstream material. Local SQLite files under `dev/` are ignored workspace state. Do not commit database files, WAL files, `.env`, `.zig-cache`, `zig-out`, or package cache directories. + +## commands + +The root `justfile` is the stable task surface: + +```sh +just test +just smoke +just invite https://pds.zat.dev +just plc-repair did:plc:... +just bench all +just docker-publish-current +``` + +Run `zig zen` before committing. diff --git a/docs/operations.md b/docs/operations.md index b2234a7..fda0411 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -5,12 +5,12 @@ Run these before committing: ```sh -zig build test -tools/smoke.sh +just test +just smoke zig zen ``` -`tools/smoke.sh` starts a local server, creates a session, writes records, +`just smoke` starts a local server, creates a session, writes records, checks blob upload, verifies repo/sync endpoints, and asserts that known app records use valid key shapes. @@ -72,6 +72,8 @@ Common deployment settings: including invite-code minting. - `ZDS_INVITE_REQUIRED`: set to `true` to require invite codes for account creation. `describeServer` reflects this value. +- Passkeys do not require a separate deployment secret. WebAuthn RP ID is + derived from the public URL host. PLC key lifecycle: @@ -126,3 +128,14 @@ ZDS supports browser-based account migration flows such as PDS Moover: Migration requires working public HTTPS, stable JWT secret, email delivery, blob storage, and writable SQLite storage. + +## conformance + +Useful external checks: + +- `https://check.cirrus.earth/?target=` for PDS and OAuth conformance. +- bsky.app and pdsls.dev for browser-visible OAuth, proxy, repo, and blob + behavior. + +Treat browser failures as request/response evidence: inspect the failing XRPC, +status code, response body, and relevant server logs before changing behavior. diff --git a/docs/passkeys.md b/docs/passkeys.md new file mode 100644 index 0000000..eb130f9 --- /dev/null +++ b/docs/passkeys.md @@ -0,0 +1,46 @@ +# passkeys + +ZDS supports WebAuthn passkeys for OAuth login. This is modeled after +Tranquil's passkey support; passkeys are outside the official Bluesky PDS +surface ZDS tracks for compatibility. + +## storage + +Each passkey row belongs to an account DID and stores: + +- credential ID +- credential public key +- signature counter +- friendly name +- created and last-used timestamps + +Challenge rows are short-lived. Registration challenges are tied to the account +DID. Discoverable login challenges are tied to the OAuth request so an +authenticator can identify the account by credential ID. + +## login behavior + +If the login hint resolves to an account with saved passkeys, the OAuth page +offers passkey login first and keeps password login available. If no passkey is +saved for the account, password login is shown directly with a link to the +security page. + +Discoverable passkey login is supported: the browser may return a credential +without the page first choosing an account. ZDS resolves the credential ID to +the stored account, verifies the assertion, updates the signature counter, and +authorizes the OAuth request. + +## management + +Passkeys are managed from `/security`. That page signs in with the account +password, lists saved passkeys, and supports add, rename, and delete. + +Deleting a passkey removes it from ZDS. The browser, OS, or password manager +may still keep its copy, but it can no longer be used with this PDS. + +## references + +- Tranquil frontend: `frontend/src/routes/OAuthLogin.svelte`, + `frontend/src/routes/Settings.svelte` +- Tranquil storage traits: `crates/tranquil-db-traits/src/user.rs` +- ZDS WebAuthn dependency: diff --git a/docs/references.md b/docs/references.md index 35f8543..dc99ce0 100644 --- a/docs/references.md +++ b/docs/references.md @@ -47,6 +47,9 @@ - Invite codes are core PDS state. Captcha, 2FA, and migration-only policies can live at the PDS boundary or a reverse proxy boundary; pds-gatekeeper is useful prior art for that layer, not a substitute for invite-code accounting. +- Passkeys are Tranquil-derived, not reference-PDS-derived: store credential + IDs, public keys, counters, names, and timestamps, and verify WebAuthn + assertions before authorizing OAuth requests. ## Tranquil comparison -- 2.51.2