Skip to content

Restyle login page with white card layout + Certified branding#110

Open
s-adamantine wants to merge 8 commits intomainfrom
restyle-login-page
Open

Restyle login page with white card layout + Certified branding#110
s-adamantine wants to merge 8 commits intomainfrom
restyle-login-page

Conversation

@s-adamantine
Copy link
Copy Markdown
Contributor

@s-adamantine s-adamantine commented Apr 22, 2026

Summary

Restyle the OAuth login page (packages/auth-service/src/routes/login-page.ts):

Before:

Screenshot 2026-04-23 at 10 57 56 Screenshot 2026-04-23 at 10 58 25

After:
Screenshot 2026-04-23 at 01 30 18

Screenshot 2026-04-23 at 10 53 02
  • White card on muted grey: floating card layout, rounded inputs, pill-shaped Continue + ATProto buttons, "Powered by Certified" footer below the card.
  • New Certified branding: inlined Certified wordmark SVG (themeable via currentColor), Certified brandmark as fallback top logo when a client omits logo_uri.
  • "Or sign in with ATProto/Bluesky" button: toggles the email form into handle-entry mode (same pattern as the demo's LoginForm). Handle submission redirects to <client-origin>/api/oauth/login?handle=X, which resolves the handle dynamically and starts a fresh PAR against the correct PDS — so off-PDS handles end up on their own PDS's auth screen, just like the demo's flow.
  • Terms & Privacy line: hardcoded By signing in, you agree to Certified's Terms of Use and Privacy Policy. — both links underlined and pointing to https://certified.app/terms and https://certified.app/privacy. Copy does not vary per client (the auth server is always Certified's, regardless of which client initiated the flow).
  • Copy tweaks: Email addressEnter your email address, Continue with emailContinue, removed to use <AppName> subtitle, removed the h1 subtitle treatment.

Theming — CSS variables for trusted clients

Trusted clients can override these from their injected branding.css:

:root {
  --page-bg: #YOUR_OUTER_BG;
  --card-bg: #YOUR_CARD_BG;
  --input-bg: #YOUR_INPUT_BG;
  --input-border: #YOUR_INPUT_BORDER;
  --muted-foreground: #YOUR_MUTED_TEXT;  /* terms + "Powered by" color */
}

Defaults: --page-bg: #E8E8E8, --card-bg: #F8F8F8, --input-bg: #ffffff, --input-border: #e5e5e5, --muted-foreground: #999. The email input and the ATProto button consume the same --input-bg / --input-border so they stay visually matched.

Hiding the "Recover with backup email" link

The recovery link in the OTP step is shown by default. Clients that don't want to surface backup-email recovery from the OAuth login flow can hide it from their branding.css:

:root { --recovery-link-display: none; }

The link itself stays in the DOM (so the recovery flow at /auth/recover is still reachable via direct navigation, e.g. from the /account page) — only its appearance on the login page is suppressed. Default is block.

Assets added

  • packages/auth-service/public/certified-text-monochrome.svg — wordmark (inlined into the page, themeable via currentColor)
  • packages/auth-service/public/certified-brandmark.svg — square icon (fallback top logo, served at /static/certified-brandmark.svg)

Before / After

Before:

After:

Preview URLs (PR-110 Railway environment)

Test plan

  • Launch stack and visit the login page via the demo app — confirm card layout, rounded inputs, footer.
  • ATProto button toggles email ↔ handle mode; submit in handle mode redirects to <client-origin>/api/oauth/login?handle=X and (for off-PDS handles) ends up on the correct PDS's auth screen.
  • Log in from a client without logo_uri — Certified brandmark appears as top logo.
  • Log in from a client with logo_uri — its logo appears at top.
  • Trusted-client branding.css overrides work: set --page-bg, --card-bg, --input-bg, --input-border, --muted-foreground and confirm all surfaces retint in unison.
  • Recovery link visible by default on the OTP step; injecting :root { --recovery-link-display: none; } from a client's branding.css hides it.
  • OTP step renders correctly (card still centered, input/button styling consistent).
  • Terms/Privacy links underline correctly and open https://certified.app/terms and https://certified.app/privacy in new tabs.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Six-digit segmented one-time-code entry with keyboard navigation, paste handling, and auto-submission.
    • New theme preset option for login page customization.
    • Account recovery link now displayed during OTP entry by default.
  • Style

    • Redesigned login page with centered card layout and updated color scheme.
    • Refreshed input and button styling across authentication interface.
    • Updated submit button copy ("Continue" in email mode).
  • Tests

    • Updated end-to-end tests to cover new segmented OTP entry behavior.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
epds-demo Ready Ready Preview, Comment Apr 29, 2026 10:02am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a theme-driven CSS system for the login page with redesigned card and typography, replaces branding.background_color with theme variables, swaps the logo fallback to a Certified brandmark with "Powered by" text, restructures OTP entry from a single input to per-digit segmented boxes with auto-submission, updates email/handle mode submit labels, adds optional terms consent block shown only on email step, and extends the demo theme system with border properties and a new cream preset.

Changes

Cohort / File(s) Summary
Login Page Redesign
packages/auth-service/src/routes/login-page.ts
Introduces theme-driven CSS variables (--page-bg, --card-bg, --focus-border, etc.), replaces branding.background_color usage, adds Certified brandmark SVG with "Powered by" text using currentColor tinting, refactors OTP entry from single #code text input to per-digit .otp-box elements with input/paste handling, arrow/backspace navigation, automatic code aggregation, and auto-submission on final digit, adds dynamic #heading and optional terms consent block (email step only), changes recovery-backup-email link to render by default with opt-out via --recovery-link-display: none CSS variable, updates submit button labels to "Continue" in email mode.
Demo Theme System
packages/demo/src/lib/theme.ts, packages/demo/src/app/components/PageShell.tsx, packages/demo/.env.example
Extends PageTheme interface with cardBorder and secondaryBorder properties, populates these in existing ocean and amber presets, adds new cream theme preset with distinct page palette and extensive injectedCss overrides for consent and login-page styling, updates .env.example documentation to reflect expanded preset list and new default theme behavior.
E2E OTP Refactoring
e2e/support/otp.ts, e2e/step-definitions/auth.steps.ts, e2e/step-definitions/client-branding.steps.ts, e2e/step-definitions/consent.steps.ts, e2e/step-definitions/account-recovery.steps.ts, e2e/support/flows.ts
Introduces shared fillOtp(page, otp) helper for segmented OTP entry targeting .otp-box elements with auto-submission, replaces direct #code filling and verify-button clicking across multiple step definitions, updates OTP length/character set derivation to read .otp-box count and first box's inputmode, changes account recovery navigation from UI-driven (clicking #recovery-link) to direct page.goto(/auth/recover), updates asserted login-page background color from rgb(248, 249, 250) to rgb(232, 232, 232).
Release Notes
.changeset/login-page-theming.md
Documents refreshed sign-in page design with centered white card on muted background, updated input/button styling, six-segment OTP entry with enhanced keyboard/paste and auto-submit behavior, new theming via CSS custom properties for client-driven overrides, deprecation of background_color metadata in favor of --page-bg, and default visibility of recovery-backup-email link with opt-out CSS variable.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • aspiers

Poem

🐰 A segmented code so fine, in boxes dancing in a line,
Cream and ocean, amber too, theming takes a brand-new hue,
OTP flows with auto-grace, Certified adorns the space,
One page redesigned with care, rabbit hops with flair!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main changes: restyling the login page to a white card layout with Certified branding.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch restyle-login-page

Comment @coderabbitai help to get the list of available commands and usage tips.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: dd5faf4

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

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

@railway-app
Copy link
Copy Markdown

railway-app Bot commented Apr 22, 2026

🚅 Deployed to the ePDS-pr-110 environment in ePDS

Service Status Web Updated (UTC)
@certified-app/pds-core ✅ Success (View Logs) Web Apr 29, 2026 at 4:15 pm
@certified-app/auth-service ✅ Success (View Logs) Web Apr 29, 2026 at 3:07 pm
@certified-app/demo ✅ Success (View Logs) Web Apr 28, 2026 at 11:51 pm
@certified-app/demo untrusted ✅ Success (View Logs) Web Apr 28, 2026 at 11:51 pm

@coveralls-official
Copy link
Copy Markdown

coveralls-official Bot commented Apr 22, 2026

Coverage Report for CI Build 25102591635

Coverage increased (+0.1%) to 48.544%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: 40 of 40 lines across 5 files are fully covered (100%).
  • 1 coverage regression across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
packages/pds-core/src/lib/client-css-injection.ts 1 74.78%

Coverage Stats

Coverage Status
Relevant Lines: 2622
Covered Lines: 1282
Line Coverage: 48.89%
Relevant Branches: 1603
Covered Branches: 769
Branch Coverage: 47.97%
Branches in Coverage %: Yes
Coverage Strength: 5.0 hits per line

💛 - Coveralls

@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:12 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:25 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:40 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:47 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:53 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 21:58 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:00 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:04 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:05 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:09 Destroyed
@railway-app railway-app Bot temporarily deployed to ePDS / ePDS-pr-110 April 22, 2026 22:17 Destroyed
@blacksmith-sh

This comment has been minimized.

@blacksmith-sh

This comment has been minimized.

@s-adamantine s-adamantine marked this pull request as ready for review April 26, 2026 19:03
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, reopen this pull request to trigger a review.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/auth-service/src/routes/preview.ts (1)

74-100: ⚠️ Potential issue | 🟡 Minor

Validate AUTH_PREVIEW_DEFAULT_CLIENT_ID before echoing it back into the page.

If this env var is typoed, resolveClientMetadata() falls into the catch block but we still return the bad string as clientId. The preview login page then uses that value to derive the handle-sign-in redirect origin, so a misconfigured preview env breaks the toggle instead of cleanly falling back to https://preview.example/....

Proposed fix
-  const envDefault = process.env.AUTH_PREVIEW_DEFAULT_CLIENT_ID?.trim()
+  const rawEnvDefault = process.env.AUTH_PREVIEW_DEFAULT_CLIENT_ID?.trim()
+  let envDefault: string | undefined
+  if (rawEnvDefault) {
+    try {
+      envDefault = new URL(rawEnvDefault).toString()
+    } catch {
+      logger.warn(
+        { clientId: rawEnvDefault },
+        'Preview: ignoring invalid AUTH_PREVIEW_DEFAULT_CLIENT_ID',
+      )
+    }
+  }
   const fakeDefault = 'https://preview.example/client-metadata.json'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/preview.ts` around lines 74 - 100, The code
currently assigns envDefault to clientId without validating it, causing a bad
string to be echoed if resolveClientMetadata fails; change the flow so you only
use envDefault as the effective clientId after it successfully resolves.
Specifically, when envDefault exists, attempt to
resolveClientMetadata(envDefault, { noCache: true }) first (or validate it as a
well-formed client metadata URL) and only set clientId = envDefault if
resolution/validation succeeds; on failure fall back to the fakeDefault return
path and avoid returning the original bad envDefault. Keep usage of
getClientCss(clientId, metadata, trustedClients) and the existing logger.warn
for real resolution errors.
🧹 Nitpick comments (1)
packages/auth-service/src/routes/login-page.ts (1)

71-71: Consider using replaceAll() for clarity.

While replace() with a global regex flag works correctly, replaceAll() is more explicit about the intent to replace all occurrences and doesn't require the /g flag.

🔧 Suggested fix
-  .replace(/fill="#726A60"/g, 'fill="currentColor"')
+  .replaceAll('fill="#726A60"', 'fill="currentColor"')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/login-page.ts` at line 71, Replace the use
of .replace(/fill="#726A60"/g, 'fill="currentColor"') with a clearer
.replaceAll('fill="#726A60"', 'fill="currentColor"') call so it's explicit that
all occurrences are replaced; locate the occurrence of
.replace(/fill="#726A60"/g, 'fill="currentColor"') in the login-page code and
swap to .replaceAll(...) ensuring the input is a plain string (not a RegExp) and
behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/preview-default-client-id.md:
- Around line 13-15: The fenced code block containing the environment string
AUTH_PREVIEW_DEFAULT_CLIENT_ID is missing a language tag (MD040); update the
fence by adding a language identifier (use "text") after the opening ``` so the
block becomes ```text and keep the existing content unchanged to satisfy
markdownlint; locate the block in the .changeset/preview-default-client-id.md
file where AUTH_PREVIEW_DEFAULT_CLIENT_ID=... appears and apply this small edit.

In `@packages/demo/.env.example`:
- Around line 44-48: Update the comment for EPDS_CLIENT_THEME to correctly
describe the unset behavior: mention that when EPDS_CLIENT_THEME is unset
getTheme() returns null, the demo pages use their own built-in light theme (not
the auth-service “Certified” styling), and the only auth-service effect is that
client-metadata.json stops shipping branding CSS; reference EPDS_CLIENT_THEME
and getTheme() in the comment for clarity.

In `@packages/demo/src/lib/theme.ts`:
- Around line 246-305: The auth-service CSS custom properties and .otp-box focus
override are only present in the cream preset; update the injectedCss for the
ocean and amber presets to include the same :root variable block (--page-bg,
--card-bg, --input-bg, --input-border, --muted-foreground, --card-border,
--btn-secondary-border, --focus-border) and the .otp-box:focus { border-color:
`#C8996C` !important; } rule so those themes apply the restyled login/OTP styling;
locate the injectedCss arrays for the ocean and amber theme entries (same
structure as the cream entry) and add the matching :root declaration and
.otp-box override alongside the existing auth-service selectors.

---

Outside diff comments:
In `@packages/auth-service/src/routes/preview.ts`:
- Around line 74-100: The code currently assigns envDefault to clientId without
validating it, causing a bad string to be echoed if resolveClientMetadata fails;
change the flow so you only use envDefault as the effective clientId after it
successfully resolves. Specifically, when envDefault exists, attempt to
resolveClientMetadata(envDefault, { noCache: true }) first (or validate it as a
well-formed client metadata URL) and only set clientId = envDefault if
resolution/validation succeeds; on failure fall back to the fakeDefault return
path and avoid returning the original bad envDefault. Keep usage of
getClientCss(clientId, metadata, trustedClients) and the existing logger.warn
for real resolution errors.

---

Nitpick comments:
In `@packages/auth-service/src/routes/login-page.ts`:
- Line 71: Replace the use of .replace(/fill="#726A60"/g, 'fill="currentColor"')
with a clearer .replaceAll('fill="#726A60"', 'fill="currentColor"') call so it's
explicit that all occurrences are replaced; locate the occurrence of
.replace(/fill="#726A60"/g, 'fill="currentColor"') in the login-page code and
swap to .replaceAll(...) ensuring the input is a plain string (not a RegExp) and
behavior remains identical.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 245a3363-f6e9-46b7-8110-75080372b2c5

📥 Commits

Reviewing files that changed from the base of the PR and between b89dbd1 and 3dde305.

⛔ Files ignored due to path filters (2)
  • packages/auth-service/public/certified-brandmark.svg is excluded by !**/*.svg
  • packages/auth-service/public/certified-text-monochrome.svg is excluded by !**/*.svg
📒 Files selected for processing (9)
  • .changeset/preview-default-client-id.md
  • .env.example
  • docs/configuration.md
  • packages/auth-service/.env.example
  • packages/auth-service/src/routes/login-page.ts
  • packages/auth-service/src/routes/preview.ts
  • packages/demo/.env.example
  • packages/demo/src/app/components/PageShell.tsx
  • packages/demo/src/lib/theme.ts

Comment thread .changeset/preview-default-client-id.md Outdated
Comment thread packages/demo/.env.example Outdated
Comment thread packages/demo/src/lib/theme.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
packages/demo/src/lib/theme.ts (1)

227-306: Well-structured cream theme that follows established patterns.

The warm cream palette is cohesive, and the injectedCss comprehensively covers both Provider-UI consent page and auth-service login/OTP/recovery markup. Good coverage of both .otp-box:focus (new multi-box OTP) and .otp-input:focus (legacy) selectors.

Regarding the static analysis warnings about escaped backslashes (e.g., .md\\:bg-slate-100): this pattern is already used in ocean and amber themes. If you'd like to address it, consider refactoring all three presets to use String.raw consistently for the Tailwind selector strings—but this is optional and low-priority since the current escaping works correctly.

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/demo/src/lib/theme.ts` around lines 227 - 306, Summary: optional
static-analysis cleanup for escaped backslashes in cream.injectedCss. If you
want to address the warnings, update the cream theme's injectedCss (the "cream"
Theme object and its injectedCss property) to use raw string literals for the
Tailwind selectors (e.g., replace escaped sequences like ".md\\:bg-slate-100"
with String.raw templates) and apply the same change consistently to the other
presets (ocean, amber) so selectors remain identical while removing
backslash-escape warnings; otherwise no functional change is required.
packages/auth-service/src/routes/login-page.ts (2)

71-71: Consider using replaceAll() for clarity.

The regex with g flag works correctly, but replaceAll() expresses intent more clearly. Per SonarCloud hint.

♻️ Suggested change
-.replace(/fill="#726A60"/g, 'fill="currentColor"')
+.replaceAll('fill="#726A60"', 'fill="currentColor"')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/login-page.ts` at line 71, Replace the
regex-based call .replace(/fill="#726A60"/g, 'fill="currentColor"') with the
clearer string-based .replaceAll('fill="#726A60"', 'fill="currentColor"') in the
code that processes the SVG (the place where .replace is invoked) so intent is
explicit and SonarCloud's suggestion is satisfied; ensure the runtime target
supports replaceAll or add a small polyfill/alternative if needed.

24-26: Node built-in imports should be placed before external packages.

Per coding guidelines, imports should be ordered: (1) Node built-ins with node: prefix, (2) External packages. These new imports should be moved above the express import on line 22.

♻️ Suggested import reordering
+import { readFileSync } from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
 import { Router, type Request, type Response } from 'express'
 import { randomBytes } from 'node:crypto'
-import { readFileSync } from 'node:fs'
-import path from 'node:path'
-import { fileURLToPath } from 'node:url'

Note: node:crypto on line 23 should also move to the top group with the other Node built-ins.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/login-page.ts` around lines 24 - 26, Reorder
the imports so Node built-ins come before external packages: move the node:fs
import (readFileSync), node:path import (path), node:url import (fileURLToPath)
and node:crypto import to the top import group, placing them above the express
import; keep external package imports (e.g., express) after the built-ins and
maintain consistent grouping/ordering and spacing between groups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/auth-service/src/routes/login-page.ts`:
- Line 71: Replace the regex-based call .replace(/fill="#726A60"/g,
'fill="currentColor"') with the clearer string-based
.replaceAll('fill="#726A60"', 'fill="currentColor"') in the code that processes
the SVG (the place where .replace is invoked) so intent is explicit and
SonarCloud's suggestion is satisfied; ensure the runtime target supports
replaceAll or add a small polyfill/alternative if needed.
- Around line 24-26: Reorder the imports so Node built-ins come before external
packages: move the node:fs import (readFileSync), node:path import (path),
node:url import (fileURLToPath) and node:crypto import to the top import group,
placing them above the express import; keep external package imports (e.g.,
express) after the built-ins and maintain consistent grouping/ordering and
spacing between groups.

In `@packages/demo/src/lib/theme.ts`:
- Around line 227-306: Summary: optional static-analysis cleanup for escaped
backslashes in cream.injectedCss. If you want to address the warnings, update
the cream theme's injectedCss (the "cream" Theme object and its injectedCss
property) to use raw string literals for the Tailwind selectors (e.g., replace
escaped sequences like ".md\\:bg-slate-100" with String.raw templates) and apply
the same change consistently to the other presets (ocean, amber) so selectors
remain identical while removing backslash-escape warnings; otherwise no
functional change is required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0b58c27b-1134-437b-b6d2-ff553e3ab603

📥 Commits

Reviewing files that changed from the base of the PR and between 3dde305 and c7fea74.

⛔ Files ignored due to path filters (2)
  • packages/auth-service/public/certified-brandmark.svg is excluded by !**/*.svg
  • packages/auth-service/public/certified-text-monochrome.svg is excluded by !**/*.svg
📒 Files selected for processing (9)
  • .changeset/preview-default-client-id.md
  • .env.example
  • docs/configuration.md
  • packages/auth-service/.env.example
  • packages/auth-service/src/routes/login-page.ts
  • packages/auth-service/src/routes/preview.ts
  • packages/demo/.env.example
  • packages/demo/src/app/components/PageShell.tsx
  • packages/demo/src/lib/theme.ts
✅ Files skipped from review due to trivial changes (4)
  • packages/demo/src/app/components/PageShell.tsx
  • docs/configuration.md
  • packages/demo/.env.example
  • .env.example
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/auth-service/.env.example

@blacksmith-sh

This comment has been minimized.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
e2e/support/otp.ts (1)

2-2: Avoid hardcoding “6-box” in the helper docs

The helper logic is dynamic; the comment should be too, to avoid future confusion when OTP_LENGTH changes.

Based on learnings: "OTP codes are configurable via the OTP_LENGTH environment variable (range 4–12...)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/support/otp.ts` at line 2, Update the helper comment in
e2e/support/otp.ts to remove the hardcoded “6-box” phrasing and instead describe
that the helper fills the segmented OTP input dynamically based on the
configured length (OTP_LENGTH env var); reference the helper that fills the
auth-service login page’s segmented OTP input and mention the OTP_LENGTH
environment variable (valid range 4–12) so the doc reflects the code’s dynamic
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/support/otp.ts`:
- Around line 18-24: The fillOtp routine currently clears inputs by the rendered
box count (boxes.count()) but fills using otp.length, which can cause
out-of-range locator errors or missed auto-submit behavior; update the filling
logic in the fillOtp function to compute a fillCount = Math.min(count,
otp.length) and iterate only up to fillCount when calling
boxes.nth(i).fill(...), and optionally handle/log the case where otp.length !==
count so callers/maintainers know a mismatch occurred.

---

Nitpick comments:
In `@e2e/support/otp.ts`:
- Line 2: Update the helper comment in e2e/support/otp.ts to remove the
hardcoded “6-box” phrasing and instead describe that the helper fills the
segmented OTP input dynamically based on the configured length (OTP_LENGTH env
var); reference the helper that fills the auth-service login page’s segmented
OTP input and mention the OTP_LENGTH environment variable (valid range 4–12) so
the doc reflects the code’s dynamic behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2271e248-6f8f-43c6-89ae-23f431512c3b

📥 Commits

Reviewing files that changed from the base of the PR and between c7fea74 and 4cd1f4d.

📒 Files selected for processing (6)
  • e2e/step-definitions/auth.steps.ts
  • e2e/step-definitions/client-branding.steps.ts
  • e2e/step-definitions/consent.steps.ts
  • e2e/step-definitions/session-reuse.steps.ts
  • e2e/support/flows.ts
  • e2e/support/otp.ts

Comment thread e2e/support/otp.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/auth-service/src/routes/login-page.ts (1)

735-748: ⚠️ Potential issue | 🟡 Minor

Reset the message styling after a successful resend.

This branch turns the shared error box green, but the normal error path never restores the default red styles. Any later real error on the same page will still render as a success message.

Suggested fix
       function showError(msg) {
         errorEl.textContent = msg;
         errorEl.style.display = 'block';
+        errorEl.style.color = '#dc3545';
+        errorEl.style.background = '#fdf0f0';
       }
...
-          showError('Code resent!');
-          errorEl.style.color = '#28a745';
-          errorEl.style.background = '#f0fff4';
+          errorEl.textContent = 'Code resent!';
+          errorEl.style.display = 'block';
+          errorEl.style.color = '#28a745';
+          errorEl.style.background = '#f0fff4';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth-service/src/routes/login-page.ts` around lines 735 - 748, The
resend handler currently sets the shared error element (errorEl) to success
styling on success but never restores default error styling for subsequent real
errors; update the click listener attached to
document.getElementById('btn-resend') (the async function that calls sendOtp and
uses clearError/showError) so that after a success it sets a flag or attaches
the original default styles back to errorEl (e.g., store original
color/background before changing them) and ensure showError restores those
default red error styles when called with an actual error; in short, capture and
restore errorEl's original styles in the resend success branch and make
showError reset to those defaults when showing an error.
🧹 Nitpick comments (1)
e2e/step-definitions/account-recovery.steps.ts (1)

189-193: Update the note about #recovery-link.

The direct page.goto() is reasonable here, but this comment now documents the wrong behavior: packages/auth-service/src/routes/login-page.ts still renders id="recovery-link" and shows it by default unless client CSS sets --recovery-link-display: none.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/step-definitions/account-recovery.steps.ts` around lines 189 - 193,
Update the inline comment to reflect the true behavior: the login page still
renders an element with id="recovery-link" (see
packages/auth-service/src/routes/login-page.ts) and it is visible by default
unless client CSS sets --recovery-link-display: none; keep the note that direct
page.goto() is acceptable and that the auth_flow cookie carries the real
request_uri for back-link resolution, but remove the incorrect statement that
the recovery link was removed from the page and instead state that it is present
but may be hidden by client CSS.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/auth-service/src/routes/login-page.ts`:
- Around line 473-480: The OTP input template renders numeric-specific
labels/placeholders; update the rendering logic that builds each `<input
class="otp-box" data-slot="...">` (the loop using opts.otpLength and inputProps)
to make labels and placeholders charset-aware by checking opts.otpCharset (e.g.,
'numeric' vs 'alphanumeric') and: set aria-label to "Digit N" and placeholder
"0" for numeric, but "Character N" and placeholder "A" (or a generic letter) for
alphanumeric; ensure inputmode/autocapitalize remain driven by inputProps and
that the conditional autocomplete behavior stays the same.
- Around line 342-345: The client-supplied b.brand_color is being interpolated
into CSS without CSS-context validation (escapeHtml is insufficient); validate
b.brand_color against a safe color pattern (e.g. only accept hex colors like
/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/) and if it fails, fall back to the default
'#1A130F'; update the assignment to brandColor (and any other uses in the later
block around the logo/branding code) to perform this check before interpolation,
leaving escapeHtml for HTML attributes like logoHtml but not as the CSS
sanitizer.
- Around line 506-507: The code currently assigns var clientId =
${JSON.stringify(opts.clientId)} and uses it to build a handle redirect target
client-side, which allows open-redirect if clientId came from a user-controlled
query; instead, stop using an unvalidated opts.clientId in the client bundle:
render a trusted, server-validated handle origin (e.g., a new server-side
variable like validatedHandleOrigin) into the template and use that value for
the redirect, and disable the handle-redirect path when no validatedHandleOrigin
is available; update the code that sets clientId/requestUri in
routes/login-page.ts to read from that server-side validated value and ensure
any branch that builds window.location.href for handle-login checks for its
presence before performing a redirect.

---

Outside diff comments:
In `@packages/auth-service/src/routes/login-page.ts`:
- Around line 735-748: The resend handler currently sets the shared error
element (errorEl) to success styling on success but never restores default error
styling for subsequent real errors; update the click listener attached to
document.getElementById('btn-resend') (the async function that calls sendOtp and
uses clearError/showError) so that after a success it sets a flag or attaches
the original default styles back to errorEl (e.g., store original
color/background before changing them) and ensure showError restores those
default red error styles when called with an actual error; in short, capture and
restore errorEl's original styles in the resend success branch and make
showError reset to those defaults when showing an error.

---

Nitpick comments:
In `@e2e/step-definitions/account-recovery.steps.ts`:
- Around line 189-193: Update the inline comment to reflect the true behavior:
the login page still renders an element with id="recovery-link" (see
packages/auth-service/src/routes/login-page.ts) and it is visible by default
unless client CSS sets --recovery-link-display: none; keep the note that direct
page.goto() is acceptable and that the auth_flow cookie carries the real
request_uri for back-link resolution, but remove the incorrect statement that
the recovery link was removed from the page and instead state that it is present
but may be hidden by client CSS.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d49ec4f8-0f51-4a5f-9b55-f97425eee997

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd1f4d and 287d623.

📒 Files selected for processing (4)
  • .changeset/login-page-theming.md
  • e2e/step-definitions/account-recovery.steps.ts
  • e2e/step-definitions/client-branding.steps.ts
  • packages/auth-service/src/routes/login-page.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/login-page-theming.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/step-definitions/client-branding.steps.ts

Comment thread packages/auth-service/src/routes/login-page.ts Outdated
Comment thread packages/auth-service/src/routes/login-page.ts Outdated
Comment thread packages/auth-service/src/routes/login-page.ts Outdated
@blacksmith-sh

This comment has been minimized.

@blacksmith-sh

This comment has been minimized.

s-adamantine and others added 5 commits April 29, 2026 00:21
White card on muted grey, pill buttons, segmented 6-box OTP input
with paste/arrow/backspace/auto-submit, "Powered by Certified"
footer, Certified brandmark as fallback top logo.

Adds an ATProto/Bluesky toggle that swaps the email form into
handle-entry mode and submits to <client-origin>/api/oauth/login?handle=X
— the client then resolves the handle to its PDS and starts a fresh
PAR. Off-PDS handles end up on their own PDS's auth screen.

Removes the "Recover with backup email" link from the page.
Hardcodes the Terms/Privacy notice to Certified's terms (the auth
server is always Certified's regardless of which client started the
flow), with both links underlined and pointing at certified.app.

Hides the Terms/Privacy notice on the OTP step. h1 reads "Sign in"
on the email step, "Enter your code" on the OTP step.

Exposes CSS custom properties for trusted-client theming:
  --page-bg, --card-bg, --input-bg, --input-border,
  --muted-foreground, --card-border, --btn-secondary-border,
  --focus-border
Defaults stay neutral grey; trusted clients can override any of
these via injected branding.css.

Inlines the Certified wordmark SVG so the "Powered by" footer can
tint via currentColor.
The login-page restyle replaced the single visible OTP input with a
hidden #code field plus six .otp-box slots. page.fill('#code', ...)
resolves the locator but Playwright refuses to fill a type=hidden
input, timing out every OTP-step scenario.

Add a fillOtp(page, otp) helper that clears any stale digits, then
types one per box. The page's input handler auto-submits on the last
digit, so the explicit verify-button click is dropped at every call
site. The 'incorrect OTP {int} times' loop registers waitForResponse
before fillOtp since the submit fires from inside the helper.

Also reworks buildIncorrectOtpCode's DOM-inference fallback: length
now comes from .otp-box count and charset from a single box's
inputmode, since the hidden #code carries no maxlength / pattern.

The recovery flow's #code input is unchanged and untouched here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores the "Recover with backup email" link on the OTP step,
shown by default, controlled by the new --recovery-link-display
CSS var so clients can hide it via branding.css.

E2E: recovery flows navigate to /auth/recover directly (more
robust than depending on the link's visibility, which is now
client-configurable). DEFAULT_LOGIN_BG_RGB updated to match the
new --page-bg default (#E8E8E8).

Adds a changeset documenting the login-page CSS theming surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Validate brand_color as a hex literal before interpolating into
  <style>; escapeHtml() doesn't sanitise CSS context, so a value
  containing `;` or `}` could break out of the declaration.
- OTP slot placeholder/aria-label now switch to "A"/"Character" when
  otpCharset === 'alphanumeric', matching the configured charset.
- Clarify EPDS_CLIENT_THEME unset behaviour in packages/demo/.env.example
  (getTheme() returns null; demo uses its own light look; no branding
  CSS shipped → auth-service renders default Certified styling).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The atproto-login-button feature was authored before PR #110's restyle
changed the email label from "Email address" to "Enter your email
address". Update the assertion to match the new copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aspiers
Copy link
Copy Markdown
Contributor

aspiers commented Apr 29, 2026

Autonomous loop done — ready for review/merge

CI: 15/15 green (e2e finished in 8m8s).

Branch tip: 60aed03. Eight commits since origin/main:

60aed03 feat(preview): add ATProto/Bluesky login-button toggle on /preview
ac1e14c feat(pds-core): inject default Certified branding CSS
eb5ff20 feat(auth-service): drive ToS/privacy links from PDS env vars
0e5bf82 fix(e2e): expect "Enter your email address" label after restyle
b83a387 fix(auth-service): address PR #110 review feedback
f4f1040 feat(auth-service): recovery link + e2e fixes after restyle
5fc9721 test(e2e): drive segmented OTP boxes instead of hidden #code input
77d136f feat(auth-service): restyle login page + ATProto handle sign-in toggle

Changes since last user touch:

  • Env-var-driven Terms/Privacy links on the login page (PDS_TERMS_OF_SERVICE_URL, PDS_PRIVACY_POLICY_URL, optional PDS_LEGAL_ENTITY_NAME for the possessive). Strict mode: line is omitted entirely if either URL is unset. Set on Railway pr-base, ePDS-pr-110, and test envs to recreate the previous hardcoded copy.
  • Default Certified-style CSS injected into upstream @atproto/oauth-provider-ui consent + chooser pages so unbranded ePDS deployments render coherently with the auth-service login page (muted page bg, light card surface with thin border + radius + soft shadow, white-with-thin-border account rows / secondary buttons, dark primary button). Trusted-client branding.css continues to override via cascade order.
  • "Powered by Certified" footer on the login page is now a link to https://certified.app/ with no underline (target=_blank, rel=noopener noreferrer).
  • /preview index gained an "ATProto/Bluesky login button" checkbox next to "Login — email step"; ?atproto=1 synthesises an epds_handle_login_url so the button is reachable in preview without editing real client metadata.
  • Docs/scripts audit applied: docs/configuration.md documents the three new legal-URL env vars; docs/tutorial.md updated to drop background_color and document the CSS-var override surface; packages/pds-core/.env.example lists the legal URLs as [shared]; scripts/setup.sh propagates all three new vars from the top-level .env into per-package files.

Unresolved review threads: 0.

Ready for review/merge.

aspiers and others added 3 commits April 29, 2026 09:59
Replace the hardcoded https://certified.app/terms and /privacy URLs in
the login page with values read from PDS_TERMS_OF_SERVICE_URL /
PDS_PRIVACY_POLICY_URL — the same env vars upstream PDS already
consumes, so deployments configure them once. The terms line is
omitted entirely when either var is missing (strict; avoids dead
half-links).

Optional PDS_LEGAL_ENTITY_NAME controls the possessive in the line:
"By signing in, you agree to <name>'s Terms of Use and Privacy Policy."
When unset, falls back to "By signing in, you agree to the Terms of
Use and Privacy Policy."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream @atproto/oauth-provider-ui renders the consent + chooser
pages with purple-on-white defaults that don't match the rest of an
ePDS deployment. Inject a neutral default stylesheet (muted grey page
bg, light card surface, dark primary button) into every
/oauth/authorize response so unbranded deployments render coherently
with auth-service's restyled login page.

Trusted-client branding.css is now layered on top of the default —
cascade order means client styles still win on overlapping selectors,
no client opt-in or migration needed.

CSP: each <style> block is hashed separately and appended to
style-src.

Also covers the /preview/consent and /preview/chooser routes so
client-app developers iterate against the production look.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Justifies the absence of any preview-only override that synthesises
`epds_handle_login_url`: the /preview/login button visibility is
identical to the real /oauth/authorize flow, so iterating against
real client metadata is the only way to see the button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

${renderFaviconTag(opts.customFaviconUrl, opts.customFaviconUrlDark)}
<title>Sign in to ${escapeHtml(appName)}</title>
<style>
:root { --muted-foreground: #999; --input-bg: #ffffff; --input-border: #e5e5e5; --page-bg: #E8E8E8; --card-bg: #F8F8F8; --card-border: #E5E5E5; --btn-secondary-border: #e5e5e5; --focus-border: ${brandColor}; }
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.

Does it make sense to have this huge chunk of CSS embedded?

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