Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fe3ec90
feat(auth-service): preview routes for iterating on client branding CSS
aspiers Apr 14, 2026
8298d25
docs(env): add AUTH_PREVIEW_ROUTES to top-level .env.example
aspiers Apr 14, 2026
c93c891
docs: put preview-routes guide in the client tutorial, not operator c…
aspiers Apr 14, 2026
9cf50f7
feat(pds-core): preview route for iterating on consent-page CSS
aspiers Apr 14, 2026
294842d
fix(pds-core): drive preview consent SPA to the consent view
aspiers Apr 16, 2026
516db31
fix(preview): escape hydration <script> against XSS + cache-bypass
aspiers Apr 16, 2026
f1cab6e
feat(preview): persisted client_id input + live cache-status panel
aspiers Apr 17, 2026
f569411
fix(preview): reword /preview copy around the URL input
aspiers Apr 17, 2026
7b68e7b
feat(preview): validate metadata URL + richer cache-entry UI
aspiers Apr 17, 2026
0510e0c
feat(preview): cross-list all routes on both /preview pages
aspiers Apr 17, 2026
ec85696
fix(demo): drop "auth-service" from preview-link copy
aspiers Apr 17, 2026
22adea0
fix(preview): carry client_id across cross-origin preview links
aspiers Apr 17, 2026
2efff88
style(preview): widen /preview index pages 640px → 768px
aspiers Apr 17, 2026
5b7e387
style(preview): move query-string snippet onto its own line
aspiers Apr 17, 2026
87d3400
feat(preview): <code> formatting for field names and URLs in checks
aspiers Apr 17, 2026
5804997
feat(preview): add choose-handle preview for picker-only mode
aspiers Apr 17, 2026
836ab69
feat(preview): sync address bar with client_id input
aspiers Apr 17, 2026
b4d6b80
docs(preview): flag cache-status client_id leak and recommend off in …
aspiers Apr 17, 2026
2e5c538
feat(preview): link sibling-service heading to its /preview index
aspiers Apr 17, 2026
9983f47
feat(preview): validate tos_uri and policy_uri on client metadata
aspiers Apr 17, 2026
0ddbaa0
feat(preview): link 'the JSON body' to the fetched metadata URL
aspiers Apr 17, 2026
b603ed4
refactor(preview): share index-page shell across both services
aspiers Apr 17, 2026
685bc7f
refactor(preview): extract queryString / sendHtml / getBranding helpers
aspiers Apr 17, 2026
ca3d60f
test(preview): cover preview-ui; silence pino under NODE_ENV=test
aspiers Apr 17, 2026
c02b24c
fix(preview): address CodeRabbit feedback
aspiers Apr 19, 2026
7141fe4
fix(preview): carry client_id across the sibling-service heading link
aspiers Apr 19, 2026
10230b4
refactor(preview-validation): split the 250-line orchestrator into pe…
aspiers Apr 19, 2026
031f8be
refactor(preview-validation): wrap tos_uri / policy_uri in named chec…
aspiers Apr 19, 2026
bb1d557
chore: address SonarCloud minor findings + document pre-push checks
aspiers Apr 19, 2026
ede6929
refactor(pds-core): extract installPreviewRoutes from main()
aspiers Apr 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 32 additions & 21 deletions .agents/skills/epds-login/references/client-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,27 +98,38 @@ key generation and serving details.

## All supported fields

| Field | Required | Description |
| --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client_id` | Yes | Must match the URL where this file is hosted |
| `client_name` | Yes | Shown on the login page and in OTP emails |
| `redirect_uris` | Yes | Array of allowed callback URLs after login |
| `scope` | Yes | Always `"atproto transition:generic"` |
| `grant_types` | Yes | Always `["authorization_code", "refresh_token"]` |
| `response_types` | Yes | Always `["code"]` |
| `token_endpoint_auth_method` | Yes | `"private_key_jwt"` (recommended) or `"none"` — see above |
| `token_endpoint_auth_signing_alg` | Conditional | Required when `token_endpoint_auth_method` is `"private_key_jwt"`. Must be `"ES256"`. |
| `jwks_uri` | Conditional | Public JWKS URL. Required for `"private_key_jwt"` unless `jwks` is provided. Mutually exclusive with `jwks`. |
| `jwks` | Conditional | Inline JWKS object (`{"keys": [...]}`). Alternative to `jwks_uri`. Mutually exclusive with `jwks_uri`. |
| `dpop_bound_access_tokens` | Yes | Always `true` |
| `client_uri` | No | Your app's homepage URL |
| `logo_uri` | No | URL to your app logo (shown on login page) |
| `email_template_uri` | No | URL to a custom OTP email HTML template |
| `email_subject_template` | No | Custom email subject line with `{{code}}` placeholder |
| `brand_color` | No | Hex colour for buttons and input focus rings (default: `#1A130F`) |
| `background_color` | No | Hex colour for the login page background (default: `#F2EBE4`) |
| `epds_handle_mode` | No | ePDS extension. Handle picker variant for new users: `"picker"`, `"random"`, or `"picker-with-random"` (default). See [tutorial](../../docs/tutorial.md#optional-control-the-handle-picker). |
| `epds_skip_consent_on_signup` | No | ePDS extension. When `true`, skip the consent screen on initial sign-up. Only honoured when the PDS has `PDS_SIGNUP_ALLOW_CONSENT_SKIP=true` AND the client is in `PDS_OAUTH_TRUSTED_CLIENTS`. |
| Field | Required | Description |
| --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client_id` | Yes | Must match the URL where this file is hosted |
| `client_name` | Yes | Shown on the login page and in OTP emails |
| `redirect_uris` | Yes | Array of allowed callback URLs after login |
| `scope` | Yes | Always `"atproto transition:generic"` |
| `grant_types` | Yes | Always `["authorization_code", "refresh_token"]` |
| `response_types` | Yes | Always `["code"]` |
| `token_endpoint_auth_method` | Yes | `"private_key_jwt"` (recommended) or `"none"` — see above |
| `token_endpoint_auth_signing_alg` | Conditional | Required when `token_endpoint_auth_method` is `"private_key_jwt"`. Must be `"ES256"`. |
| `jwks_uri` | Conditional | Public JWKS URL. Required for `"private_key_jwt"` unless `jwks` is provided. Mutually exclusive with `jwks`. |
| `jwks` | Conditional | Inline JWKS object (`{"keys": [...]}`). Alternative to `jwks_uri`. Mutually exclusive with `jwks_uri`. |
| `dpop_bound_access_tokens` | Yes | Always `true` |
| `client_uri` | No | Your app's homepage URL |
| `logo_uri` | No | URL to your app logo (shown on login page) |
| `email_template_uri` | No | URL to a custom OTP email HTML template |
| `email_subject_template` | No | Custom email subject line with `{{code}}` placeholder |
| `brand_color` | No | Hex colour for buttons and input focus rings (default: `#1A130F`) |
| `background_color` | No | Hex colour for the login page background (default: `#F2EBE4`) |
| `epds_handle_mode` | No | ePDS extension. Handle picker variant for new users: `"picker"`, `"random"`, or `"picker-with-random"` (default). See [tutorial](../../../../docs/tutorial.md#optional-control-the-handle-picker). |
| `epds_skip_consent_on_signup` | No | ePDS extension. When `true`, skip the consent screen on initial sign-up. Only honoured when the PDS has `PDS_SIGNUP_ALLOW_CONSENT_SKIP=true` AND the client is in `PDS_OAUTH_TRUSTED_CLIENTS`. |
| `branding` | No | ePDS extension. Object containing a `css` string (max 32 KB). ePDS injects this CSS into login, OTP, choose-handle, recovery, and consent pages. Only honoured when the client is in `PDS_OAUTH_TRUSTED_CLIENTS`. Iterate via auth-service preview routes (see below). |

## Iterating on `branding.css`

Both auth-service and pds-core expose static `/preview/*` routes (when the
operator sets `AUTH_PREVIEW_ROUTES=1` / `PDS_PREVIEW_ROUTES=1`, typically on
preview envs and `pr-base`) that render each page with fixture data so client
devs can iterate without going through a real OAuth flow. auth-service covers
login / OTP / choose-handle / recovery; pds-core covers the consent page. See
[the client tutorial's "Iterating on `branding.css`" section](../../../../docs/tutorial.md#iterating-on-brandingcss)
for the route list and example URLs.

## Custom email templates

Expand Down
11 changes: 11 additions & 0 deletions .changeset/auth-preview-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'ePDS': minor
---

Add preview routes on auth-service and pds-core for iterating on client branding CSS.

**Affects:** Client app developers, Operators

**Client app developers:** Two new sets of preview routes render the ePDS sign-in pages against fixture data, so you can iterate on your `branding.css` without walking through a real OAuth flow each time. The auth-service exposes `/preview`, `/preview/login`, `/preview/login-otp`, `/preview/choose-handle`, `/preview/recovery`, and `/preview/recovery-otp`. pds-core exposes `/preview` and `/preview/consent` — the consent route renders the same `@atproto/oauth-provider-ui` SPA as the real `/oauth/authorize` page, hydrated with fixture data. Pass `?client_id=<URL-of-your-client-metadata.json>` on any of them to inject that client's `branding.css` into the page, subject to the same `PDS_OAUTH_TRUSTED_CLIENTS` check as a real OAuth flow. Without `client_id` the page renders unbranded (baseline). Iterating becomes: edit `branding.css`, refresh the preview URL — no OTP emails, no walking through the full flow. Visit `/preview` on either service for an index.

**Operators:** Two new env vars gate the preview routes, one per service: `AUTH_PREVIEW_ROUTES=1` on auth-service, `PDS_PREVIEW_ROUTES=1` on pds-core. Both are independent and safe to enable on preview deployments (Railway PR previews, `pr-base`, dev) and on local development instances. The preview routes have no effect on real auth flows — they short-circuit real state — so they can technically run in production too, but they are a developer-only surface and are best left off outside of preview/dev envs. See `packages/auth-service/.env.example` and `packages/pds-core/.env.example` for the full notes.
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@ SESSION_UPDATE_AGE=86400
# Defaults to 'picker-with-random' if not set.
EPDS_DEFAULT_HANDLE_MODE=picker-with-random

# Expose /preview/* routes that render each auth-service / pds-core page
# with fixture data, so client-app developers can iterate on their
# branding.css without walking through a real OAuth flow each time. The
# trusted-clients gate on CSS injection is preserved: a client_id passed via
# ?client_id=... only gets its branding.css injected when it's on
# PDS_OAUTH_TRUSTED_CLIENTS, exactly as in a real OAuth flow. Intended for
# preview envs and dev instances — the routes have no effect on real flows
# but are a developer-only surface that shouldn't be left on in production.
# AUTH_PREVIEW_ROUTES covers login / OTP / choose-handle / recovery pages
# (auth-service); PDS_PREVIEW_ROUTES covers the consent page (pds-core).
# Both flags are independent — enable whichever pages you need.
# AUTH_PREVIEW_ROUTES=1
# PDS_PREVIEW_ROUTES=1
Comment thread
aspiers marked this conversation as resolved.
Outdated

# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# GITHUB_CLIENT_ID=
Expand Down
9 changes: 9 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ tag closure. The CSP `style-src` directive is updated with a SHA-256
hash of the injected CSS. Untrusted clients never get CSS injection
regardless of what their metadata contains.

Client-app developers can iterate on their `branding.css` without
walking through a real OAuth flow each time by setting
`AUTH_PREVIEW_ROUTES=1` on the auth-service (covers login / OTP /
choose-handle / recovery) and `PDS_PREVIEW_ROUTES=1` on pds-core
(covers the consent page). See the
["Iterating on `branding.css`" section of the client tutorial](./tutorial.md#iterating-on-brandingcss)
for the list of preview routes and example URLs. Intended for preview
envs and dev instances only, not production.

Optional PDS email variables:

| Variable | Description |
Expand Down
Loading
Loading