diff --git a/.changeset/applywrites-result-cid.md b/.changeset/applywrites-result-cid.md deleted file mode 100644 index 498df2bf..00000000 --- a/.changeset/applywrites-result-cid.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@getcirrus/pds": patch ---- - -`applyWrites` now returns the record CID on `createResult` and `updateResult` even when the record is removed later in the same batch. The lexicon marks `cid` as required, but the previous code looked it up in the post-commit MST — for a record that was created then deleted within one batch, the MST has no entry and the field was missing. The CID is now computed from the record bytes up front, matching reference PDS behaviour. diff --git a/.changeset/applywrites-same-rkey-batch.md b/.changeset/applywrites-same-rkey-batch.md deleted file mode 100644 index c07dc0b2..00000000 --- a/.changeset/applywrites-same-rkey-batch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@getcirrus/pds": patch ---- - -`com.atproto.repo.applyWrites` now accepts batches that touch the same rkey more than once, matching the reference PDS. The common case is a create followed by a delete on the same rkey within one batch (an atomic no-op pattern several clients rely on); previously Cirrus rejected this with `400 InvalidRequest: duplicate rkey in batch`. Two creates on the same rkey still fail, but now as `409 RecordAlreadyExists` from the repo layer rather than a pre-flight 400. diff --git a/.changeset/oauth-jwks-uri.md b/.changeset/oauth-jwks-uri.md deleted file mode 100644 index 659d7d58..00000000 --- a/.changeset/oauth-jwks-uri.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@getcirrus/oauth-provider": minor -"@getcirrus/pds": patch ---- - -Advertise a `jwks_uri` in OAuth authorization-server metadata and serve an empty JWKS at `/oauth/jwks`. OAuth clients that run JWKS discovery against the metadata endpoint no longer fail when talking to Cirrus. The key set is empty because Cirrus signs access tokens with HS256 (symmetric `JWT_SECRET`) — there are no public keys to publish. diff --git a/.changeset/oauth-scope-query-form.md b/.changeset/oauth-scope-query-form.md deleted file mode 100644 index 52998ba7..00000000 --- a/.changeset/oauth-scope-query-form.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@getcirrus/oauth-provider": patch ---- - -Fix `parseScope` rejecting valid granular scopes that use the query-only form (e.g. `repo?collection=a&collection=b`) with `Unknown scope resource`. The parser previously only looked for `:` as the prefix delimiter, but per `@atproto/oauth-scopes` syntax a scope can use `prefix:positional`, `prefix?query`, or both. This affected permission sets whose `repo` permission listed multiple collections, since those expand to a single query-form token. diff --git a/.changeset/par-resolve-includes-eagerly.md b/.changeset/par-resolve-includes-eagerly.md deleted file mode 100644 index 9c945703..00000000 --- a/.changeset/par-resolve-includes-eagerly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@getcirrus/oauth-provider": patch ---- - -PAR (`/oauth/par`) now resolves every `include:` permission-set scope eagerly and rejects with `invalid_scope` when an include points at a nonexistent or non-permission-set lexicon. Previously the resolver only ran at the authorize step, so clients with a typo in an include scope got a fresh `request_uri` from PAR and only learned about the bad scope at consent time. Matches reference oauth-provider behaviour (`request-manager.ts:297-313`). diff --git a/.changeset/par-validate-redirect-uri.md b/.changeset/par-validate-redirect-uri.md deleted file mode 100644 index da6f1326..00000000 --- a/.changeset/par-validate-redirect-uri.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@getcirrus/oauth-provider": patch ---- - -PAR (`/oauth/par`) now validates `redirect_uri` against the client's registered redirect_uris at push time. Previously the check only ran at the authorize step, which let a malicious caller obtain a `request_uri` for an unregistered redirect even though the subsequent authorize would have rejected it. Reject early per RFC 6749 §3.1.2.4. diff --git a/.changeset/pds-identity-endpoints.md b/.changeset/pds-identity-endpoints.md deleted file mode 100644 index fa4711ad..00000000 --- a/.changeset/pds-identity-endpoints.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@getcirrus/pds": minor ---- - -Implement three PDS-side identity endpoints that previously fell through to the AppView proxy and returned 501: - -- `com.atproto.identity.resolveDid` returns the DID document for the local account. -- `com.atproto.identity.resolveIdentity` returns `{did, handle, didDoc}` for the local handle or DID. -- `com.atproto.identity.getRecommendedDidCredentials` (authenticated) returns the rotation keys, `alsoKnownAs`, verification methods, and PDS service entry that a migrating account should advertise. - -Requests for foreign DIDs or handles continue to fall through to the AppView proxy unchanged. diff --git a/.changeset/scopes-supported-spec-compliant.md b/.changeset/scopes-supported-spec-compliant.md deleted file mode 100644 index 7a2b1784..00000000 --- a/.changeset/scopes-supported-spec-compliant.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@getcirrus/oauth-provider": patch ---- - -`scopes_supported` in the authorization-server metadata now lists only the values the spec calls out: `atproto`, `transition:generic`, `transition:email`, `transition:chat.bsky`. Granular resource scopes (`repo:`, `rpc:`, `blob:`, `account:<…>`, `identity:<…>`) and permission-set scopes (`include:`) are parameterised and aren't enumerable, so bare prefixes like `repo` or `include` are no longer advertised — clients discover support by attempting the scope and falling back on `invalid_scope`, matching the reference PDS. diff --git a/.changeset/session-auth-polish.md b/.changeset/session-auth-polish.md deleted file mode 100644 index 7a070660..00000000 --- a/.changeset/session-auth-polish.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@getcirrus/pds": patch ---- - -Fix three conformance issues found by pdscheck: - -- `com.atproto.server.getSession` now accepts OAuth access tokens presented with the `DPoP` scheme (RFC 9449), not just `Bearer`. OAuth clients can now read session info without first being rejected with 401. -- `com.atproto.server.listAppPasswords` returns `createdAt` as an RFC 3339 datetime (e.g. `2026-03-29T15:30:17.000Z`) instead of the SQLite `"YYYY-MM-DD HH:MM:SS"` form that violated the lexicon. -- `com.atproto.server.getAccountInviteCodes` is now implemented and returns `{ codes: [] }` for authenticated callers (Cirrus has `inviteCodeRequired: false`, so there are no invite codes to list). Previously it fell through to the AppView proxy and returned 501. diff --git a/.changeset/sync-1-1-compliance.md b/.changeset/sync-1-1-compliance.md deleted file mode 100644 index a7c9f6cb..00000000 --- a/.changeset/sync-1-1-compliance.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@getcirrus/pds": minor ---- - -The firehose now emits the sync 1.1 message shape, matching what the bsky.network relay and other AT Protocol consumers expect. Existing subscribers will start seeing new fields and new event types; nothing has to change on the consumer side, but the warnings some relays were logging against Cirrus hosts (notably `missing prevData field`) will stop. - -What changed on the wire: - -- `#commit` messages now include `prevData` (the prior commit's MST root CID), so relays can verify each commit inductively without re-fetching the repo. The CAR slice now also carries the MST covering-proof blocks needed for that verification. -- Each `ops[]` entry on update and delete now includes `prev`, the previous CID of the touched record. Creates omit it as before. -- `tooBig` is always `false`. It was previously set based on payload size, which never matched the field's meaning under sync 1.1. -- New `#account` events are emitted on activation and deactivation, so relays learn about account status changes without polling. Deactivation reports `status: "deactivated"`; activation reports `active: true` with no status. -- New `#sync` events are emitted on activation (after migration or initial setup), giving relays the current commit block without a diff. -- `#identity` events now allow the `handle` field to be omitted, per spec. -- A `#info` frame with `name: "OutdatedCursor"` is sent when a client connects with a cursor older than the retained event window. The stream continues from the oldest available event instead of disconnecting. -- `applyWrites` rejects calls with more than 200 operations, matching the spec cap. diff --git a/.changeset/sync-get-latest-commit.md b/.changeset/sync-get-latest-commit.md deleted file mode 100644 index 28b4b637..00000000 --- a/.changeset/sync-get-latest-commit.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@getcirrus/pds": minor ---- - -Implement `com.atproto.sync.getLatestCommit`. - -This sync XRPC endpoint was previously unimplemented, so requests fell through to the XRPC proxy and returned `501 MethodNotImplemented`. Relays call `getLatestCommit` during their crawl bootstrap, so a freshly created repo could never be indexed by a fresh `requestCrawl`. The endpoint now returns the repo's head commit as `{ cid, rev }` (sourced from the same `rpcGetRepoStatus` data used by `getRepoStatus`/`listRepos`). diff --git a/.changeset/sync-list-repos-by-collection.md b/.changeset/sync-list-repos-by-collection.md deleted file mode 100644 index 7eebfb13..00000000 --- a/.changeset/sync-list-repos-by-collection.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@getcirrus/pds": minor ---- - -Implement `com.atproto.sync.listReposByCollection`. - -Relays and crawlers use this endpoint to discover which PDSes host repos that contain a given record collection. The PDS now answers with `{ repos: [{ did }] }` when its account has at least one record in the requested collection, or an empty list otherwise. Invalid or missing `collection` parameters return `InvalidRequest`. diff --git a/packages/oauth-provider/CHANGELOG.md b/packages/oauth-provider/CHANGELOG.md index 5c69ee32..5ac4888a 100644 --- a/packages/oauth-provider/CHANGELOG.md +++ b/packages/oauth-provider/CHANGELOG.md @@ -1,5 +1,21 @@ # @getcirrus/oauth-provider +## 0.5.0 + +### Minor Changes + +- [#177](https://github.com/ascorbic/cirrus/pull/177) [`ec284fd`](https://github.com/ascorbic/cirrus/commit/ec284fde3544e9ea4e2b6b085f718de449e4862e) Thanks [@ascorbic](https://github.com/ascorbic)! - Advertise a `jwks_uri` in OAuth authorization-server metadata and serve an empty JWKS at `/oauth/jwks`. OAuth clients that run JWKS discovery against the metadata endpoint no longer fail when talking to Cirrus. The key set is empty because Cirrus signs access tokens with HS256 (symmetric `JWT_SECRET`) — there are no public keys to publish. + +### Patch Changes + +- [#175](https://github.com/ascorbic/cirrus/pull/175) [`54ab459`](https://github.com/ascorbic/cirrus/commit/54ab459588393a58ea906977c1ffc8996d8d0700) Thanks [@ascorbic](https://github.com/ascorbic)! - Fix `parseScope` rejecting valid granular scopes that use the query-only form (e.g. `repo?collection=a&collection=b`) with `Unknown scope resource`. The parser previously only looked for `:` as the prefix delimiter, but per `@atproto/oauth-scopes` syntax a scope can use `prefix:positional`, `prefix?query`, or both. This affected permission sets whose `repo` permission listed multiple collections, since those expand to a single query-form token. + +- [#186](https://github.com/ascorbic/cirrus/pull/186) [`22f09de`](https://github.com/ascorbic/cirrus/commit/22f09def6b2ed644fb88b3f707fde4da35a6f04a) Thanks [@ascorbic](https://github.com/ascorbic)! - PAR (`/oauth/par`) now resolves every `include:` permission-set scope eagerly and rejects with `invalid_scope` when an include points at a nonexistent or non-permission-set lexicon. Previously the resolver only ran at the authorize step, so clients with a typo in an include scope got a fresh `request_uri` from PAR and only learned about the bad scope at consent time. Matches reference oauth-provider behaviour (`request-manager.ts:297-313`). + +- [#184](https://github.com/ascorbic/cirrus/pull/184) [`47c8c1e`](https://github.com/ascorbic/cirrus/commit/47c8c1e401dff16c3983a2151cffd20bc83551d6) Thanks [@ascorbic](https://github.com/ascorbic)! - PAR (`/oauth/par`) now validates `redirect_uri` against the client's registered redirect_uris at push time. Previously the check only ran at the authorize step, which let a malicious caller obtain a `request_uri` for an unregistered redirect even though the subsequent authorize would have rejected it. Reject early per RFC 6749 §3.1.2.4. + +- [#185](https://github.com/ascorbic/cirrus/pull/185) [`aed8e1b`](https://github.com/ascorbic/cirrus/commit/aed8e1b629afe9d2eae6d2d5b9c5265d769b057b) Thanks [@ascorbic](https://github.com/ascorbic)! - `scopes_supported` in the authorization-server metadata now lists only the values the spec calls out: `atproto`, `transition:generic`, `transition:email`, `transition:chat.bsky`. Granular resource scopes (`repo:`, `rpc:`, `blob:`, `account:<…>`, `identity:<…>`) and permission-set scopes (`include:`) are parameterised and aren't enumerable, so bare prefixes like `repo` or `include` are no longer advertised — clients discover support by attempting the scope and falling back on `invalid_scope`, matching the reference PDS. + ## 0.4.0 ### Minor Changes diff --git a/packages/oauth-provider/package.json b/packages/oauth-provider/package.json index 58489b62..c11223cb 100644 --- a/packages/oauth-provider/package.json +++ b/packages/oauth-provider/package.json @@ -1,6 +1,6 @@ { "name": "@getcirrus/oauth-provider", - "version": "0.4.0", + "version": "0.5.0", "description": "OAuth 2.1 Provider with AT Protocol extensions for Cloudflare Workers", "type": "module", "main": "dist/index.js", diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index 087f6c5d..34a65098 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,5 +1,52 @@ # @getcirrus/pds +## 0.16.0 + +### Minor Changes + +- [#179](https://github.com/ascorbic/cirrus/pull/179) [`9f8adee`](https://github.com/ascorbic/cirrus/commit/9f8adeef6ebd009dcdbc76f88da81bfaa7142037) Thanks [@ascorbic](https://github.com/ascorbic)! - Implement three PDS-side identity endpoints that previously fell through to the AppView proxy and returned 501: + - `com.atproto.identity.resolveDid` returns the DID document for the local account. + - `com.atproto.identity.resolveIdentity` returns `{did, handle, didDoc}` for the local handle or DID. + - `com.atproto.identity.getRecommendedDidCredentials` (authenticated) returns the rotation keys, `alsoKnownAs`, verification methods, and PDS service entry that a migrating account should advertise. + + Requests for foreign DIDs or handles continue to fall through to the AppView proxy unchanged. + +- [#171](https://github.com/ascorbic/cirrus/pull/171) [`bf2f857`](https://github.com/ascorbic/cirrus/commit/bf2f857b0df9cc9e8eaeb7e336114107596473af) Thanks [@ascorbic](https://github.com/ascorbic)! - The firehose now emits the sync 1.1 message shape, matching what the bsky.network relay and other AT Protocol consumers expect. Existing subscribers will start seeing new fields and new event types; nothing has to change on the consumer side, but the warnings some relays were logging against Cirrus hosts (notably `missing prevData field`) will stop. + + What changed on the wire: + - `#commit` messages now include `prevData` (the prior commit's MST root CID), so relays can verify each commit inductively without re-fetching the repo. The CAR slice now also carries the MST covering-proof blocks needed for that verification. + - Each `ops[]` entry on update and delete now includes `prev`, the previous CID of the touched record. Creates omit it as before. + - `tooBig` is always `false`. It was previously set based on payload size, which never matched the field's meaning under sync 1.1. + - New `#account` events are emitted on activation and deactivation, so relays learn about account status changes without polling. Deactivation reports `status: "deactivated"`; activation reports `active: true` with no status. + - New `#sync` events are emitted on activation (after migration or initial setup), giving relays the current commit block without a diff. + - `#identity` events now allow the `handle` field to be omitted, per spec. + - A `#info` frame with `name: "OutdatedCursor"` is sent when a client connects with a cursor older than the retained event window. The stream continues from the oldest available event instead of disconnecting. + - `applyWrites` rejects calls with more than 200 operations, matching the spec cap. + +- [#168](https://github.com/ascorbic/cirrus/pull/168) [`71b988e`](https://github.com/ascorbic/cirrus/commit/71b988e485d5128b185fb4970aaa35011bf67e04) Thanks [@simnaut](https://github.com/simnaut)! - Implement `com.atproto.sync.getLatestCommit`. + + This sync XRPC endpoint was previously unimplemented, so requests fell through to the XRPC proxy and returned `501 MethodNotImplemented`. Relays call `getLatestCommit` during their crawl bootstrap, so a freshly created repo could never be indexed by a fresh `requestCrawl`. The endpoint now returns the repo's head commit as `{ cid, rev }` (sourced from the same `rpcGetRepoStatus` data used by `getRepoStatus`/`listRepos`). + +- [#178](https://github.com/ascorbic/cirrus/pull/178) [`aceda62`](https://github.com/ascorbic/cirrus/commit/aceda62d0e653c139302dc45b01091886b3234ae) Thanks [@ascorbic](https://github.com/ascorbic)! - Implement `com.atproto.sync.listReposByCollection`. + + Relays and crawlers use this endpoint to discover which PDSes host repos that contain a given record collection. The PDS now answers with `{ repos: [{ did }] }` when its account has at least one record in the requested collection, or an empty list otherwise. Invalid or missing `collection` parameters return `InvalidRequest`. + +### Patch Changes + +- [#181](https://github.com/ascorbic/cirrus/pull/181) [`6589e1d`](https://github.com/ascorbic/cirrus/commit/6589e1dc854be11164b2ad80d77840ed7820bd4d) Thanks [@ascorbic](https://github.com/ascorbic)! - `applyWrites` now returns the record CID on `createResult` and `updateResult` even when the record is removed later in the same batch. The lexicon marks `cid` as required, but the previous code looked it up in the post-commit MST — for a record that was created then deleted within one batch, the MST has no entry and the field was missing. The CID is now computed from the record bytes up front, matching reference PDS behaviour. + +- [#176](https://github.com/ascorbic/cirrus/pull/176) [`36b79fd`](https://github.com/ascorbic/cirrus/commit/36b79fdcabb6d0620b7dc3b184549f9813f6ebcb) Thanks [@ascorbic](https://github.com/ascorbic)! - `com.atproto.repo.applyWrites` now accepts batches that touch the same rkey more than once, matching the reference PDS. The common case is a create followed by a delete on the same rkey within one batch (an atomic no-op pattern several clients rely on); previously Cirrus rejected this with `400 InvalidRequest: duplicate rkey in batch`. Two creates on the same rkey still fail, but now as `409 RecordAlreadyExists` from the repo layer rather than a pre-flight 400. + +- [#177](https://github.com/ascorbic/cirrus/pull/177) [`ec284fd`](https://github.com/ascorbic/cirrus/commit/ec284fde3544e9ea4e2b6b085f718de449e4862e) Thanks [@ascorbic](https://github.com/ascorbic)! - Advertise a `jwks_uri` in OAuth authorization-server metadata and serve an empty JWKS at `/oauth/jwks`. OAuth clients that run JWKS discovery against the metadata endpoint no longer fail when talking to Cirrus. The key set is empty because Cirrus signs access tokens with HS256 (symmetric `JWT_SECRET`) — there are no public keys to publish. + +- [#180](https://github.com/ascorbic/cirrus/pull/180) [`d107c59`](https://github.com/ascorbic/cirrus/commit/d107c596406b5b7c414f35010ea76d9d5a348b72) Thanks [@ascorbic](https://github.com/ascorbic)! - Fix three conformance issues found by pdscheck: + - `com.atproto.server.getSession` now accepts OAuth access tokens presented with the `DPoP` scheme (RFC 9449), not just `Bearer`. OAuth clients can now read session info without first being rejected with 401. + - `com.atproto.server.listAppPasswords` returns `createdAt` as an RFC 3339 datetime (e.g. `2026-03-29T15:30:17.000Z`) instead of the SQLite `"YYYY-MM-DD HH:MM:SS"` form that violated the lexicon. + - `com.atproto.server.getAccountInviteCodes` is now implemented and returns `{ codes: [] }` for authenticated callers (Cirrus has `inviteCodeRequired: false`, so there are no invite codes to list). Previously it fell through to the AppView proxy and returned 501. + +- Updated dependencies [[`ec284fd`](https://github.com/ascorbic/cirrus/commit/ec284fde3544e9ea4e2b6b085f718de449e4862e), [`54ab459`](https://github.com/ascorbic/cirrus/commit/54ab459588393a58ea906977c1ffc8996d8d0700), [`22f09de`](https://github.com/ascorbic/cirrus/commit/22f09def6b2ed644fb88b3f707fde4da35a6f04a), [`47c8c1e`](https://github.com/ascorbic/cirrus/commit/47c8c1e401dff16c3983a2151cffd20bc83551d6), [`aed8e1b`](https://github.com/ascorbic/cirrus/commit/aed8e1b629afe9d2eae6d2d5b9c5265d769b057b)]: + - @getcirrus/oauth-provider@0.5.0 + ## 0.15.2 ### Patch Changes diff --git a/packages/pds/package.json b/packages/pds/package.json index 06aacab5..685f93c7 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@getcirrus/pds", - "version": "0.15.2", + "version": "0.16.0", "description": "Cirrus – A single-user AT Protocol PDS on Cloudflare Workers", "type": "module", "main": "dist/index.js",