# Multi-account login — enablement authorized

## Release boundary

The user authorized enabling login on 2026-09-11. The broker and ordinary mobile
builds now enable the new login flow by default. Explicit environment/build
values of false still disable it for rollback. The old Code entry stays OFF.

Code enablement is not proof of a live rollout. Deploy the backend, then
distribute a new App build. The directory is deliberately empty at the user's
request. Until a Super Admin adds an enabled company, sign-in returns 503 and
the App asks the user to contact their administrator. Existing server
`.env` values of `AGENT_COMPANY_LOGIN_ENABLED=false` must be changed explicitly
and the Laravel configuration cache refreshed. No customer values are invented
or committed as part of changing the default.

This design supersedes the first-login Code/link flow in
`company-entry-deployment.md`. Company Code/link support remains dormant source;
the current app entry flow uses email/password and a saved-account list.

## Contract

`POST /agent-api/company-auth/login`, JSON `{email, password}`, no bearer token.
Only the trusted broker receives this request from the app. Response:

```json
{"data":{"accounts":[{"email":"agent@example.com","name":"Agency","api_base_url":"https://agency.example.com","token":"opaque-bearer","expires_in":3600,"account_scope":"11111111-1111-4111-8111-111111111111"}],"partial":false}}
```

There is no public customer-list API. Each successful result identifies only a
backend that accepted the submitted credentials. An empty list means no matches
were obtained; `partial=true` means retrying may discover more. No failed company
names, URLs, counts or upstream response bodies are returned. Invalid input gets
422; rate limits get 429; disabled broker gets 404; no enabled company gets 503.
Successful credential responses are no-store.

The app holds its own catalog of successfully authenticated identities. It never
reuses a stored password to find new memberships; the user signs in again to
sync. Same email/different password needs a separate sign-in. Existing matches
are updated by origin + server UUID, and new matches are appended. A failed or
partial sync does not remove existing accounts or change their default.

## Configuration for rollout

Central server only:

```dotenv
AGENT_COMPANY_LOGIN_ENABLED=true
AGENT_COMPANY_CODE_ENTRY_ENABLED=false
```

Keep the old Code resolver/landing/association feature OFF, including when the
new login broker is later enabled. Otherwise known Code values could expose a
company without password verification.

On the central petaV3 deployment, open **Setting > App Companies**
(`/manage/settings/company-accounts?suite=other`) as a **Super Admin**. Click
**Add company**, enter its display name and HTTPS API origin, choose whether it
is enabled, and **Save companies**. Names and addresses can be edited; rows can
be disabled or removed. The page starts empty and no company is seeded.

The directory is stored as JSON in the existing `settings` table under
`agent_app.company_directory`; no migration or new table is needed. The broker
reads it on every login, so saved edits need no config-cache reset, backend
redeploy or App update. The legacy `AGENT_COMPANY_DIRECTORY_JSON` variable is
only for the dormant Code/link feature and is ignored by the login broker.
Only the Super Admin page receives the directory; it is not included in shared
page props or any public/App directory endpoint. Do not copy this setting to
customer deployments.

Disabling/removing a company stops new broker logins but does not revoke existing
sessions. Revoke those separately on the owning company's backend if needed.
Only verified operator-configured HTTPS DNS origins (port 443, no path, query,
fragment or embedded credentials) are accepted. Requests never follow redirects.
Do not configure arbitrary third-party hosts; each host receives the password.
The operator must verify ownership, DNS and certificate identity of every entry.
Each agency must have independent DB provisioning and signing/JWT keys.

The broker calls the existing `/agent-api/agent-auth/token` on each backend,
including the original PropertyLab deployment if configured. It never calls
itself recursively. Five requests run concurrently; each has a 2-second connect
and 5-second overall timeout, with no automatic login retries. Upstream 401/422
are credential refusals; outages, rate limits, redirects and malformed success
bodies mark the overall response partial. Duplicate origins are checked once.
The broker's 5/minute per-email and 60/minute global limits supplement each
backend's existing login limits.

Size HTTP/PHP worker capacity and proxy timeouts for five-request batches; the
app waits up to 60 seconds for the response. A growing directory requires load
validation before rollout (worst case roughly 5 seconds per batch of five).
Self-host calls need spare PHP workers on the original backend.

Passwords and tokens must not be captured by reverse-proxy/APM request or response
body logging. This implementation excludes the broker route from Inspector and
uses a dedicated Guzzle client rather than the instrumented Laravel HTTP facade.
It never logs upstream request exceptions or persists passwords/tokens centrally.
Keep production debug mode off and verify the deployment's external logging rules.

Ordinary mobile builds now enable company login without an extra define. Use
`--dart-define=COMPANY_ACCOUNTS_ENABLED=false` only for a rollback build.
Keep `PETAV3_API_BASE_URL=https://app.propertylab.com.my/` as the trusted broker
and original credential namespace; do not change it to a customer URL.

## Acceptance before distribution

1. Empty directory: signing in shows the not-configured message. Add an enabled
   company in Setting > App Companies and retry without rebuilding the App.
   Verify an ordinary admin cannot read or edit this page.
2. New install: Sign in -> email/password -> only accepted company/email rows;
   select a row to enter. No customer directory appears in network responses.
3. Log in with a second email; both identities remain. Re-enter the first email
   to add a newly configured agency. Different passwords only add matching accounts.
4. Reopen: one identity auto-enters; multiple show a list; a default auto-enters.
   Switch always returns to the list, even with one/default identity.
5. Stop an active recording before switching. Verify BLE disconnect and old
   runtime disposal complete before the next runtime opens. Test native-button
   recording during switching on Android and iOS.
6. Queue recordings under A, switch to B, verify A's background jobs still use A's
   token, URL and local owner. Test cloned UUIDs and two emails on the same agency.
7. Sign out A only: B remains. Sign out all: no automatic old-login resurrection.
   Local recordings remain for later recovery when the same identity signs in.
8. Upgrade an installed original-backend user: session and local recording UUIDs
   survive. Legacy email was not stored; the row initially says Existing account
   and obtains its email on the next successful sign-in/sync.
9. Test wrong credentials, partial outages, 429, missing/expired tokens, offline
   startup, secure-storage failure and an inaccessible default. No failed sync
   should clear unrelated logins or send uploads to a fallback company.

Local tests use fake backends; live independent deployments and signed physical
phones are still required for this acceptance. Verify actual server configuration and the installed build before declaring the feature live.

## Enablement validation — 2026-09-11

- Combined Agent API suite: 264 tests / 1,752 assertions passed, including
  Super Admin access, empty directory, URL validation and dynamic broker lookup.
- Client and SSR builds passed; targeted Pint, suite-link and migration-constant
  guards passed. The existing PHPUnit XML deprecation remains.
- Companion App: 79 affected tests passed and Flutter analyze reported no issues.
- No real directory entries, live authentication, production deployment or
  App distribution were performed.
