PR 1903 removes customer password authentication and password settings across the API and public widgets. Customer signup and sign-in now establish sessions through emailed, tenant-bound, single-use magic links. The change also makes link-request responses uniform, prevents inbound links from replacing valid stored sessions, preserves in-progress gift-card state through the email round trip, and drops the obsolete password and mailbox-proof columns in migration 0190.
fix/1885-magic-link-only-auth at code commit a7d3107de, pushed before this evidence source was added
Generated
Reviewer brief
Outcome and scope
Customer sign-in is email-only and the migration chain converges
The built account widget renders no password control and reaches a conditional check-inbox state after an email request. Focused API and database suites prove uniform request responses, token issuance and consumption, single-use enforcement, revocation, tenant scoping and email-change races. A fresh database migrated through 0190 and matched its generated snapshot. Independent correctness and security follow-up reviews found no remaining merge blocker.
What this report proves
The screenshot is a real browser rendering of the production widget bundle built from code commit a7d3107de, with the API response intercepted to avoid sending email. It proves the rendered email-only interaction and conditional copy, not live provider delivery. The database-backed API tests and fresh migration run establish the server and schema claims. This report is reviewer evidence, not approval or a substitute for CI.
Claim map
Evidence coverage
Each material changed behavior or state maps to direct evidence or an explicit gap.
Behavior or claim
Evidence
Status
Customer sign-in and account settings contain no password path
Built-widget screenshot, contract table, source diff and focused widget tests
Covered
Known and unknown magic-link requests return the same body and do not wait for provider acceptance
Focused API excerpt and a provider-pending regression test
Covered
An inbound magic link cannot silently replace a valid stored customer or impersonation session
Hydration-order excerpt and a browser-state regression test
Covered
Magic links remain tenant-bound, expire after 30 minutes and can be consumed only once
Migration 0190 removes both obsolete credential columns without forking migration 0189
Migration excerpt, fresh-chain convergence check and independent snapshot-lineage review
Covered
A signed-out cross-device browser confirms identity before adopting an unbound link
Not implemented in this change. The documented bearer-link tradeoff is tracked in issue #1902
Gap
Observed change
Review evidence
Evidence is grouped by the reviewer question each item addresses.
Account widget
Screenshot
Email request reaches a non-enumerating confirmation state
Production widget bundle rendered at 606 by 757 CSS pixels. The request used unknown@example.com against an intercepted successful response, matching the response contract for both known and unknown addresses.
The panel says a link arrives only if an account exists, exposes a 60-second resend countdown, and contains zero password inputs.After
Authentication contract
Table
Customer credential surface before and after
Clean cutover with no compatibility aliases or deprecated password routes.
Contract
Base
PR 1903
Sign-in methods
Password or emailed magic link
Emailed magic link only
Customer API routes
POST /widget/auth/login and POST /widget/auth/password present
Both routes removed
Account settings
Set, change and remove password card
No password setting
Session response
hasPassword and mailboxProven status fields
Credential status fields removed
Database
customers.password_hash and magic_link_tokens.self_requested
Both columns dropped by 0190
API request privacy
Code
Delivery state no longer reveals whether an address matched
Global delivery configuration is checked before the customer lookup. A known account prepares its token without waiting for provider acceptance, then returns the same okBody as an unknown account.
Uniform response path at code commit a7d3107de.After
Existing valid sessions take precedence over inbound links
Hydration validates impersonation and customer storage candidates first. Only when no candidate survives does it redeem the mlt fragment.
Session-fixation guard at code commit a7d3107de.After
packages/widgets/src/lib/auth.tsx:768-857
for (const candidate of candidates) {
const r = await api.rawRequest("/api/widget/auth/me", {
auth: false,
headers: { Authorization: `Bearer ${candidate.token}` },
}).catch(() => null);
if (!r) return;
if (r.ok) {
const data = await r.json();
setToken(candidate.token);
setCustomer(data.customer);
setImpersonatedBy(data.impersonatedBy ?? null);
return;
}
// Authoritatively rejected candidates are removed from their own store.
}
if (mlt) {
const customer = await verifyMagicLink(mlt);
if (customer) return;
}
Database migration
Code
0190 drops obsolete customer credential columns
The migration was regenerated after origin/main's 0189 booking migration. Its snapshot derives from 0189 and omits both customer-only credential fields.
ALTER TABLE "customers" DROP COLUMN "password_hash";--> statement-breakpoint
ALTER TABLE "magic_link_tokens" DROP COLUMN "self_requested";
Confidence
Validation
Pass
Fresh migration chain and schema convergence
A new local Postgres database migrated from 0000 through 0190. The post-migrate assertion reported 96 tables and 56 enums matching 0190_snapshot.json.
Pass
Focused API authentication and route coverage
95 tests passed across widget auth, public widget origin, customer sign-in links, safe customer projections, friend booking and audit route inventory.
Pass
Database-backed magic-link boundaries
42 tests passed across request, captured mailbox follow, successful verification, replay rejection, session revocation and concurrent email-change issuance.
Pass
Widget authentication behavior
84 tests passed across API auth, WidgetAuthProvider hydration, SignInPanel and GiftCardPurchase. BookingCalendar added 66 passing authentication-flow tests.
Pass
Tenant inventory simulation
18 tenant-isolation scenarios passed after recreating the simulation database from the proposed schema.
Pass
Affected-package typechecks and lint
API, widgets, simulation and docs typechecks exited 0. Biome checked all changed supported files with no findings, and git diff --check passed.
Pass
Independent correctness and security review
The correctness reviewer verified 0190 lineage after rebase and found no merge fallout. The security reviewer confirmed the request status oracle and valid-session overwrite findings are fixed, with the signed-out cross-device tradeoff recorded below.
Warn
Exact-head CI
Pending on the draft pull request. Local focused proof passed before evidence authoring.
Limits
Known issues, risks, and gaps
Destructive credential removal and the intentional cross-device bearer-link boundary
Password credentials are removed without compatibility Migration 0190 permanently drops customer password hashes. The API routes, response fields and widget controls are removed in the same release. This is intentional because no customers are using the pre-launch application.
Signed-out cross-device redemption remains bearer-link authentication A valid stored customer or impersonation session can no longer be replaced by an inbound link. A browser with no valid stored session still auto-redeems an unused same-brand link to preserve documented cross-device sign-in. Customers are warned not to share links. Explicit identity confirmation for unbound links is tracked in #1902.
Provider rejection is not shown as an account-specific error The request endpoint cannot expose provider acceptance without revealing whether the address matched. Its UI therefore says a link will arrive only if an account exists, while asynchronous provider failures are logged. Global delivery misconfiguration still returns the same 503 for known and unknown addresses.
The visual evidence is head-only The report captures the proposed email-only state but does not include a matched base screenshot of the removed password form. The base-to-head source diff and deleted password test suites establish the removal boundary.