feat: pluggable auth providers — add AT Protocol, refactor GitHub/Google#398
feat: pluggable auth providers — add AT Protocol, refactor GitHub/Google#398simnaut wants to merge 24 commits intoemdash-cms:mainfrom
Conversation
🦋 Changeset detectedLatest commit: 995bd46 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
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 |
|
All contributors have signed the CLA ✍️ ✅ |
Scope checkThis PR changes 2,616 lines across 55 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new pluggable auth provider system in emdash, adds AT Protocol authentication as the first plugin-based provider, and refactors GitHub/Google OAuth into the same provider interface so all login methods are rendered uniformly in the login page and setup wizard.
Changes:
- Add
AuthProviderDescriptor+ route injection +virtual:emdash/auth-providersregistry generation. - Add
@emdash-cms/plugin-atprotoauth provider (routes, OAuth client, handle/DID verification, admin UI components). - Refactor admin login/setup UI to render configured providers dynamically and add a public
/_emdash/api/auth/modeendpoint.
Reviewed changes
Copilot reviewed 52 out of 55 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds workspace links and new dependencies for the AT Protocol plugin and demo site updates. |
| packages/plugins/atproto/tests/resolve-handle.test.ts | Adds tests for independent handle→DID verification error paths. |
| packages/plugins/atproto/tests/plugin.test.ts | Updates plugin descriptor tests to new expected entrypoint/format/storage. |
| packages/plugins/atproto/tests/oauth-client.test.ts | Adds tests for AT Protocol OAuth client metadata and singleton behavior. |
| packages/plugins/atproto/tests/auth.test.ts | Adds tests for atproto() auth provider descriptor contract/config passthrough. |
| packages/plugins/atproto/src/routes/setup-admin.ts | Implements setup-step route to initiate AT Protocol OAuth and persist setup state. |
| packages/plugins/atproto/src/routes/login.ts | Implements login initiation route for AT Protocol OAuth. |
| packages/plugins/atproto/src/routes/client-metadata.ts | Serves /.well-known/atproto-client-metadata.json for AT Protocol OAuth. |
| packages/plugins/atproto/src/routes/callback.ts | Implements AT Protocol OAuth callback, user provisioning, allowlist enforcement, setup finalization, session creation. |
| packages/plugins/atproto/src/resolve-handle.ts | Adds independent handle resolution via DNS-over-HTTPS + well-known HTTP. |
| packages/plugins/atproto/src/oauth-client.ts | Adds OAuth client singleton + DB-backed state/session storage support for Workers. |
| packages/plugins/atproto/src/env.d.ts | References core locals types for plugin route context typing. |
| packages/plugins/atproto/src/db-store.ts | Adds DB-backed store implementation for atcute OAuth state/sessions. |
| packages/plugins/atproto/src/auth.ts | Exposes atproto() returning an AuthProviderDescriptor (adminEntry, routes, public routes). |
| packages/plugins/atproto/src/admin.tsx | Adds provider UI components (LoginButton/LoginForm/SetupStep) for admin app. |
| packages/plugins/atproto/package.json | Exports new auth/admin/routes modules and declares deps/peers. |
| packages/core/tsconfig.json | Includes new *-admin.tsx auth provider admin components in TS compilation. |
| packages/core/src/virtual-modules.d.ts | Adds typing for virtual:emdash/auth-providers and config authProviders. |
| packages/core/src/index.ts | Re-exports new auth provider types. |
| packages/core/src/auth/types.ts | Defines AuthProviderDescriptor, AuthRouteDescriptor, and admin export contract types. |
| packages/core/src/auth/providers/google.ts | Adds google() provider descriptor returning adminEntry for UI integration. |
| packages/core/src/auth/providers/google-admin.tsx | Adds Google provider LoginButton component. |
| packages/core/src/auth/providers/github.ts | Adds github() provider descriptor returning adminEntry for UI integration. |
| packages/core/src/auth/providers/github-admin.tsx | Adds GitHub provider LoginButton component. |
| packages/core/src/auth/mode.ts | Re-exports new auth provider types from core auth mode module. |
| packages/core/src/astro/routes/PluginRegistry.tsx | Passes authProviders registry into the admin app. |
| packages/core/src/astro/routes/api/setup/status.ts | Adjusts setup status commentary for external auth mode. |
| packages/core/src/astro/routes/api/setup/index.ts | Adjusts setup state persistence comments for provider-based flows. |
| packages/core/src/astro/routes/api/setup/admin.ts | Merges admin info into existing setup state (preserve step 1 values). |
| packages/core/src/astro/routes/api/auth/oauth/[provider]/callback.ts | Makes OAuth callback setup-aware (first user becomes admin; finalize setup). |
| packages/core/src/astro/routes/api/auth/oauth/[provider].ts | Improves OAuth initiation error redirect target and missing-env error messaging. |
| packages/core/src/astro/routes/api/auth/mode.ts | Adds public endpoint exposing active auth mode + provider list + signupEnabled. |
| packages/core/src/astro/middleware/auth.ts | Merges provider-supplied public route bypass prefixes into middleware checks; whitelists /_emdash/api/auth/mode. |
| packages/core/src/astro/middleware.ts | Adds fast-path bypass for non-/_emdash injected provider routes like /.well-known/*. |
| packages/core/src/astro/integration/vite-config.ts | Adds virtual module IDs and generator for auth providers registry. |
| packages/core/src/astro/integration/virtual-modules.ts | Implements generateAuthProvidersModule() to bundle provider adminEntry exports. |
| packages/core/src/astro/integration/runtime.ts | Adds authProviders to EmDashConfig. |
| packages/core/src/astro/integration/routes.ts | Injects core auth mode route + provider-declared routes. |
| packages/core/src/astro/integration/index.ts | Wires authProviders through resolved config and injects provider routes. |
| packages/core/src/api/setup-complete.ts | Introduces shared idempotent finalizeSetup() helper. |
| packages/core/src/api/schemas/setup.ts | Adds schemas for AT Protocol login/setup request bodies. |
| packages/core/src/api/route-utils.ts | Public re-export bundle for plugin route handlers (parse/api helpers + finalizeSetup). |
| packages/core/package.json | Exposes new public exports for provider descriptors/admin components + route utils/schemas; adds optional peer for atproto plugin. |
| packages/auth/src/oauth/consumer.ts | Extracts shared findOrCreateOAuthUser() and updates callback path to use it. |
| packages/auth/src/index.ts | Re-exports findOrCreateOAuthUser + CanSelfSignup type. |
| packages/admin/src/lib/auth-provider-context.tsx | Adds React context for provider UI modules and helper hooks (list ordering). |
| packages/admin/src/lib/api/index.ts | Re-exports fetchAuthMode(). |
| packages/admin/src/lib/api/client.ts | Adds fetchAuthMode() client for public auth mode endpoint. |
| packages/admin/src/index.ts | Re-exports auth provider context utilities from admin package. |
| packages/admin/src/components/SetupWizard.tsx | Refactors setup wizard to offer passkey + provider methods and show URL error banners. |
| packages/admin/src/components/LoginPage.tsx | Refactors login page to render providers dynamically and fetch auth mode from public endpoint. |
| packages/admin/src/App.tsx | Wires auth provider registry into admin app via context provider. |
| demos/simple/package.json | Adds atproto plugin to demo dependencies. |
| demos/simple/astro.config.mjs | Configures authProviders: [github(), google(), atproto()] in the demo. |
| .changeset/stale-knives-fix.md | Changeset documenting the new pluggable auth provider system and AT Protocol support. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
08c0312 to
173d8ed
Compare
173d8ed to
d64888e
Compare
|
I have read the CLA Document and I hereby sign the CLA |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
|
Great! The test failures are just an outdated lockfile. I have a few things I'd like addressing, but I need to dig into it a bit deeper. I'll leave another review in a few hours. |
…rage, update terminology Addresses all 10 review comments on PR emdash-cms#398: - Split auth provider into @emdash-cms/auth-atproto (npm-installable), keep syndication plugin in @emdash-cms/plugin-atproto (marketplace) - Add `storage` field to AuthProviderDescriptor, reuse plugin storage infrastructure instead of manual SQL table creation - Rename "AT Protocol" → "Atmosphere", remove "PDS" from user-facing strings - Forbid self-signup when no allowlists configured (except first admin) - Fix core callback.ts import to use #db alias - Fix env.d.ts to reference emdash/locals package Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove module-scope singleton from OAuth client (unsafe on Workers where module vars persist between requests). Construct per-request. Replace url.origin with getPublicOrigin() in all atproto routes so the correct origin is used behind reverse proxies. Export getPublicOrigin from route-utils. Narrow the EmDashConfig import in public-url.ts to an inline interface to avoid pulling the full core type tree into auth-atproto's typecheck.
…st test The test was missing a mock for the virtual:emdash/config module imported by auth middleware, causing a module resolution error.
|
@jimray it would be great to get your eyes on the atproto part of this |
|
@simnaut the conflicts here are mostly related to the i18n. Once you've reolved the conflicts in the JSX you should be able to run |
cfc58e7 to
517d916
Compare
Add eslint-disable comments for type assertions required by the @ATCUTE SDK's type signatures and Astro's opaque locals types.
|
@ascorbic Conflicts resolved, also suppressed lint warnings at some library boundaries. |
| // Normalize localhost ↔ 127.0.0.1 so the authorize and callback URLs match | ||
| // (authorize uses localhost, callback may arrive on 127.0.0.1). | ||
| if (isLoopback(baseUrl)) { | ||
| baseUrl = baseUrl.replace("://localhost", "://127.0.0.1"); | ||
| } | ||
|
|
There was a problem hiding this comment.
This doesn't work in local dev for me, because by default Astro expects to be loaded on localhost not 127.0.0.1
There was a problem hiding this comment.
Hmm. Interesting. 127.0.0.1 is required by the OAuth spec (RFC 8252), and the atcute library enforces it: loopbackRedirectUriSchema explicitly rejects localhost in redirect URIs. The server-side normalization is working on my end but I want it to work universally. What kind of error did you run in to?
There was a problem hiding this comment.
The callback tries to load 127.0.0.1:4321 and just gets no server response
I can take a look in a bit |
ascorbic
left a comment
There was a problem hiding this comment.
This is generally great, and aside from the loclahost thing it worked great for me. My main other comment is that we should standardise on the Kumo components that are used elsewhere in the admin to keep things consistent. I'd make it a peer dependency of the auth packages.
- Replace raw HTML form elements with Kumo Input, Button, and LinkButton in atproto admin components - Update GitHub and Google LoginButtons to use Kumo LinkButton - Rename auth provider label from "AT Protocol" to "Atmosphere" - Improve oauth-client.ts loopback comment to cite RFC 8252 §8.3 - Remove broken client-side localhost→127.0.0.1 redirect that reset the setup wizard; server-side normalization handles this correctly
| type="button" | ||
| className="w-full inline-flex items-center justify-center gap-2 rounded-md border border-kumo-tint bg-kumo-base px-4 py-2 text-sm font-medium text-kumo-default hover:bg-kumo-tint" | ||
| > | ||
| <LinkButton href="#" variant="outline" className="w-full justify-center"> |
There was a problem hiding this comment.
Yeah, sorry I just meant LinkButton for the ones that were actually links. Others should use Button.
What does this PR do?
Introduces a pluggable auth provider system and uses it to add AT Protocol authentication as the first plugin-based provider. GitHub and Google OAuth are refactored from hardcoded buttons into the same provider interface. All auth methods — passkey, AT Protocol, GitHub, Google — are equal options on the login page and setup wizard.
Auth provider descriptor
Every provider implements
AuthProviderDescriptor:Each descriptor declares an
adminEntry(React components for login/setup UI), optionalroutes(Astro route handlers), andpublicRoutes(middleware bypass prefixes). Components are distributed to the admin app via avirtual:emdash/auth-providersVite virtual module.What changed
New pluggable infrastructure (core):
AuthProviderDescriptor/AuthRouteDescriptortypes inauth/types.tsauthProvidersconfig field onEmDashConfigvirtual:emdash/auth-providersvirtual module withgenerateAuthProvidersModule()injectAuthProviderRoutes()for generic route injection from descriptorsAuthProviderContextin admin for distributing provider UI components/_emdash/api/auth/modeendpoint returning available providersemdash/api/route-utilspublic export for API utilities used by plugin routesfinalizeSetup()shared helper for setup completion (idempotent, used by all auth callbacks)Shared OAuth user creation (
@emdash-cms/auth):findOrCreateOAuthUser()extracted as a shared export — handles OAuth account lookup, email auto-linking, and user creation withcanSelfSignuppolicyLogin page:
OAUTH_PROVIDERSarrayuseAuthProviderList()LoginButtonshow in a button grid; those withLoginFormexpand inline on clickSetup wizard:
AT Protocol (new —
@emdash-cms/plugin-atproto):atproto()returnsAuthProviderDescriptorwith routes for login, callback, setup-admin, and client metadata@emdash-cms/plugin-atproto/routes/*), not in core — removing the plugin removes the routesLoginButton(compact butterfly icon),LoginForm(handle input),SetupStepexported from@emdash-cms/plugin-atproto/admin@atcute/oauth-node-clientwith PKCE (public client, no key management needed)allowedDIDsallowlist for access control by permanent DID identifiersallowedHandlesallowlist with wildcard support for org-level domain gating (e.g.,*.mycompany.com). Handle ownership is independently verified via DNS-over-HTTPS and HTTP well-known resolution using@atcute/identity-resolver— the PDS's handle claim is never trusted directlyfindOrCreateOAuthUser()withcanSelfSignuppolicy for consistent signup gatingGitHub / Google (refactored to descriptors):
github()andgoogle()returnAuthProviderDescriptorwithadminEntrypointing to-admin.tsxcomponentsLoginButtoncomponents with provider icons, reusing core's existing[provider]OAuth routesOAuth error handling:
Refererto redirect errors back to the originating page (setup wizard vs login)Configuration
AT Protocol options:
New routes (AT Protocol)
POST /_emdash/api/auth/atproto/login— initiate OAuth flow (handle → PDS authorization URL)GET /_emdash/api/auth/atproto/callback— handle PDS redirect, create/find user, establish sessionPOST /_emdash/api/setup/atproto-admin— setup wizard flowGET /.well-known/atproto-client-metadata.json— client metadata (PDS fetches this)Type of change
Checklist
pnpm typecheckpassespnpm --silent lint:json | jq '.diagnostics | length'returns 0pnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
Test results (2026-04-12)
Full CI suite run locally after addressing review comments. All 3,470 tests pass across 10 packages (204 test files, 0 failures). Typecheck clean across all 21 packages. Lint returns 18 diagnostics (all pre-existing, none introduced by this PR).
emdash (core): 2,279 tests, 131 files — all passing
@emdash-cms/auth-atproto: 30 tests, 3 files — all passing
@emdash-cms/auth: 45 tests, 3 files — all passing
@emdash-cms/admin: 723 tests, 49 files — all passing
Other packages: all passing