# Browser-direct profile and email management

These are SSO POST operations below the [browser-direct base](https://m7.org/docs/api/sso.user.m7.org/browser-direct.md).
Every body includes selected session handles; every call needs JSON, admitted
Origin and a fresh endpoint `DPoP` proof. The selected session must be active,
activated and currently admitted. No caller-supplied member UUID is accepted.
These operations need no five-minute reauthentication requirement. Expired
access can be renewed once before retrying; a revoked session needs sign-in.

## Profile

| Relative path | Scope | Additional input | Success |
| --- | --- | --- | --- |
| `/profile/get` | `profile` | None | `sub`, `display_name`, `avatar` |
| `/profile/update` | `profile` | Nonempty `profile` object | Same projection after update |

Only `display_name` and `avatar` are editable. Display name is trimmed, at most
128 Unicode characters. Avatar is a valid HTTPS URL, at most 255 Unicode
characters, without userinfo, controls or backslashes. An empty string clears
a field; omission preserves it. Unset values return null. Unknown fields and
an empty patch are rejected with `invalid_request`; missing scope is
`insufficient_scope` (403). These updates do not rotate tokens or close sessions.

```json
{"bd_carrier":"<carrier>","bd_session":"<session>","bd_key":"<carrier secret>","profile":{"display_name":"Sample Member","avatar":"https://images.example.org/avatar.png"}}
```

```json
{"sub":"cccccccc-cccc-4ccc-8ccc-cccccccccccc","display_name":"Sample Member","avatar":"https://images.example.org/avatar.png"}
```

In [the tenant `me` projection](https://m7.org/docs/api/sso.user.m7.org/browser-direct-authentication.md#me-projection),
`name` remains the system login name, `display_name` is the editable name,
`picture` reflects avatar, and `preferred_username` is absent. Profile editing
does not rename the login, alter groups or grant manager permissions.

## Email operations

All six require the `email` scope. An address's presence is distinct from verified
ownership, active status and primary selection. Adding an address does not send
mail automatically or assert ownership.

| Relative path | Additional input | Success |
| --- | --- | --- |
| `/emails/list` | None | Email view |
| `/emails/add` | `email` (valid, trimmed/lowercased, ≤254 bytes) | Email view plus `email_id` |
| `/emails/resend` | `email_id` UUID | Email view after delivery request |
| `/emails/verify` | `email_id` UUID, `code` six-digit string | Email view after proof |
| `/emails/primary` | `email_id` UUID | Email view with chosen primary |
| `/emails/remove` | `email_id` UUID | Email view after removal |

Every email view contains `sub` and `emails`. Rows contain only `id`, `email`,
`status`, boolean `verified`, `primary_email`, `can_send`, `can_verify`,
`can_make_primary`, `can_remove`, and numeric `resend_after` seconds. Archived
rows are omitted; disabled rows can be displayed but are not editable. Use
capabilities as a current UI hint and handle a later policy/state rejection.

```json
{
  "sub":"cccccccc-cccc-4ccc-8ccc-cccccccccccc",
  "emails":[{
    "id":"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
    "email":"member@example.org","status":"active","verified":true,
    "primary_email":true,"can_send":false,"can_verify":false,
    "can_make_primary":false,"can_remove":false,"resend_after":0
  }]
}
```

```json
{"bd_carrier":"<carrier>","bd_session":"<session>","bd_key":"<carrier secret>","email_id":"dddddddd-dddd-4ddd-8ddd-dddddddddddd","code":"123456"}
```

There can be at most ten stored addresses. Add creates pending, unverified,
nonprimary email; repeating add for this member's current usable address returns
its row. A disabled/archived row is not silently revived. Another unarchived
member owning that address in the same organization conflicts; other organizations
have separate ownership. Addresses are not account-merging instructions.

Explicit resend issues a ten-minute proof tied to member, address, application,
origin, key and initiating selected session. Switching session requires a new
send from that context. Resend replaces older proof, including from another
carrier. Each challenge permits five attempts; the member has 20 attempts/hour,
one send/minute and five sends/hour across apps/carriers. Failed delivery attempts
also count. Successful proof consumes the challenge. Resend/verify on an already
verified active row returns the current view without another proof consumption.
After a lost response reload the list; if still unverified, resend rather than
assume success. The first verified address becomes primary when no verified
primary exists.

Only verified active email can become primary. Switch primary before removing
it; the final verified address is protected. Pending nonprimary addresses may
be removed. Choosing primary and removing email affect subsequent `me`, recovery
and email sign-in immediately. Missing/ambiguous primary state is not resolved
by arbitrary selection, and email login requires unique current verified
ownership. Password recovery can use another verified address as documented
in [recovery](https://m7.org/docs/api/sso.user.m7.org/browser-direct-registration-recovery.md).

Self-service email edits, including removing a verified nonprimary address,
keep sessions. **Manager removal** has a different credential-revocation effect;
see [manager security](https://m7.org/docs/api/api.user.m7.org/tenant-member-security).
No dedicated universal self-service audit ledger is exposed by these responses.

| Error | Handling |
| --- | --- |
| `email_unavailable` (409) | Address conflict; select another address, never infer the other owner |
| `email_limit`, `email_not_verified`, `email_primary`, `email_last_verified` (409) | Resolve the ten-address, verification, primary or final-verified protection, then reload |
| `invalid_request` (400/404) | Malformed ID/code/address or unavailable row |
| `email_code_invalid` (400) | Wrong/expired code; reload/resend |
| `email_verification_required` (409) | Expired, exhausted or wrong-session challenge; request a new code within the member budget |
| `email_rate_limited` (429) | Respect member cooldown and send window |
| `access_denied` (403) | Organization-managed or inactive address; no self-service edit |
| `session_changed` (409) | Reload after changed selected authority or address |
| `temporarily_unavailable` (503) | Concurrent update; retry later with a fresh proof |
| `insufficient_scope` (403) | Obtain a session with registered `email` scope |
| Operational/delivery failure | Do not mark verified; reload and follow resend cooldown |

Operators need working existing Post/email delivery, protected encrypted
transaction storage, and the existing email-confirmation slot secret (at least
32 bytes). These secrets stay server-side. A successful send request does not
prove mailbox delivery; setup and live delivery acceptance are separate checks.
