atproto (Atmosphere) login: fix Workers handle resolution, hardened error page, themed UI, ATPROTO_ENABLED#106
Merged
Conversation
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>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
startup-api | 55f5e74 | Jun 28 2026, 11:45 PM |
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>
There was a problem hiding this comment.
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_ENABLEDenv 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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.sociallogins failed withCould not resolve handle "…" to a DID. Root cause: handle→DID resolution's HTTPS well-known step usedfetch(…, { redirect: 'error' }), but the Workers runtime (workerd) doesn't implement the'error'redirect mode — it throws aTypeError, which the surroundingcatchswallowed. With the DNS_atprotofallback 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 underwrangler 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):hepackage (audited, dependency-free, Workers-compatible) — no hand-rolled escaping. DOMPurify/sanitize-html were rejected (wrong tool for text, need a DOM).default-src 'none', noscript-src) plusnosniff/no-referrer, so no script runs even if encoding were bypassed.4. Recover into the form + "Atmosphere" rebrand
5. Atmosphere logo, theming, no-wrap
src/auth/atmosphereMark.ts); brand color#0085FF→#4a8ad4./users/style.cssand use its CSS variables, so they follow the user's OS/chosen theme like the rest of the app. Error-page CSP gainsstyle-src 'self'; scripts stay blocked.white-space: nowrapso the longer Atmosphere label never wraps.6.
ATPROTO_ENABLEDenv flagatproto needs no secret, but enabling it required editing the factory call in code. Add an
ATPROTO_ENABLEDenv var (truthy =true/1/yes/on) so a deployment can toggle it without a code change.isAtprotoEnabledis true when the factory config opts in or the env flag is set; a factoryatproto: { enabled: false }overrides the env flag. Not on by default (published library; still needs HTTPS + stable public origin since the client-metadata URL is theclient_id).Testing
tsc+eslintclean; full suite 128/128.redirect:'manual'resolution, error-form re-render + pre-fill, untrusted-handle escaping, and the env toggle (truthy/falsy values + factory opt-out wins).wrangler dev: the original throw, the fix, the themed pages, client-metadata 200, andhebundling viawrangler deploy --dry-run.0.4.1.🤖 Generated with Claude Code