Appearance
Users can sign in with a password, recover through a verified mailbox and change their password without changing their user ID or holdings. Password replacement revokes all previous roots, access credentials and refresh sessions atomically. The existing email-code route and old-client compatibility remain available.
Contract
POST /auth/password/loginaccepts{email,password}and returns the same{userId,token,email,action}shape as email verification. Unknown, incorrect and blocked accounts return generic 401. Ten failures within fifteen minutes lock that normalized address for fifteen minutes through PostgreSQL.POST /me/passwordaccepts authenticated{password,currentPassword?}. A correct current password or an unrevoked email proof issued within the last ten minutes is required. A used/revoked proof cannot be replayed. It returns a new root for the same owner after revoking every previous credential/session.- Email verification accepts
purpose:"reset". Only after mailbox proof, an unknown address returns 404account_not_foundwithout creating an empty account. Public code requests keep the same response for all addresses. - Passwords contain 15–128 Unicode code points, with whitespace preserved and no composition rules. Storage uses salted scrypt hashes (N=131072, r=8, p=1, 32-byte output); at most two password requests run per API process.
- Clients exchange roots through
/auth/sessionfor 15-minute access and rotating refresh credentials with rolling 90-day inactivity expiry. Legacy compatibility roots still have NULL expiry; this is not a claim that all old credentials now expire. Logout uses/auth/session/revokewith either proof. - Both logout proofs lock the owner before token/session rows, consistently with password changes, refresh and erasure. Actual PostgreSQL tests reproduce and guard the previous lock-order inversion.
- Attempt digests are removed on success or erasure. Startup/hourly cleanup removes up to 1000 unlocked entries older than 24 hours per pass and logs the count or failure; a large backlog can require subsequent passes. Passwords cascade on user deletion. All authentication responses are private/no-store.
Migration and deployed evidence
0045_account_passwords.sql adds password hashes and address attempt budgets. 0040 already belongs to avatars; the obsolete local password migration under that number must not be applied. Migration 0045 does not backfill token expiry or rewrite existing users/holdings. It requires the existing 0043 session schema.
On 2026-10-19, merged staging commit 5bde22e790ae851e306af2b1e9ec95340994c654 was deployed as skns-api:public-accounts-5bde22e to the existing Snag VPS. Every source file was verified against the final Git archive before building. The rollout preserved all active Compose overrides and the complete runtime environment, including Brevo and pricing credentials. Only skns-api-1 was recreated; no other running container was removed or replaced.
Before migration, a 99,145,706-byte PostgreSQL custom-format backup was created privately on the VPS and its restore catalogue validated. Migration 0045 was applied twice in one transaction; counts and content fingerprints of users and holdings were identical before and after. /api/healthz and /api/readyz passed. The previous image and private rollback evidence remain in the release folder.
The actual route lifecycle passed on PostgreSQL 16.15 with separate connections; 38 existing collection mutation/erasure SQL tests also passed against an ephemeral PostgreSQL container. All six PR CI jobs passed, including Docker smoke and API end-to-end tests. Local API tests passed 1281 cases with nine optional skips, and ingest passed 553 cases.
Public HTTPS tests used uniquely named disposable accounts to verify anonymous claim, password login, renewal, account isolation, change, recovery, proof reuse denial, old-credential rejection, logout and deletion. Synthetic accounts were erased afterwards; existing users were not used for these checks. Mail delivery was tested separately with an explicitly authorized recipient, who confirmed arrival in the primary inbox. Public HTTPS recovery checks used synthetic code fixtures and do not substitute for that separate actual-mail evidence.
Physical iPhone execution remains a launch gate. See the paired mobile release.