Skip to content

fix(identity)!: authorise the ATProto binding write and move it to fyi.xnet - #589

Merged
crs48 merged 2 commits into
mainfrom
claude/magical-hypatia-f930ba
Jul 19, 2026
Merged

fix(identity)!: authorise the ATProto binding write and move it to fyi.xnet#589
crs48 merged 2 commits into
mainfrom
claude/magical-hypatia-f930ba

Conversation

@crs48

@crs48 crs48 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

The defect

site/public/oauth/atproto-client.json declared "scope": "atproto"identity only, no data access — while apps/web/src/identity/atproto-ceremony.ts performed com.atproto.repo.putRecord to write the identity binding. The write was never authorised, and nothing failed loudly.

Confirmed against live systems rather than docs alone:

  • production served "scope": "atproto" (curl https://xnet.fyi/oauth/atproto-client.json)
  • com.atproto.sync.listReposByCollection on relay1.us-west.bsky.network returned {"repos":[]}0 DIDs held the binding

Context: exploration 0371, defects D1 and D2.

What changed

Granular scope. Now atproto repo:fyi.xnet.identity.binding?action=create&action=update — narrowed to the two actions putRecord needs, rather than bare repo:<nsid> which would also grant delete.

A second client_id for the fallback. The scope lives in the metadata document, and older self-hosted PDSes reject granular syntax in that document outright (#4118). One document therefore cannot carry both: a PDS that chokes on repo: chokes on it whatever we go on to request. atproto-client-compat.json serves atproto transition:generic, and the ceremony retries against it only on a scope-syntax rejection — never on user cancellation or a transport failure, since reopening a popup the user just dismissed would be worse than failing.

Namespace rename. net.x.identity.bindingfyi.xnet.identity.binding. NSID authority is DNS-rooted, so net.x.* requires control of x.net — IANA's, never ours. Migration cost is nil precisely because the scope defect meant no record was ever written.

Verification

The regression test ties the declared scope to the exported ATPROTO_BINDING_COLLECTION constant, so a rename on either side fails in CI. Mutation-tested rather than trusted green — reverting the scope to bare atproto, using repo:fyi.xnet.*, and renaming the collection without updating the scope each fail it; restored, 10/10 pass.

  • apps/web: 856 tests / 119 files
  • identity: 311 tests / 25 files
  • hub binding + recovery-anchor: 12 tests (verifier follows the rename end-to-end)
  • typecheck 45/45, prettier clean

Not verified: the real putRecord

A live putRecord has not been exercised. The PDS fetches client metadata from the deployed https://xnet.fyi/... URL, so local edits are invisible to it and production still serves the old scope. Verification is only possible after this merges and site/public/oauth/ deploys: sign in with a test handle, then re-query the relay for fyi.xnet.identity.binding. A non-empty result is the proof.

Two caveats worth knowing

  • bsky.social advertises only atproto and transition:* in scopes_supported. Granular scopes are implemented there — parameterized scopes simply aren't enumerable in that list — but discovery cannot confirm support, so the compat path may carry more traffic than "old self-hosted PDSes only" implies.
  • Bluesky's August 2025 guidance was to hold off shipping granular scopes to production pending final docs. Possibly stale by now; the two-client design means a rejection degrades to transition:generic rather than breaking sign-in.

Follow-up (not in scope here)

_lexicon.xnet.fyi has no TXT record, so the new lexicon isn't resolvable yet. Publication is "strongly advised" but not required — natural next step.

Release

Changeset is major: the collection name is a wire contract appearing in the record $type, in every binding's at:// URI, and in the hub's verification path.

🤖 Generated with Claude Code

…i.xnet

The OAuth client declared `"scope": "atproto"`, which is identity-only and
grants no data access, while the ceremony performed `com.atproto.repo.putRecord`
to write the identity binding. Nothing failed loudly — the write was simply
never authorised. Corroborated live: the binding was held by 0 DIDs
network-wide.

Two fixes, taken together because the second makes the first's cost zero:

- Declare the granular `repo:` scope the write actually needs, narrowed to
  `action=create&action=update` (putRecord needs no delete). Older self-hosted
  PDSes reject granular scope syntax in client-metadata.json outright, and the
  scope lives *in* that document, so a single document cannot carry both. A
  second client_id serving `atproto transition:generic` provides the fallback,
  with the ceremony retrying against it only on a scope-syntax rejection —
  never on user cancellation or a transport failure.

- Move the binding collection from `net.x.identity.binding` to
  `fyi.xnet.identity.binding`. NSIDs are DNS-rooted, so authority over
  `net.x.*` requires control of `x.net`, which belongs to IANA and can never be
  ours. Migration cost is nil precisely because the scope defect meant no
  record was ever written.

A regression test ties the declared scope to the exported collection constant,
so a rename on either side fails in CI rather than in production. Verified by
mutation: reverting the scope, using a prefix wildcard, or renaming the
collection alone each fail the suite.

BREAKING CHANGE: `ATPROTO_BINDING_COLLECTION` changes from
`net.x.identity.binding` to `fyi.xnet.identity.binding`. The name appears in
the record `$type`, in every binding's `at://` URI, and in the hub's
verification path. No records existed under the old name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-589 July 19, 2026 18:51 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

No visual differences detected in the changed UI.

CI run

github-actions Bot added a commit that referenced this pull request Jul 19, 2026
…ange

`check:api-report` runs inside the typecheck job and flagged
`packages/react/etc/react.api.md`. The only delta is a bundled chunk's content
hash in a trailing warning comment, shifted by the doc-comment edit in
`onboarding/atproto-ceremony.ts` — no public API surface changed.

Regenerated with the repo's own command (`pnpm --filter @xnetjs/react
api:update`) rather than hand-editing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-589 July 19, 2026 18:59 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 19, 2026
github-actions Bot added a commit that referenced this pull request Jul 19, 2026
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #589.

github-actions Bot added a commit that referenced this pull request Jul 19, 2026
@crs48
crs48 merged commit d170e24 into main Jul 19, 2026
22 checks passed
@crs48
crs48 deleted the claude/magical-hypatia-f930ba branch July 19, 2026 19:08
github-actions Bot added a commit that referenced this pull request Jul 19, 2026
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