The white-label guard only fired for hostnames hand-listed in
CUSTOMER_PRODUCTION_WHITE_LABEL_HOSTS. improveone.accounted.se was never added,
so when the .accounted.se domains were pinned to a feature-branch preview the
guard let it through: a customer-branded login page served from a build that
inlines the staging Supabase project, on the open internet, with no alert. The
26 August willem.accounted.se 503s were the same misrouting caught correctly,
because willem was on the list.
Inverts the model. Any customer-facing production hostname (not a *.vercel.app
preview, not localhost) must be served by the production Supabase project or
the guard trips. Adding a new white-label host no longer requires editing a
list in order to be protected.
Also closes two fail-open holes found alongside it. parseBackendHostname
returned null for an undefined NEXT_PUBLIC_SUPABASE_URL, so a missing project
read as "not staging" and fell through; it now trips the guard. And proxy.ts
gains an explicit env guard: today lib/supabase/middleware.ts asserts the URL
and key non-null and @supabase/ssr throws synchronously as the first statement
of updateSessionInner, which takes down every path including /login and
/robots.txt with an opaque crash rather than a deliberate 503.
Claude-Session: https://claude.ai/code/session_016ifKg6Ec67A39oxfGPU1yc
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Exclude auth/ and reset-password from the legacy-host redirect (#1092):
email links sent before the cutover carry a PKCE code or recovery
session whose cookies live on app.gnubok.se; forwarding them to the
new domain breaks password resets and signup confirmations clicked
after the flip. login/MFA stay redirected on purpose: a usable login
page on the legacy host would establish sessions there and loop.
Exclusion pattern extracted to lib/domains/legacy-redirect.ts with a
unit test pinning the behavior.
- Clean up ephemeral oauth state rows (incl. oauth_user_id) when the
SKV token exchange fails (#1090): identity data must not outlive the
flow; best-effort so cleanup failure never masks the user-facing error.
- Assert the stored user is still a member of the company before the
service-role storeTokens write (#1091): membership can be revoked
between /authorize and the callback, and RLS no longer backstops the
write. Checked before the exchange so the one-shot code is not burned.
Fixes#1090, fixes#1091, fixes#1092.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>