Skip to content

fix(registry-cli): granular OAuth scopes, error reporting, exit hang#929

Merged
ascorbic merged 2 commits intomainfrom
registry-cli-oauth-improvements
May 7, 2026
Merged

fix(registry-cli): granular OAuth scopes, error reporting, exit hang#929
ascorbic merged 2 commits intomainfrom
registry-cli-oauth-improvements

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic commented May 7, 2026

What does this PR do?

Four related improvements to the @emdash-cms/registry-cli login flow, surfaced while logging in to mk.gg:

  1. Granular OAuth scopes. transition:generic is replaced with the narrowest scope set derived from @emdash-cms/registry-lexicons:

    • atproto
    • repo:<nsid> for every record-shaped lexicon (package profile, package release, publisher profile, publisher verification)
    • rpc:<nsid>?aud=* for every aggregator query (the publish CLI doesn't call them yet, but granting them at login means future tooling that resumes the stored session can call the aggregator without forcing a re-login).

    If the AS rejects the granular request with invalid_scope, login automatically retries once with transition:generic and prints a notice — keeps publishers on un-upgraded PDSes unblocked.

  2. OAuth error reporting. OAuthResponseError is caught in the login command and rendered with HTTP status, endpoint, OAuth error code/description, and a body snippet when the AS response wasn't OAuth-shaped JSON. The original symptom was a bare unknown_error plus a stack trace whenever the PDS hiccuped at PAR — users had no way to tell "transient PDS issue" from "config problem."

  3. Display name / handle resolution. Post-login handle resolution moved off com.atproto.server.getSession (which 401s on cirrus PDSes that only accept Authorization: Bearer ..., since atcute sends Authorization: DPoP ... for OAuth sessions) onto LocalActorResolver.resolve(did). The handle is read from the DID document's alsoKnownAs and round-tripped through DNS/well-known to verify it points back. No PDS XRPC, no DPoP/Bearer compatibility, no rpc:com.atproto.* scope needed.

  4. Login/logout exit hang. After a successful login or logout, run() was returning correctly but the CLI hung indefinitely. Root cause is a ref'd handle somewhere in the OAuth path (atcute, undici, or the loopback server) that prevents Node's event loop from draining. Workaround: force-exit at the top level once runMain resolves. The underlying handle leak is unidentified — flagged in the changeset and the source comment.

Bonus: @emdash-cms/registry-lexicons now exports RECORD_NSIDS and QUERY_NSIDS so other consumers can derive permission/scope lists from the lexicon set instead of hand-rolling one that drifts.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable) — no tests added; the changes are presentation, scope strings, and a top-level workaround. The existing 83 tests in the package still pass.
  • User-visible strings in the admin UI are wrapped for translation — N/A; this is the CLI.
  • I have added a changeset (four changesets — three patches on @emdash-cms/registry-cli, one minor on @emdash-cms/registry-lexicons for the new exports)
  • New features link to an approved Discussion — N/A; this is a bug fix bundle.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.7

Screenshots / test output

node packages/registry-cli/dist/index.mjs login mk.gg before this PR (the actual symptom that prompted the work):

ℹ Waiting for authorization...
✔ Logged in as did:plc:uwbl4k3tza7eyjv3morkrld2 (Matt Kane)
ℹ PDS: https://mk.pds.mk.gg/
^C   <-- CLI never returned to the shell prompt

After this PR: handle resolves to mk.gg (via DID doc, no PDS XRPC), and the prompt returns immediately after PDS: prints.

…n/logout exit hang

- Switch login from `transition:generic` to granular scopes derived from `@emdash-cms/registry-lexicons`: `repo:` for every record-shaped lexicon and `rpc:?aud=*` for every aggregator query. Auto-fall-back to `transition:generic` if the AS returns `invalid_scope`.
- Surface OAuth response failures with HTTP status, endpoint, error code/description, and a body snippet when the response wasn't OAuth-shaped JSON, instead of bubbling up a bare `unknown_error` and stack trace.
- Resolve the post-login handle from the DID document via `LocalActorResolver` instead of `com.atproto.server.getSession`, which avoids PDS-side DPoP/Bearer compatibility quirks and lets us drop the corresponding `rpc:` scope.
- Force-exit on success at the top level. After login/logout, run() returns but a ref'd handle in the OAuth path keeps the event loop alive indefinitely; this is a workaround pending root-cause.
- Expose `RECORD_NSIDS` / `QUERY_NSIDS` from `@emdash-cms/registry-lexicons` so consumers can derive scope/permission lists from the lexicon set instead of hand-rolling them.
Copilot AI review requested due to automatic review settings May 7, 2026 08:40
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 7, 2026

🦋 Changeset detected

Latest commit: e934ee4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@emdash-cms/registry-cli Patch
@emdash-cms/registry-lexicons Minor
@emdash-cms/registry-client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

cloudflare-workers-and-pages Bot commented May 7, 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
emdash-perf-coordinator e934ee4 May 07 2026, 09:55 AM

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

cloudflare-workers-and-pages Bot commented May 7, 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
emdash-i18n e934ee4 May 07 2026, 09:55 AM

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

cloudflare-workers-and-pages Bot commented May 7, 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
docs e934ee4 May 07 2026, 09:56 AM

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

cloudflare-workers-and-pages Bot commented May 7, 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
emdash-playground e934ee4 May 07 2026, 09:56 AM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 7, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@929

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@929

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@929

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@929

emdash

npm i https://pkg.pr.new/emdash@929

create-emdash

npm i https://pkg.pr.new/create-emdash@929

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@929

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@929

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@929

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@929

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@929

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@929

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@929

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@929

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@929

commit: e934ee4

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

cloudflare-workers-and-pages Bot commented May 7, 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
emdash-demo-cache e934ee4 May 07 2026, 09:56 AM

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the @emdash-cms/registry-cli OAuth login/logout flow and scope handling, and adds lexicon-derived NSID lists in @emdash-cms/registry-lexicons so scope/permission lists can be generated from the authoritative lexicon set.

Changes:

  • Add RECORD_NSIDS / QUERY_NSIDS exports to @emdash-cms/registry-lexicons for consumers deriving repo: / rpc: scopes from lexicons.
  • Update registry CLI OAuth to request granular scopes by default, retrying once with transition:generic on invalid_scope, and improve OAuth error reporting in login.
  • Move handle resolution to DID-document-based resolution (via LocalActorResolver) and force-exit the CLI after successful completion to avoid post-login/logout hangs.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/registry-lexicons/src/index.ts Exports RECORD_NSIDS and QUERY_NSIDS lists derived from the lexicon NSIDs.
packages/registry-cli/src/profile.ts Switches handle resolution to DID document resolution via LocalActorResolver.
packages/registry-cli/src/oauth.ts Builds granular default OAuth scopes from lexicon NSID lists; adds invalid_scope fallback flow and factors out an actor resolver factory.
packages/registry-cli/src/index.ts Forces process.exit(0) after runMain resolves to work around an event-loop hang.
packages/registry-cli/src/commands/login.ts Adds structured OAuth response error reporting and messaging for legacy-scope fallback.
.changeset/registry-lexicons-nsid-lists.md Changeset for new lexicon NSID list exports.
.changeset/registry-cli-login-error.md Changeset for improved OAuth error reporting.
.changeset/registry-cli-granular-scopes.md Changeset for granular scopes + handle resolution change.
.changeset/registry-cli-exit-hang.md Changeset for the forced-exit hang workaround.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/registry-cli/src/oauth.ts Outdated
Comment on lines +244 to +246
* the atproto OAuth permission spec, scoped to the two record collections
* the CLI publishes plus an `rpc:com.atproto.server.getSession` for handle
* resolution.
Comment on lines +60 to +66
* NSIDs of record-shaped lexicons in this package (one row per NSID in the
* publisher's repo). Embedded objects (`releaseExtension`) and shared defs
* (`aggregator.defs`) are excluded — they don't address their own collection.
*
* Useful for consumers building OAuth `repo:` scopes or enumerating writable
* collections without hand-rolling a list that drifts from the lexicons.
*/
@ascorbic
Copy link
Copy Markdown
Collaborator Author

ascorbic commented May 7, 2026

/review

* the atproto OAuth permission spec, scoped to the two record collections
* the CLI publishes plus an `rpc:com.atproto.server.getSession` for handle
* resolution.
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: stale JSDoc. This comment now describes neither the actual default scope nor the current handle-resolution path:

  • "the two record collections the CLI publishes" — DEFAULT_CLI_SCOPE derives from RECORD_NSIDS, which has four entries (package profile, package release, publisher profile, publisher verification).
  • "rpc:com.atproto.server.getSession for handle resolution" — getSession was removed in this PR (the whole point of switching to LocalActorResolver). The actual rpc: scopes in the default cover the five aggregator queries from QUERY_NSIDS.

Suggest something like: "Defaults to DEFAULT_CLI_SCOPEatproto plus repo:<nsid> for every record-shaped lexicon in @emdash-cms/registry-lexicons and rpc:<nsid>?aud=* for every aggregator query." Same comment was raised by Copilot.

await reportOAuthFailure(error);
process.exit(1);
}
throw error;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: OAuthCallbackError falls through to a stack trace. The whole motivation for the new error reporting is that users were seeing bare unknown_error + stack traces and couldn't tell a transient PDS issue from a config issue. This catch handles OAuthResponseError (the PAR/token-endpoint path) but @atcute/oauth-node-client throws OAuthCallbackError for the other half of the surface — user denied consent (error=access_denied), state mismatch, expired code, issuer mismatch, etc. Those still surface as a citty stack trace, which is the same UX bug just in a different code path.

Not blocking — the cirrus symptom that prompted this PR was a PAR failure, so this catch fixes the immediate report. Worth a follow-up to extend reportOAuthFailure (or a sibling) to render OAuthCallbackError cleanly too: at minimum render error.error and error.errorDescription without the stack.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 7, 2026

Review posted successfully with two line-anchored comments at #929 (review).

Findings summary:

The PR is well-scoped and well-justified — four cleanly separated fixes with matching changesets and clear forensic notes for the unidentified handle leak. Verified all the substantive claims:

  • DEFAULT_CLI_SCOPE correctly derives from the lexicon NSID lists.
  • The invalid_scope retry path is structurally sound (each createCliOAuthClient shares the on-disk state stores, so the legacy client's callback resolves the legacy state).
  • OAuthResponseError shape (response, error, errorDescription, status) matches what reportOAuthFailure reads.
  • citty's runMain catches all errors and exits non-zero internally, so .then(() => process.exit(0)) is safe and only fires on success.
  • releaseExtension is correctly excluded from RECORD_NSIDS (it's type: "object", not record). The pre-existing inconsistency in RegistryRecords Copilot flagged was not introduced by this PR — left it alone.

Two low-severity comments posted:

  1. oauth.ts:247 — Stale JSDoc on the scope? option. Says "two record collections" (now four) and references rpc:com.atproto.server.getSession (removed in this PR). Same observation Copilot made.
  2. login.ts:126 — The new try/catch only handles OAuthResponseError. OAuthCallbackError (user denied consent, state mismatch, expired code, issuer mismatch) still surfaces as a citty stack trace, which is the same UX bug the PR set out to fix, just on a different axis. Not blocking — the cirrus symptom that prompted the work was a PAR failure — but worth a follow-up.

github run

Fix stale JSDoc on scope option, remove releaseExtension from
RegistryRecords (embedded object, not a standalone collection),
and catch OAuthCallbackError for clean error reporting.
@ascorbic ascorbic merged commit 5464b55 into main May 7, 2026
36 checks passed
@ascorbic ascorbic deleted the registry-cli-oauth-improvements branch May 7, 2026 11:44
@emdashbot emdashbot Bot mentioned this pull request May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants