From fa83e668e3123475f187cd38ec4877895a7004ce Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 14 Aug 2026 12:51:11 +0000
Subject: [PATCH] chore: release
---
.changeset/announce-auth-errors.md | 9 -
.changeset/atproto-upstream-upgrade.md | 15 --
.changeset/charset-filter-on-otp-forms.md | 9 -
.changeset/clear-otp-boxes-on-resend.md | 9 -
.changeset/email-autocomplete-hints.md | 9 -
...allback-app-name-on-malformed-client-id.md | 9 -
.changeset/handle-mode-through-approval.md | 9 -
.changeset/handle-not-available-message.md | 9 -
.changeset/human-sign-in-error-copy.md | 9 -
.../keyboard-focus-visible-on-buttons.md | 9 -
.changeset/muted-text-contrast-on-sign-in.md | 11 --
.changeset/record-email-as-confirmed.md | 13 --
.changeset/require-secret-env-vars.md | 11 --
.changeset/resend-log-message-context.md | 9 -
.changeset/send-a-new-code-label.md | 9 -
.changeset/sign-in-button-readiness.md | 9 -
.../sign-in-errors-at-point-of-failure.md | 11 --
.changeset/suggest-mistyped-email-domains.md | 9 -
.changeset/unify-link-affordances.md | 11 --
.changeset/use-different-email-clears-form.md | 9 -
CHANGELOG.md | 174 ++++++++++++++++++
package.json | 2 +-
22 files changed, 175 insertions(+), 199 deletions(-)
delete mode 100644 .changeset/announce-auth-errors.md
delete mode 100644 .changeset/atproto-upstream-upgrade.md
delete mode 100644 .changeset/charset-filter-on-otp-forms.md
delete mode 100644 .changeset/clear-otp-boxes-on-resend.md
delete mode 100644 .changeset/email-autocomplete-hints.md
delete mode 100644 .changeset/fallback-app-name-on-malformed-client-id.md
delete mode 100644 .changeset/handle-mode-through-approval.md
delete mode 100644 .changeset/handle-not-available-message.md
delete mode 100644 .changeset/human-sign-in-error-copy.md
delete mode 100644 .changeset/keyboard-focus-visible-on-buttons.md
delete mode 100644 .changeset/muted-text-contrast-on-sign-in.md
delete mode 100644 .changeset/record-email-as-confirmed.md
delete mode 100644 .changeset/require-secret-env-vars.md
delete mode 100644 .changeset/resend-log-message-context.md
delete mode 100644 .changeset/send-a-new-code-label.md
delete mode 100644 .changeset/sign-in-button-readiness.md
delete mode 100644 .changeset/sign-in-errors-at-point-of-failure.md
delete mode 100644 .changeset/suggest-mistyped-email-domains.md
delete mode 100644 .changeset/unify-link-affordances.md
delete mode 100644 .changeset/use-different-email-clears-form.md
diff --git a/.changeset/announce-auth-errors.md b/.changeset/announce-auth-errors.md
deleted file mode 100644
index b499d6c2..00000000
--- a/.changeset/announce-auth-errors.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-Sign-in, recovery and handle-choice feedback is now announced automatically by screen readers.
-
-**Affects:** End users
-
-**End users:** authentication errors now use standard alert and live-region semantics across sign-in, recovery, handle selection, and the demo client, so screen-reader users hear failures without having to search the page for changed text. Repeated failures — such as entering a second incorrect one-time code — announce each time rather than falling silent, and handle availability ("Available!" / "Already taken.") is now announced as you type.
diff --git a/.changeset/atproto-upstream-upgrade.md b/.changeset/atproto-upstream-upgrade.md
deleted file mode 100644
index ea5a5ea9..00000000
--- a/.changeset/atproto-upstream-upgrade.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-'ePDS': minor
----
-
-Signing in and managing your account now run on the latest AT Protocol server, which requires operators to upgrade to Node.js 22.19 or newer.
-
-**Affects:** End users, Operators
-
-**End users:** the account and sign-in screens now come from a newer version of the underlying AT Protocol software, which adds changing your handle and deactivating or deleting your account through the account-management interface. The passwordless email-code sign-in and email verification you already use through ePDS are unchanged.
-
-**Operators:**
-
-- ePDS now requires **Node.js 22.19.0 or newer** (previously 20). Deployments pinned to Node 20 must upgrade; the Docker images already use `node:22-alpine`. `engines.node` is now `>=22.19.0` (the floor is set by a transitive `undici@8` runtime dependency, which fails on Node 22.0–22.18).
-- The bundled AT Protocol packages moved to their latest releases: `@atproto/pds` 0.5.23, `@atproto/oauth-provider` 0.21.1, `@atproto/oauth-provider-ui` 0.8.9.
-- No configuration changes are required, but the native `better-sqlite3` module must be built for the Node 22 ABI — a clean `pnpm install` on Node 22 handles this; an in-place Node version switch needs `pnpm rebuild better-sqlite3`.
diff --git a/.changeset/charset-filter-on-otp-forms.md b/.changeset/charset-filter-on-otp-forms.md
deleted file mode 100644
index ca87a8dd..00000000
--- a/.changeset/charset-filter-on-otp-forms.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-Sign-in code boxes now ignore stray characters pasted or typed alongside your code.
-
-**Affects:** End users
-
-**End users:** every screen that asks for a sign-in code — the main sign-in screen, Account Settings sign-in, and account recovery — now drops characters your code can't contain, so a code pasted with surrounding quotes, brackets, or a trailing full stop is accepted instead of rejected. The main sign-in screen previously ignored only spaces and line breaks. Where the operator has turned on codes that mix letters and numbers, typing them in lowercase now works too.
diff --git a/.changeset/clear-otp-boxes-on-resend.md b/.changeset/clear-otp-boxes-on-resend.md
deleted file mode 100644
index d0ee7e58..00000000
--- a/.changeset/clear-otp-boxes-on-resend.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-Asking for a new sign-in code now clears the boxes and tells you the old code has stopped working.
-
-**Affects:** End users
-
-**End users:** the boxes reset on **Send a new code**, so a half-typed old code no longer has to be deleted by hand before you can type the new one. The confirmation also warns that only the newest code will be accepted, and points at the spam folder — the two things most likely to be going wrong for anyone who got that far.
diff --git a/.changeset/email-autocomplete-hints.md b/.changeset/email-autocomplete-hints.md
deleted file mode 100644
index ed312c3b..00000000
--- a/.changeset/email-autocomplete-hints.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-Sign-in and recovery email fields now work more reliably with browser autofill and password managers.
-
-**Affects:** End users
-
-**End users:** email fields now identify themselves to browsers as email addresses, while the same sign-in field identifies itself as a username after switching to handle entry. This helps browsers and password managers offer the right saved value in each mode.
diff --git a/.changeset/fallback-app-name-on-malformed-client-id.md b/.changeset/fallback-app-name-on-malformed-client-id.md
deleted file mode 100644
index 376284f8..00000000
--- a/.changeset/fallback-app-name-on-malformed-client-id.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-Sign-in pages no longer leak a malformed client_id as the displayed app name.
-
-**Affects:** End users
-
-**End users:** if you arrived at a sign-in page with a broken `client_id` parameter on the URL (e.g. from a misconfigured app or a tampered link), the page would display that raw broken value as the app's name, e.g. "Sign in to not-a-url" or "Sign in to my-local-app". The page now falls back to "an application" in that case, so a malformed value in the URL doesn't leak into the visible page text or browser tab title.
diff --git a/.changeset/handle-mode-through-approval.md b/.changeset/handle-mode-through-approval.md
deleted file mode 100644
index 2936e07c..00000000
--- a/.changeset/handle-mode-through-approval.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-The approval and account-chooser screens now honour the handle mode your app asked for, instead of falling back to the server default.
-
-**Affects:** Client app developers
-
-**Client app developers:** no action needed — `epds_handle_mode` is resolved exactly as before (the `/oauth/authorize` query parameter, then your OAuth client metadata, then the server default). Previously the resolved mode was lost on the way to the approval step, so a flow that asked for a chosen handle could still show a generated one there. The mode is now carried through `/oauth/epds-callback` and applied on the approval and chooser screens. Values that are not one of the recognised modes are ignored rather than forwarded.
diff --git a/.changeset/handle-not-available-message.md b/.changeset/handle-not-available-message.md
deleted file mode 100644
index 6eb0bf43..00000000
--- a/.changeset/handle-not-available-message.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-Handle-picker error wording is now accurate when the handle is reserved.
-
-**Affects:** End users
-
-**End users:** if you tried to claim a handle that was on the reserved list (admin, www, …) the picker said "That handle was just taken — please choose another." That's misleading — it wasn't just taken, it's permanently unavailable. The wording is now "That handle is not available — please choose another." which doesn't imply you can wait it out.
diff --git a/.changeset/human-sign-in-error-copy.md b/.changeset/human-sign-in-error-copy.md
deleted file mode 100644
index 7d27cc54..00000000
--- a/.changeset/human-sign-in-error-copy.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-Sign-in errors are written in plain English instead of showing the raw failure code.
-
-**Affects:** End users
-
-**End users:** a rejected code used to report "Invalid OTP", an unexplained acronym that ran straight into the recovery link beside it — "Invalid OTP Send a new code". It now reads "That code didn't work." followed by the link, and the two other common failures are similarly rewritten: an aged-out code says "That code has expired.", and one rejected after repeated wrong attempts says "Too many tries — that code is no longer usable.". Any failure outside those three is still shown as-is rather than hidden behind a generic apology, so an unexpected problem can still be reported accurately.
diff --git a/.changeset/keyboard-focus-visible-on-buttons.md b/.changeset/keyboard-focus-visible-on-buttons.md
deleted file mode 100644
index bf99df16..00000000
--- a/.changeset/keyboard-focus-visible-on-buttons.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-Sign-in buttons now show a visible keyboard focus ring.
-
-**Affects:** End users
-
-**End users:** keyboard users tabbing through the sign-in pages couldn't tell where focus was — the buttons styled out the browser default focus ring without replacing it. Verify, Resend, Use different email, Recover, Send recovery code, Continue with email, Update handle, etc. all now show a clear outline when focused via the keyboard, while still hiding the ring on a mouse click.
diff --git a/.changeset/muted-text-contrast-on-sign-in.md b/.changeset/muted-text-contrast-on-sign-in.md
deleted file mode 100644
index 153de33b..00000000
--- a/.changeset/muted-text-contrast-on-sign-in.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'ePDS': patch
----
-
-Small grey text on the sign-in page is darker and easier to read.
-
-**Affects:** End users, Client app developers
-
-**End users:** the 13px grey text — the terms line, the "Recover with backup email" link, the "or continue with" / "or use email" separators, and the "powered by Certified" footer — was too light to meet the WCAG AA contrast minimum against either the card or the page background behind it. It is now dark enough to pass on both.
-
-**Client app developers:** the `--muted-foreground` custom property on the sign-in page changes from `#999` to `#666`. If your `branding.css` sets it, check your value clears 4.5:1 against both `#F8F8F8` (the card) and `#E8E8E8` (the page) — the old default reached only 2.68:1 and 2.33:1 respectively, so a value chosen to sit alongside it may be equally low. The separator text between the social and email sign-in options now follows `--muted-foreground` too, where it previously ignored your override and stayed grey.
diff --git a/.changeset/record-email-as-confirmed.md b/.changeset/record-email-as-confirmed.md
deleted file mode 100644
index efc2852d..00000000
--- a/.changeset/record-email-as-confirmed.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-'ePDS': patch
----
-
-Apps you sign in to are now told that your email address is confirmed.
-
-**Affects:** End users, Client app developers, Operators
-
-**End users:** Apps no longer ask you to verify an address you have already confirmed with an emailed code.
-
-**Client app developers:** The `email_verified` claim is now `true` for accounts that signed in through the emailed-code flow, instead of always `false`.
-
-**Operators:** Deploy the auth service and the PDS together — the signed handover between them carries a new required field, and a mixed pair rejects sign-in until both are updated. An older auth service is rejected with an explicit `Missing or invalid email_verified parameter` rather than a generic signature error, so a mixed-version rollout is recognisable in the logs. Accounts predating this release are repaired the next time their owner signs in **through the emailed-code flow**; a sign-in that does not prove control of the address leaves them unconfirmed. To fix the rest, see "Backfilling Email Confirmation" in `docs/deployment.md`.
diff --git a/.changeset/require-secret-env-vars.md b/.changeset/require-secret-env-vars.md
deleted file mode 100644
index c437bde3..00000000
--- a/.changeset/require-secret-env-vars.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'ePDS': patch
----
-
-Both services now refuse to start unless their secret environment variables are set, instead of silently falling back to well-known development defaults.
-
-**Affects:** Operators
-
-**Operators:** `AUTH_SESSION_SECRET` and `EPDS_CALLBACK_SECRET` are now mandatory. Previously, a deployment that omitted them would boot successfully using hardcoded placeholders such as `dev-session-secret-change-me`, leaving session cookies and the signed `/oauth/epds-callback` redirect verifiable by anyone who has read the source. The auth service and PDS Core now throw at startup with a message naming the missing variable and the `openssl rand -hex 32` command to generate one. Check that both variables are set in your deployment before upgrading, and note that `EPDS_CALLBACK_SECRET` must be identical across the two services.
-
-`AUTH_CSRF_SECRET` has been removed entirely. CSRF protection derives its tokens from `crypto.randomBytes` and never consumed the value, so the variable was inert; it is no longer read, no longer generated by `scripts/setup.sh`, and can be deleted from your environment.
diff --git a/.changeset/resend-log-message-context.md b/.changeset/resend-log-message-context.md
deleted file mode 100644
index 118462a3..00000000
--- a/.changeset/resend-log-message-context.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-Resend webhook logs now identify the email event type and provider in their message text.
-
-**Affects:** Operators
-
-**Operators:** Log messages use text such as `Received email event 'delivered' from Resend`, making events legible in log viewers that de-emphasize structured fields.
diff --git a/.changeset/send-a-new-code-label.md b/.changeset/send-a-new-code-label.md
deleted file mode 100644
index 739071d0..00000000
--- a/.changeset/send-a-new-code-label.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-The sign-in button that emails you another code now says "Send a new code" instead of "Resend code".
-
-**Affects:** End users
-
-**End users:** each request emails a genuinely new code and stops the previous one working, so "Resend" was misleading — it suggested the code already in your inbox was still the one to type. The confirmation after clicking now starts "Sent!" rather than "Resent!", and still tells you that earlier codes no longer work. The same wording appears on the recovery and account sign-in pages, and on the link offered beside a rejected or expired code.
diff --git a/.changeset/sign-in-button-readiness.md b/.changeset/sign-in-button-readiness.md
deleted file mode 100644
index d3a7cc2d..00000000
--- a/.changeset/sign-in-button-readiness.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-The email sign-in button now waits until the page is ready before accepting clicks.
-
-**Affects:** End users
-
-**End users:** tapping Continue before the sign-in page finished loading could start a broken sign-in attempt that silently did nothing. The button is now disabled until the page is ready. Browsers with JavaScript disabled see a message explaining that JavaScript is required to sign in with email, instead of a button that never responds.
diff --git a/.changeset/sign-in-errors-at-point-of-failure.md b/.changeset/sign-in-errors-at-point-of-failure.md
deleted file mode 100644
index ab0c5174..00000000
--- a/.changeset/sign-in-errors-at-point-of-failure.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'ePDS': patch
----
-
-Sign-in error messages now appear next to the field that caused them, instead of at the top of the page.
-
-**Affects:** End users
-
-**End users:** a rejected sign-in code used to report the problem above the page heading, several elements away from the boxes you had just typed into — easy to miss, and it left the **Verify** button looking like the thing to press again. The message now sits directly between the code boxes and **Verify**, and a failed email submission likewise reads under the email field rather than above it.
-
-A rejected code now also carries a **Send a new code** link beside the message. The standalone button below the form was easy to overlook, and "that code didn't work" often means there is no usable code left at all rather than that you mistyped it — after too many wrong attempts, after signing in from another tab, or once an old code has aged out. Retyping cannot recover any of those. The boxes still clear and refocus, so retyping remains one keystroke away when that is what you need.
diff --git a/.changeset/suggest-mistyped-email-domains.md b/.changeset/suggest-mistyped-email-domains.md
deleted file mode 100644
index cc872278..00000000
--- a/.changeset/suggest-mistyped-email-domains.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': minor
----
-
-Catch likely email-domain typos before sending a sign-in code.
-
-**Affects:** End users
-
-**End users:** addresses one edit away from Gmail, Hotmail, Outlook, Yahoo, or iCloud now show a “Did you mean…?” prompt as soon as they are typed. Accepting replaces the address in the email field; dismissing keeps the original. The prompt never changes or blocks an address silently.
diff --git a/.changeset/unify-link-affordances.md b/.changeset/unify-link-affordances.md
deleted file mode 100644
index b9966d3a..00000000
--- a/.changeset/unify-link-affordances.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'ePDS': patch
----
-
-Clickable text on the sign-in screens now looks the same wherever it appears.
-
-**Affects:** End users, Client app developers
-
-**End users:** the same action no longer renders differently depending on where it sits. "Resend code", "Use different email" and "Recover with backup email" are now all plain text that darkens when you point at it, while actions embedded in a sentence — "Send a new code" in an error message, and the Terms of Use and Privacy Policy links — stay underlined so they remain visible against the text around them. Pointing at an underlined action no longer makes its underline vanish, and every one of these actions now shows a focus outline when reached with the keyboard.
-
-**Client app developers:** `--muted-foreground` now also sets the colour of the `.btn-secondary` actions on the sign-in page, which previously hardcoded `#6b6b6b`. If you override that custom property in your `branding.css`, it will now recolour "Resend code" and "Use different email" alongside "Recover with backup email" and the other muted text. Choose a value that clears 4.5:1 contrast against your card background. `--recovery-link-display` is unchanged.
diff --git a/.changeset/use-different-email-clears-form.md b/.changeset/use-different-email-clears-form.md
deleted file mode 100644
index 3fa6e176..00000000
--- a/.changeset/use-different-email-clears-form.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'ePDS': patch
----
-
-"Use different email" on the sign-in code page now clears the email field.
-
-**Affects:** End users
-
-**End users:** clicking **Use different email** on the code-entry page used to take you back to the email form with your previous address still filled in — exactly the opposite of what the button suggests, and forcing you to clear the field manually before you could type a new address. The form now starts empty, with the cursor in the field, so you can just type the new address and continue.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 34fe1351..dbc7c04f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,179 @@
# ePDS
+## 0.8.0
+
+### Who should read this release
+
+- **End users:**
+ - [Signing in and managing your account now run on the latest AT Protocol server, which requires operators to upgrade to Node.js 22.19 or newer.](#v0.8.0-signing-in-and-managing-your-account-now-run-on-the-latest)
+ - [Catch likely email-domain typos before sending a sign-in code.](#v0.8.0-catch-likely-email-domain-typos-before-sending-a-sign-in)
+ - [Sign-in, recovery and handle-choice feedback is now announced automatically by screen readers.](#v0.8.0-sign-in-recovery-and-handle-choice-feedback-is-now)
+ - [Sign-in code boxes now ignore stray characters pasted or typed alongside your code.](#v0.8.0-sign-in-code-boxes-now-ignore-stray-characters-pasted-or)
+ - [Asking for a new sign-in code now clears the boxes and tells you the old code has stopped working.](#v0.8.0-asking-for-a-new-sign-in-code-now-clears-the-boxes-and)
+ - [Sign-in and recovery email fields now work more reliably with browser autofill and password managers.](#v0.8.0-sign-in-and-recovery-email-fields-now-work-more-reliably)
+ - [Sign-in pages no longer leak a malformed client_id as the displayed app name.](#v0.8.0-sign-in-pages-no-longer-leak-a-malformed-client-id-as-the)
+ - [Handle-picker error wording is now accurate when the handle is reserved.](#v0.8.0-handle-picker-error-wording-is-now-accurate-when-the-handle)
+ - [Sign-in errors are written in plain English instead of showing the raw failure code.](#v0.8.0-sign-in-errors-are-written-in-plain-english-instead-of)
+ - [Sign-in buttons now show a visible keyboard focus ring.](#v0.8.0-sign-in-buttons-now-show-a-visible-keyboard-focus-ring)
+ - [Small grey text on the sign-in page is darker and easier to read.](#v0.8.0-small-grey-text-on-the-sign-in-page-is-darker-and-easier-to)
+ - [Apps you sign in to are now told that your email address is confirmed.](#v0.8.0-apps-you-sign-in-to-are-now-told-that-your-email-address-is)
+ - [The sign-in button that emails you another code now says "Send a new code" instead of "Resend code".](#v0.8.0-the-sign-in-button-that-emails-you-another-code-now-says)
+ - [The email sign-in button now waits until the page is ready before accepting clicks.](#v0.8.0-the-email-sign-in-button-now-waits-until-the-page-is-ready)
+ - [Sign-in error messages now appear next to the field that caused them, instead of at the top of the page.](#v0.8.0-sign-in-error-messages-now-appear-next-to-the-field-that)
+ - [Clickable text on the sign-in screens now looks the same wherever it appears.](#v0.8.0-clickable-text-on-the-sign-in-screens-now-looks-the-same)
+ - ["Use different email" on the sign-in code page now clears the email field.](#v0.8.0-use-different-email-on-the-sign-in-code-page-now-clears-the)
+- **Client app developers:**
+ - [The approval and account-chooser screens now honour the handle mode your app asked for, instead of falling back to the server default.](#v0.8.0-the-approval-and-account-chooser-screens-now-honour-the)
+ - [Small grey text on the sign-in page is darker and easier to read.](#v0.8.0-small-grey-text-on-the-sign-in-page-is-darker-and-easier-to)
+ - [Apps you sign in to are now told that your email address is confirmed.](#v0.8.0-apps-you-sign-in-to-are-now-told-that-your-email-address-is)
+ - [Clickable text on the sign-in screens now looks the same wherever it appears.](#v0.8.0-clickable-text-on-the-sign-in-screens-now-looks-the-same)
+- **Operators:**
+ - [Signing in and managing your account now run on the latest AT Protocol server, which requires operators to upgrade to Node.js 22.19 or newer.](#v0.8.0-signing-in-and-managing-your-account-now-run-on-the-latest)
+ - [Apps you sign in to are now told that your email address is confirmed.](#v0.8.0-apps-you-sign-in-to-are-now-told-that-your-email-address-is)
+ - [Both services now refuse to start unless their secret environment variables are set, instead of silently falling back to well-known development defaults.](#v0.8.0-both-services-now-refuse-to-start-unless-their-secret)
+ - [Resend webhook logs now identify the email event type and provider in their message text.](#v0.8.0-resend-webhook-logs-now-identify-the-email-event-type-and)
+
+### Minor Changes
+
+- [#233](https://github.com/hypercerts-org/ePDS/pull/233) [`ebd214e`](https://github.com/hypercerts-org/ePDS/commit/ebd214effb0072b5280f2902083f8d0ab1366e4d) Thanks [@aspiers](https://github.com/aspiers)! - Signing in and managing your account now run on the latest AT Protocol server, which requires operators to upgrade to Node.js 22.19 or newer.
+
+ **Affects:** End users, Operators
+
+ **End users:** the account and sign-in screens now come from a newer version of the underlying AT Protocol software, which adds changing your handle and deactivating or deleting your account through the account-management interface. The passwordless email-code sign-in and email verification you already use through ePDS are unchanged.
+
+ **Operators:**
+ - ePDS now requires **Node.js 22.19.0 or newer** (previously 20). Deployments pinned to Node 20 must upgrade; the Docker images already use `node:22-alpine`. `engines.node` is now `>=22.19.0` (the floor is set by a transitive `undici@8` runtime dependency, which fails on Node 22.0–22.18).
+ - The bundled AT Protocol packages moved to their latest releases: `@atproto/pds` 0.5.23, `@atproto/oauth-provider` 0.21.1, `@atproto/oauth-provider-ui` 0.8.9.
+ - No configuration changes are required, but the native `better-sqlite3` module must be built for the Node 22 ABI — a clean `pnpm install` on Node 22 handles this; an in-place Node version switch needs `pnpm rebuild better-sqlite3`.
+
+- [#231](https://github.com/hypercerts-org/ePDS/pull/231) [`fe3b15c`](https://github.com/hypercerts-org/ePDS/commit/fe3b15c17b85319e7d7c73b036bafe90ade8c06b) Thanks [@aspiers](https://github.com/aspiers)! - Catch likely email-domain typos before sending a sign-in code.
+
+ **Affects:** End users
+
+ **End users:** addresses one edit away from Gmail, Hotmail, Outlook, Yahoo, or iCloud now show a “Did you mean…?” prompt as soon as they are typed. Accepting replaces the address in the email field; dismissing keeps the original. The prompt never changes or blocks an address silently.
+
+### Patch Changes
+
+- [#224](https://github.com/hypercerts-org/ePDS/pull/224) [`610b5e6`](https://github.com/hypercerts-org/ePDS/commit/610b5e6318e363f9987adaf7846393e66cd988a0) Thanks [@aspiers](https://github.com/aspiers)! - Sign-in, recovery and handle-choice feedback is now announced automatically by screen readers.
+
+ **Affects:** End users
+
+ **End users:** authentication errors now use standard alert and live-region semantics across sign-in, recovery, handle selection, and the demo client, so screen-reader users hear failures without having to search the page for changed text. Repeated failures — such as entering a second incorrect one-time code — announce each time rather than falling silent, and handle availability ("Available!" / "Already taken.") is now announced as you type.
+
+- [#223](https://github.com/hypercerts-org/ePDS/pull/223) [`0637fa7`](https://github.com/hypercerts-org/ePDS/commit/0637fa77e71fc425647a67ced188372311500ef3) Thanks [@aspiers](https://github.com/aspiers)! - Sign-in code boxes now ignore stray characters pasted or typed alongside your code.
+
+ **Affects:** End users
+
+ **End users:** every screen that asks for a sign-in code — the main sign-in screen, Account Settings sign-in, and account recovery — now drops characters your code can't contain, so a code pasted with surrounding quotes, brackets, or a trailing full stop is accepted instead of rejected. The main sign-in screen previously ignored only spaces and line breaks. Where the operator has turned on codes that mix letters and numbers, typing them in lowercase now works too.
+
+- [#220](https://github.com/hypercerts-org/ePDS/pull/220) [`68b2104`](https://github.com/hypercerts-org/ePDS/commit/68b21040c6e823b038c2e30410288a225c680f6c) Thanks [@aspiers](https://github.com/aspiers)! - Asking for a new sign-in code now clears the boxes and tells you the old code has stopped working.
+
+ **Affects:** End users
+
+ **End users:** the boxes reset on **Send a new code**, so a half-typed old code no longer has to be deleted by hand before you can type the new one. The confirmation also warns that only the newest code will be accepted, and points at the spam folder — the two things most likely to be going wrong for anyone who got that far.
+
+- [#222](https://github.com/hypercerts-org/ePDS/pull/222) [`9e12e30`](https://github.com/hypercerts-org/ePDS/commit/9e12e30edf8a9259fec8d261f8587d90d24a2e37) Thanks [@aspiers](https://github.com/aspiers)! - Sign-in and recovery email fields now work more reliably with browser autofill and password managers.
+
+ **Affects:** End users
+
+ **End users:** email fields now identify themselves to browsers as email addresses, while the same sign-in field identifies itself as a username after switching to handle entry. This helps browsers and password managers offer the right saved value in each mode.
+
+- [#229](https://github.com/hypercerts-org/ePDS/pull/229) [`6f4fa2d`](https://github.com/hypercerts-org/ePDS/commit/6f4fa2dd43b32720154d0b8cc80e272f32568b7b) Thanks [@aspiers](https://github.com/aspiers)! - Sign-in pages no longer leak a malformed client_id as the displayed app name.
+
+ **Affects:** End users
+
+ **End users:** if you arrived at a sign-in page with a broken `client_id` parameter on the URL (e.g. from a misconfigured app or a tampered link), the page would display that raw broken value as the app's name, e.g. "Sign in to not-a-url" or "Sign in to my-local-app". The page now falls back to "an application" in that case, so a malformed value in the URL doesn't leak into the visible page text or browser tab title.
+
+- [#243](https://github.com/hypercerts-org/ePDS/pull/243) [`534647d`](https://github.com/hypercerts-org/ePDS/commit/534647da664abe07cf543759f94b8286dc7b81a6) Thanks [@aspiers](https://github.com/aspiers)! - The approval and account-chooser screens now honour the handle mode your app asked for, instead of falling back to the server default.
+
+ **Affects:** Client app developers
+
+ **Client app developers:** no action needed — `epds_handle_mode` is resolved exactly as before (the `/oauth/authorize` query parameter, then your OAuth client metadata, then the server default). Previously the resolved mode was lost on the way to the approval step, so a flow that asked for a chosen handle could still show a generated one there. The mode is now carried through `/oauth/epds-callback` and applied on the approval and chooser screens. Values that are not one of the recognised modes are ignored rather than forwarded.
+
+- [#227](https://github.com/hypercerts-org/ePDS/pull/227) [`68b0760`](https://github.com/hypercerts-org/ePDS/commit/68b0760afa923ca2a3f3b4997137eca1a5ca0b77) Thanks [@aspiers](https://github.com/aspiers)! - Handle-picker error wording is now accurate when the handle is reserved.
+
+ **Affects:** End users
+
+ **End users:** if you tried to claim a handle that was on the reserved list (admin, www, …) the picker said "That handle was just taken — please choose another." That's misleading — it wasn't just taken, it's permanently unavailable. The wording is now "That handle is not available — please choose another." which doesn't imply you can wait it out.
+
+- [#237](https://github.com/hypercerts-org/ePDS/pull/237) [`ddde1b2`](https://github.com/hypercerts-org/ePDS/commit/ddde1b22f89b45dfd90a38a6f34915dce9b013d3) Thanks [@aspiers](https://github.com/aspiers)! - Sign-in errors are written in plain English instead of showing the raw failure code.
+
+ **Affects:** End users
+
+ **End users:** a rejected code used to report "Invalid OTP", an unexplained acronym that ran straight into the recovery link beside it — "Invalid OTP Send a new code". It now reads "That code didn't work." followed by the link, and the two other common failures are similarly rewritten: an aged-out code says "That code has expired.", and one rejected after repeated wrong attempts says "Too many tries — that code is no longer usable.". Any failure outside those three is still shown as-is rather than hidden behind a generic apology, so an unexpected problem can still be reported accurately.
+
+- [#225](https://github.com/hypercerts-org/ePDS/pull/225) [`23dcd0f`](https://github.com/hypercerts-org/ePDS/commit/23dcd0f0b88ca5e87b4b465523be19d14864ea04) Thanks [@aspiers](https://github.com/aspiers)! - Sign-in buttons now show a visible keyboard focus ring.
+
+ **Affects:** End users
+
+ **End users:** keyboard users tabbing through the sign-in pages couldn't tell where focus was — the buttons styled out the browser default focus ring without replacing it. Verify, Resend, Use different email, Recover, Send recovery code, Continue with email, Update handle, etc. all now show a clear outline when focused via the keyboard, while still hiding the ring on a mouse click.
+
+- [#232](https://github.com/hypercerts-org/ePDS/pull/232) [`87ef832`](https://github.com/hypercerts-org/ePDS/commit/87ef8320a169e970474747ca0e9d65b26a3984da) Thanks [@aspiers](https://github.com/aspiers)! - Small grey text on the sign-in page is darker and easier to read.
+
+ **Affects:** End users, Client app developers
+
+ **End users:** the 13px grey text — the terms line, the "Recover with backup email" link, the "or continue with" / "or use email" separators, and the "powered by Certified" footer — was too light to meet the WCAG AA contrast minimum against either the card or the page background behind it. It is now dark enough to pass on both.
+
+ **Client app developers:** the `--muted-foreground` custom property on the sign-in page changes from `[#999](https://github.com/hypercerts-org/ePDS/issues/999)` to `[#666](https://github.com/hypercerts-org/ePDS/issues/666)`. If your `branding.css` sets it, check your value clears 4.5:1 against both `#F8F8F8` (the card) and `#E8E8E8` (the page) — the old default reached only 2.68:1 and 2.33:1 respectively, so a value chosen to sit alongside it may be equally low. The separator text between the social and email sign-in options now follows `--muted-foreground` too, where it previously ignored your override and stayed grey.
+
+- [#234](https://github.com/hypercerts-org/ePDS/pull/234) [`3924855`](https://github.com/hypercerts-org/ePDS/commit/39248551ccaa1097cdfd569bbe4d6617ad0fed7f) Thanks [@aspiers](https://github.com/aspiers)! - Apps you sign in to are now told that your email address is confirmed.
+
+ **Affects:** End users, Client app developers, Operators
+
+ **End users:** Apps no longer ask you to verify an address you have already confirmed with an emailed code.
+
+ **Client app developers:** The `email_verified` claim is now `true` for accounts that signed in through the emailed-code flow, instead of always `false`.
+
+ **Operators:** Deploy the auth service and the PDS together — the signed handover between them carries a new required field, and a mixed pair rejects sign-in until both are updated. An older auth service is rejected with an explicit `Missing or invalid email_verified parameter` rather than a generic signature error, so a mixed-version rollout is recognisable in the logs. Accounts predating this release are repaired the next time their owner signs in **through the emailed-code flow**; a sign-in that does not prove control of the address leaves them unconfirmed. To fix the rest, see "Backfilling Email Confirmation" in `docs/deployment.md`.
+
+- [#41](https://github.com/hypercerts-org/ePDS/pull/41) [`d4256ea`](https://github.com/hypercerts-org/ePDS/commit/d4256ea03a0982386662c3a9adb1ff7d9f347a38) Thanks [@aspiers](https://github.com/aspiers)! - Both services now refuse to start unless their secret environment variables are set, instead of silently falling back to well-known development defaults.
+
+ **Affects:** Operators
+
+ **Operators:** `AUTH_SESSION_SECRET` and `EPDS_CALLBACK_SECRET` are now mandatory. Previously, a deployment that omitted them would boot successfully using hardcoded placeholders such as `dev-session-secret-change-me`, leaving session cookies and the signed `/oauth/epds-callback` redirect verifiable by anyone who has read the source. The auth service and PDS Core now throw at startup with a message naming the missing variable and the `openssl rand -hex 32` command to generate one. Check that both variables are set in your deployment before upgrading, and note that `EPDS_CALLBACK_SECRET` must be identical across the two services.
+
+ `AUTH_CSRF_SECRET` has been removed entirely. CSRF protection derives its tokens from `crypto.randomBytes` and never consumed the value, so the variable was inert; it is no longer read, no longer generated by `scripts/setup.sh`, and can be deleted from your environment.
+
+- [#209](https://github.com/hypercerts-org/ePDS/pull/209) [`0af6d7f`](https://github.com/hypercerts-org/ePDS/commit/0af6d7f690f848517269e181d18a9a6229828e79) Thanks [@aspiers](https://github.com/aspiers)! - Resend webhook logs now identify the email event type and provider in their message text.
+
+ **Affects:** Operators
+
+ **Operators:** Log messages use text such as `Received email event 'delivered' from Resend`, making events legible in log viewers that de-emphasize structured fields.
+
+- [#236](https://github.com/hypercerts-org/ePDS/pull/236) [`e238c38`](https://github.com/hypercerts-org/ePDS/commit/e238c3830566b5e2960d4bd5bf40e474ac6db0e4) Thanks [@aspiers](https://github.com/aspiers)! - The sign-in button that emails you another code now says "Send a new code" instead of "Resend code".
+
+ **Affects:** End users
+
+ **End users:** each request emails a genuinely new code and stops the previous one working, so "Resend" was misleading — it suggested the code already in your inbox was still the one to type. The confirmation after clicking now starts "Sent!" rather than "Resent!", and still tells you that earlier codes no longer work. The same wording appears on the recovery and account sign-in pages, and on the link offered beside a rejected or expired code.
+
+- [#242](https://github.com/hypercerts-org/ePDS/pull/242) [`a0a819a`](https://github.com/hypercerts-org/ePDS/commit/a0a819af2d39288351ea17f4f4f9de62af026be8) Thanks [@aspiers](https://github.com/aspiers)! - The email sign-in button now waits until the page is ready before accepting clicks.
+
+ **Affects:** End users
+
+ **End users:** tapping Continue before the sign-in page finished loading could start a broken sign-in attempt that silently did nothing. The button is now disabled until the page is ready. Browsers with JavaScript disabled see a message explaining that JavaScript is required to sign in with email, instead of a button that never responds.
+
+- [#220](https://github.com/hypercerts-org/ePDS/pull/220) [`50c8620`](https://github.com/hypercerts-org/ePDS/commit/50c8620896d8c797ab629467e09701d983426468) Thanks [@aspiers](https://github.com/aspiers)! - Sign-in error messages now appear next to the field that caused them, instead of at the top of the page.
+
+ **Affects:** End users
+
+ **End users:** a rejected sign-in code used to report the problem above the page heading, several elements away from the boxes you had just typed into — easy to miss, and it left the **Verify** button looking like the thing to press again. The message now sits directly between the code boxes and **Verify**, and a failed email submission likewise reads under the email field rather than above it.
+
+ A rejected code now also carries a **Send a new code** link beside the message. The standalone button below the form was easy to overlook, and "that code didn't work" often means there is no usable code left at all rather than that you mistyped it — after too many wrong attempts, after signing in from another tab, or once an old code has aged out. Retyping cannot recover any of those. The boxes still clear and refocus, so retyping remains one keystroke away when that is what you need.
+
+- [#237](https://github.com/hypercerts-org/ePDS/pull/237) [`43baeb5`](https://github.com/hypercerts-org/ePDS/commit/43baeb54c60402ab97050b5e4af5b143b8901640) Thanks [@aspiers](https://github.com/aspiers)! - Clickable text on the sign-in screens now looks the same wherever it appears.
+
+ **Affects:** End users, Client app developers
+
+ **End users:** the same action no longer renders differently depending on where it sits. "Resend code", "Use different email" and "Recover with backup email" are now all plain text that darkens when you point at it, while actions embedded in a sentence — "Send a new code" in an error message, and the Terms of Use and Privacy Policy links — stay underlined so they remain visible against the text around them. Pointing at an underlined action no longer makes its underline vanish, and every one of these actions now shows a focus outline when reached with the keyboard.
+
+ **Client app developers:** `--muted-foreground` now also sets the colour of the `.btn-secondary` actions on the sign-in page, which previously hardcoded `#6b6b6b`. If you override that custom property in your `branding.css`, it will now recolour "Resend code" and "Use different email" alongside "Recover with backup email" and the other muted text. Choose a value that clears 4.5:1 contrast against your card background. `--recovery-link-display` is unchanged.
+
+- [#215](https://github.com/hypercerts-org/ePDS/pull/215) [`1a90062`](https://github.com/hypercerts-org/ePDS/commit/1a900621936941066372228b423d26a6808cb21b) Thanks [@aspiers](https://github.com/aspiers)! - "Use different email" on the sign-in code page now clears the email field.
+
+ **Affects:** End users
+
+ **End users:** clicking **Use different email** on the code-entry page used to take you back to the email form with your previous address still filled in — exactly the opposite of what the button suggests, and forcing you to clear the field manually before you could type a new address. The form now starts empty, with the cursor in the field, so you can just type the new address and continue.
+
## 0.7.0
### Who should read this release
diff --git a/package.json b/package.json
index 6a1a3fec..0e5e638f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ePDS",
- "version": "0.7.0",
+ "version": "0.8.0",
"private": true,
"description": "ePDS — extended Personal Data Server for AT Protocol with passwordless OTP authentication",
"license": "MIT",