Skip to content

atproto (Atmosphere) login: fix Workers handle resolution, hardened error page, themed UI, ATPROTO_ENABLED#106

Merged
sergeychernyshev merged 7 commits into
mainfrom
fix/atproto-handle-resolution
Jun 28, 2026
Merged

atproto (Atmosphere) login: fix Workers handle resolution, hardened error page, themed UI, ATPROTO_ENABLED#106
sergeychernyshev merged 7 commits into
mainfrom
fix/atproto-handle-resolution

Conversation

@sergeychernyshev

@sergeychernyshev sergeychernyshev commented Jun 28, 2026

Copy link
Copy Markdown
Member

Fixes AT Protocol (Atmosphere) login on Cloudflare Workers and builds it out into a polished, themeable sign-in. Six commits, each self-contained.

1. The bug — handle resolution failed on Workers

*.bsky.social logins failed with Could not resolve handle "…" to a DID. Root cause: handle→DID resolution's HTTPS well-known step used fetch(…, { redirect: 'error' }), but the Workers runtime (workerd) doesn't implement the 'error' redirect mode — it throws a TypeError, which the surrounding catch swallowed. With the DNS _atproto fallback absent for Bluesky-hosted handles, resolution failed. Masked locally because Node/undici support 'error'.

Fix: switch to redirect: 'manual' (supported on Workers, preserves the no-follow intent). Verified resolving the real handle under wrangler dev.

2–3. Styled, hardened error page

Replaced the raw Auth failed: … plain-text responses with a styled error card, then hardened it since auth messages embed untrusted external text (handles, PDS/auth-server bodies):

  • Output-encode with the he package (audited, dependency-free, Workers-compatible) — no hand-rolled escaping. DOMPurify/sanitize-html were rejected (wrong tool for text, need a DOM).
  • Serve under a strict Content-Security-Policy (default-src 'none', no script-src) plus nosniff / no-referrer, so no script runs even if encoding were bypassed.

4. Recover into the form + "Atmosphere" rebrand

  • When a handle fails to authorize, re-render the handle form with the error inline and the handle pre-filled (HTTP 400), so the user can fix a typo and retry in place. Callback-phase failures still use the error page.
  • Drop all "Bluesky" wording → "Atmosphere". Button and form read "Login with your Atmosphere account".

5. Atmosphere logo, theming, no-wrap

  • Replace the butterfly with the official Atmosphere "union" logo across all five render paths (shared via src/auth/atmosphereMark.ts); brand color #0085FF#4a8ad4.
  • The handle form and error page now link /users/style.css and use its CSS variables, so they follow the user's OS/chosen theme like the rest of the app. Error-page CSP gains style-src 'self'; scripts stay blocked.
  • Widen the login overlay and form card + white-space: nowrap so the longer Atmosphere label never wraps.

6. ATPROTO_ENABLED env flag

atproto needs no secret, but enabling it required editing the factory call in code. Add an ATPROTO_ENABLED env var (truthy = true/1/yes/on) so a deployment can toggle it without a code change. isAtprotoEnabled is true when the factory config opts in or the env flag is set; a factory atproto: { enabled: false } overrides the env flag. Not on by default (published library; still needs HTTPS + stable public origin since the client-metadata URL is the client_id).

Testing

  • tsc + eslint clean; full suite 128/128.
  • New tests: redirect:'manual' resolution, error-form re-render + pre-fill, untrusted-handle escaping, and the env toggle (truthy/falsy values + factory opt-out wins).
  • Verified under workerd / wrangler dev: the original throw, the fix, the themed pages, client-metadata 200, and he bundling via wrangler deploy --dry-run.
  • Version bumped to 0.4.1.

🤖 Generated with Claude Code

The HTTPS well-known step in handle->DID resolution used
`fetch(..., { redirect: 'error' })`, but the Workers runtime does not
implement the 'error' redirect mode and throws a TypeError. The throw
was swallowed by the surrounding catch, silently disabling this method.

For *.bsky.social handles there is no `_atproto` DNS TXT record, so the
DNS fallback also returns nothing, leaving every Bluesky-hosted login
failing with `Could not resolve handle "..." to a DID`. The bug was
masked locally because Node/undici support 'error'.

Switch to `redirect: 'manual'`, which is supported on Workers and keeps
the original intent of not following redirects (a 3xx yields res.ok ===
false, falling through to the DNS method).

Also replace the raw plain-text "Auth failed" responses in the auth
router with a styled error page (errorPage.ts), so failed logins across
all providers render a card instead of bare text.

Bump patch version to 0.4.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 28, 2026

Copy link
Copy Markdown

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
startup-api 55f5e74 Jun 28 2026, 11:45 PM

sergeychernyshev and others added 5 commits June 28, 2026 17:31
Auth error messages can embed untrusted text from external servers (the
user-supplied handle, raw PDS/authorization-server response bodies).
HTML-escaping alone left gaps: control characters, Unicode bidi/zero-width
tricks, and unbounded length all survived it.

Layer the protection:
- sanitizeText() reduces the message to a bounded, single-line, printable
  string, stripping C0/C1 controls, format/bidi chars (\p{Cf}) and line
  separators before escaping, and truncating to 300 chars.
- Serve the page under a strict Content-Security-Policy (default-src
  'none'; style-src 'unsafe-inline') plus X-Content-Type-Options: nosniff
  and Referrer-Policy: no-referrer, so no script can execute even if
  escaping were bypassed.
- Drop the javascript: back-link (blocked by the CSP and an anti-pattern).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the custom escape/sanitize functions with `he.escape()` — a
battle-tested, dependency-free HTML output encoder — so untrusted text in
auth error messages (user handles, external PDS/auth-server response
bodies) is encoded by an audited implementation rather than our own.

DOMPurify/sanitize-html were considered but are the wrong fit: they
sanitize rich HTML you want to keep (we render untrusted text AS text),
and DOMPurify needs a DOM, which is unavailable on Cloudflare Workers.
`he` is pure JS with zero dependencies and bundles cleanly for Workers
(verified via `wrangler deploy --dry-run`).

The restrictive Content-Security-Policy and nosniff/Referrer-Policy
headers remain as defense in depth (standard headers, not custom
sanitization), so no script can execute even if encoding were bypassed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UX: when a supplied handle fails to authorize, re-render the handle-entry
form with the error shown inline and the handle pre-filled (HTTP 400)
instead of a dead-end error page, so the user can correct and retry in
place. Callback-phase failures still fall back to renderAuthError. Also
switch this provider's own HTML escaping to the `he` package.

Rebrand: drop all "Bluesky" wording in favor of "Atmosphere". The login
button and handle form now read "Login with your Atmosphere account";
comments/README updated to "AT Protocol (Atmosphere)". The atproto icon
(butterfly mark) is unchanged.

Tests: cover the error-form re-render (pre-fill + inline error) and that
an untrusted handle is HTML-escaped in the re-rendered form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Icon: replace the old butterfly mark with the official Atmosphere "union"
logo across all five render paths (provider getIcon, SSR getProviderIcon,
the power-strip badge + login button, and the provider-badge color). The
mark is shared via a new src/auth/atmosphereMark.ts so the server-side
copies never drift; power-strip.js keeps its own copy since it cannot
import from src/. Brand color moves from Bluesky blue (#0085FF) to the
Atmosphere blue (#4a8ad4).

Theming: the handle-entry form and the error page now link
/users/style.css and use its CSS variables, so they follow the user's
OS/chosen theme exactly like profile.html. The error page's CSP gains
style-src 'self' for that same-origin stylesheet; scripts stay fully
blocked. renderAuthError now takes usersPath to build the stylesheet URL.

Layout: widen the login overlay (max-width 25rem) and the handle form
card (23rem), and apply white-space: nowrap so "Login with your
Atmosphere account" never wraps in either the button or the form title.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
atproto is a public OAuth client with no secret, but enabling it
previously required editing the createStartupAPI factory call in code.
Add an ATPROTO_ENABLED env var (truthy = true/1/yes/on) so a deployment
can switch it on without a code change — handy for on-in-prod /
off-in-preview.

isAtprotoEnabled now returns true when the factory config opts in OR the
env flag is truthy; a factory `atproto: { enabled: false }` remains an
explicit opt-out that overrides the env flag, so a deployment can force
it off. env is threaded through getActiveProviders and
AtprotoProvider.create (both already have it at request time).

Not on by default: it is a published library (opt-in is predictable) and
still needs HTTPS + a stable public origin, since the served
client-metadata URL is the OAuth client_id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sergeychernyshev sergeychernyshev changed the title Fix atproto (Bluesky) handle resolution failing on Cloudflare Workers atproto (Atmosphere) login: fix Workers handle resolution, hardened error page, themed UI, ATPROTO_ENABLED Jun 28, 2026
@sergeychernyshev sergeychernyshev requested a review from Copilot June 28, 2026 23:38

Copilot AI left a comment

Copy link
Copy Markdown

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 fixes AT Protocol (Atmosphere) login behavior on Cloudflare Workers and upgrades the atproto sign-in experience with a themeable UI, safer error rendering, and an ATPROTO_ENABLED deployment toggle.

Changes:

  • Fix handle→DID resolution on Workers by switching the well-known fetch redirect mode to manual.
  • Replace plain-text auth failures with a styled HTML error page that output-encodes untrusted details (via he) and applies a restrictive CSP.
  • Rebrand atproto UI to “Atmosphere”, share a single logo path across server/client rendering, and add ATPROTO_ENABLED env gating with tests/docs.

Reviewed changes

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

Show a summary per file
File Description
test/atproto.spec.ts Adds tests for form re-render on resolution failure, HTML escaping, and ATPROTO_ENABLED behavior.
src/StartupAPIEnv.ts Introduces ATPROTO_ENABLED?: string env var type.
src/handlers/utils.ts Enables atproto provider via factory config or env flag (with factory opt-out winning).
src/handlers/ssr.ts Updates the atproto provider icon to the shared Atmosphere mark.
src/auth/index.ts Routes auth failures through the new styled error page renderer.
src/auth/errorPage.ts New hardened, theme-aware HTML error page using he and a CSP.
src/auth/AtprotoProvider.ts Adds env-flag enablement logic, rebrands UI, uses shared logo, and re-renders handle form with inline errors.
src/auth/atproto/identity.ts Switches redirect handling to redirect: 'manual' for Workers compatibility.
src/auth/atmosphereMark.ts New shared SVG path constant for the Atmosphere mark.
README.md Documents ATPROTO_ENABLED and updates atproto section wording/rebrand.
public/users/power-strip.js Updates atproto button/badge branding, icon, colors, and layout tweaks.
package.json Bumps version to 0.4.1 and adds he + @types/he.
package-lock.json Updates lockfile for version/name alignment and new deps.

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

Comment thread src/auth/errorPage.ts Outdated
Comment thread src/auth/errorPage.ts
Address Copilot review on the error page, and apply the same protections
to the handle-entry form since both reflect the user-supplied handle:

- Add `frame-ancestors 'none'` to the CSP. It does not fall back to
  `default-src`, so without it these auth pages could be framed by
  another origin (clickjacking).
- Add `Cache-Control: no-store` so a browser or intermediary never
  caches a response that can embed a handle or a remote server message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sergeychernyshev sergeychernyshev merged commit 41fcfb8 into main Jun 28, 2026
2 checks passed
@sergeychernyshev sergeychernyshev deleted the fix/atproto-handle-resolution branch June 28, 2026 23:52
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.

2 participants