# Customer authentication

## Local test override — 2026-09-29

At the user's request, local new-account signup now skips SMS/OTP and signs in using the supplied mobile number and password. It uses an isolated, expiring create-only adapter; production signup and administrator MFA are unchanged. Test accounts are explicitly marked in trusted metadata and admin notifications. The local override expires on 2026-10-06T15:57:24.350Z. See `REGISTRATION_TEST_MODE.md` for configuration, successful live signup/login and notification checks, and deactivation. New test accounts use mobile/password login; the supplied email is contact metadata until separately verified.

The public marketplace login and the administration login are separate. Customer signup never writes `marketplace_private.members` and never confers an administrative role. The admin application continues to require its independent session and verified TOTP.

## Implemented

- Password login accepts normalized email, E.164 phone numbers (including Iranian 09… input), or a private username.
- Phone signup collects first name, last name, contact email, phone, optional username, and a password of at least 12 characters. Signup requires an actual SMS verification step. The contact email is attached after successful phone verification and must be confirmed separately before email login is available.
- SMS login uses `create_user: false`. OTP verification accepts Persian and Arabic numerals. Resend has a 60-second interface cooldown; Supabase remains responsible for enforcing server rate limits.
- Remember-me uses HttpOnly cookies for access and rotating refresh credentials. Unchecked uses browser-session cookies; checked persists refresh credentials for up to 30 days, subject to Supabase session policy. Browsers that restore sessions may also restore session cookies. Passwords and OTPs are never stored in localStorage.
- Password recovery uses a phone OTP or an email PKCE recovery link. Email links must be opened in the requesting browser. Recovery credentials are restricted to the password-reset endpoint and cleared after use; reset requests revoke Supabase refresh sessions. Already issued access JWTs expire according to Supabase policy.
- All public auth mutations require same-origin JSON, bounded input and generic failure responses. Public responses contain no access/refresh tokens.

## Current project status (2026-09-07)

Project `wlsxzyeernwtsetzyltq` has email and Phone auth enabled, with email and phone confirmation required. Phone uses the deployed MeliPayamak Send SMS Hook, six-digit OTPs and the existing 60-second expiry. The live hook rejected an unsigned request with HTTP 401 and accepted one authorized, signed SMS delivery test with HTTP 200. Handset delivery and the complete Auth signup/login flow remain unverified. No customer or administrator account was created by this work, and no test email was sent.

Migration `202609070002_customer_auth.sql` has been installed. Live checks confirm customer profile RLS is on, anon/authenticated roles cannot read the table or execute the contact resolver, authenticated users can call only their own username function, and admin membership remains empty. Usernames are reserved only once the phone is verified. If a requested username is already taken, a unique generated username is assigned and shown in the account panel.

The isolated `username-lookup` Edge Function is prepared and tested locally; **it has not been deployed**. The public marketplace currently has neither `AUTH_DIRECTORY_URL` nor `AUTH_DIRECTORY_TOKEN`. Username login therefore reports that it is unavailable and offers email/phone instead. The public marketplace needs no Supabase secret/service-role key.

## Activation

1. Phone and the MeliPayamak Send SMS Hook are enabled (see `MELIPAYAMAK_SETUP.md`). Credentials and the hook signing secret are stored in Supabase Secrets. Keep phone confirmation required and OTP length at six digits. Complete handset delivery and signup/login verification with a consenting test account.
2. Configure production email delivery. In Auth URL Configuration, allow the exact `${MARKETPLACE_ORIGIN}/auth/recovery` URL, including `http://localhost:3000/auth/recovery` for local development. Set Site URL to the correct marketplace origin. These URL/SMTP settings have not been changed or verified in this task. Complete an actual recovery and email-confirmation test before launch.
3. Deploy `supabase/functions/username-lookup` to this same project. Set a cryptographically random `AUTH_DIRECTORY_TOKEN` of at least 32 characters in its secrets, and the same value in the marketplace server environment. Set `AUTH_DIRECTORY_URL` to `https://wlsxzyeernwtsetzyltq.supabase.co/functions/v1/username-lookup`. The function has `verify_jwt=false` because it validates this separate server-to-server credential itself, before reading data. It uses Supabase's built-in service-role credential only inside the isolated function. Never put a Supabase administrative key in the marketplace environment or browser.
4. Apply edge abuse/rate limits on password, directory lookup, signup, recovery and OTP endpoints before public launch. Supabase's own auth rate limits remain active; CAPTCHA is not integrated in this version. Validate the complete browser and SMS/email flows with real provider configuration. Local tests mock hosted Auth and do not verify SMS delivery, hosted refresh rotation or email delivery.

Official references: [phone login](https://supabase.com/docs/guides/auth/phone-login), [PKCE password recovery](https://supabase.com/docs/reference/javascript/auth-resetpasswordforemail), [Edge Function authentication](https://supabase.com/docs/guides/functions/auth), [function secrets](https://supabase.com/docs/guides/functions/secrets).

The public Auth settings readiness check now uses `phone_autoconfirm`, as verified against the live endpoint and the official Supabase settings contract. It fails closed for missing/unknown values or automatic confirmation. All 31 tests and TypeScript checks pass.

## MeliPayamak update

The MeliPayamak SendOtp hook is deployed with provider credentials and its signing secret configured. The hook and Phone provider are enabled. One authorized SMS test was accepted by the provider; handset delivery and end-to-end Auth verification remain pending. The integer SendOtp code is normalized back to six digits on verification, preserving leading zeros without changing Supabase OTP validation. See `MELIPAYAMAK_SETUP.md`.

## Kavenegar migration (2026-09-08)

The Kavenegar Lookup hook is deployed and its unsigned-request rejection was verified live (401). The user stored the API key in Supabase Secrets; it was not read or copied into source. The `planklogin` template (ID 1526198) was submitted in technical-test mode and is awaiting provider review. The free seven-day Advanced trial is active; no paid renewal was purchased. The user currently needs only a login OTP template. Website-template onboarding is deferred.

Both deployed SMS adapters now require `SMS_ALLOWED_PHONE=+989121888745` and reject all other recipients before contacting a provider. Removing the setting disables sending rather than removing the restriction. The active Auth Hook remains MeliPayamak until the Kavenegar template is approved. No Kavenegar SMS was sent and no Auth end-to-end test was completed. There is no automatic retry/fallback. All 39 local tests, TypeScript checks and the production build pass. See `KAVENEGAR_SETUP.md` for setup and remaining activation steps.


## Login device notices and retained network audit (2026-09-30)

A newly created customer login session produces one retained `session/login` activity. Password and SMS sign-in and the local direct-registration sign-in enrich that activity with device family, operating system and browser. The same transaction creates a localized customer notification linking to `/account/notifications`. The notification contains device/software and sign-in time, never IP, raw User-Agent or upstream network metadata. Ordinary session reads and refresh-token rotation do not create extra notices or login activities.

`202609300004_customer_login_context.sql` captures `auth.sessions INSERT`, replacing only the old `auth.users.last_sign_in_at` login branch; profile/password audit remains. Initial provider evidence and trusted app enrichment are retained separately, with no cascading foreign key to expiring auth sessions. The existing activity formatter joins enrichment at read time without editing original history. Direct table access is denied. The ingestion RPC accepts only the current active customer's own real, fresh post-rollout session from the signed JWT, with no user/session selector and at most one enrichment/notice.

`LOGIN_AUDIT_INGEST_TOKEN` is a random server-only integration credential (minimum 32 characters); only its SHA-256 hash is provisioned in the private database configuration. This is not a Supabase secret/service-role key and grants no read or admin access. Keep the actual token in ignored local environment or a deployment secret, never a public-prefixed variable, browser bundle, SQL editor or migration. `LOGIN_CONTEXT_PLATFORM=local` enables development-only signed metadata from the real Node socket; the middleware replaces untrusted copies in both Node header representations before the Cloudflare development bridge. `LOGIN_CONTEXT_PLATFORM=cloudflare` enables production IP/geography only from runtime-owned `Request.cf` and its edge IP header. Production hosting needs these server settings provisioned before activation; other platforms report unavailable until a trusted adapter is added. No external IP lookup or device GPS is called.

Locations are approximate network locations, not exact user positions. Private/loopback addresses show local execution; unknown geography stays unknown. Modern frozen Windows/macOS UA versions are not presented as an actual OS release. Browser/device identification uses client-advertised metadata and is descriptive, never authorization evidence. Provider-side egress IP is retained as upstream evidence and is never presented as the customer's IP.

Enrichment is bounded to five seconds and cannot block an otherwise successful sign-in. Failure logs a generic warning without tokens or network data; the auth-session baseline remains recorded. No background retry, fabricated old-entry backfill or notification on an existing session is performed.

Deployment: installed on approved Supabase project `wlsxzyeernwtsetzyltq`; SQL returned `Success. No rows returned`. Eight read-only checks verified the session trigger, removed duplicate branch, protected tables, denied anonymous/direct access, configured server key and connected activity/session binding. Migration SHA-256: `6b0572db2d2a0899e52b9f833725d5842ae0129fcda50b19790868bb37f4e2e5`. A runtime-only local diagnostic verified signed `127.0.0.1` and local location, then was removed. Tests cover spoofing, IPv4/IPv6, scope/expiry/replay, refresh deduplication, notification atomicity, retention and existing auth behavior. No real customer was signed in/out or given synthetic history during verification; the next new sign-in exercises the full live flow.
