# CTM Phone Passkey Identity

CTM phone pairing has one durable identity rule:

> The same phone on the same WebAuthn relying-party ID reuses the same CTM
> passkey and the same CTM device row. It does not create another passkey.

This document defines the pairing, recovery, origin, cleanup, and testing
contracts for that rule.

## Terms

- **Phone profile**: CTM's best local match for a physical phone, currently the
  normalized device label plus the browser/device hint parsed from the user
  agent, for example `Owner iPhone` plus `iPhone Safari`.
- **Origin**: Browser origin such as
  `https://ctm-a8f39c-3456.usw2.devtunnels.ms`.
- **RP ID**: WebAuthn relying-party ID. In CTM it is the origin host.
- **Device token**: CTM's opaque cookie-backed token stored only as a hash in
  `ctm_device_tokens`.
- **Passkey credential**: WebAuthn public-key credential stored in
  `ctm_webauthn_credentials`.

## Pairing Decision

When a claim reaches `POST /api/auth/claim/begin-passkey`, CTM resolves the
claim origin and RP ID, then applies this order:

1. Find an active authorized device with the same phone profile and a passkey
   credential for the same RP ID.
2. If one exists, return WebAuthn authentication options with `mode: "recover"`.
   The phone confirms Face ID against the existing passkey.
3. On `POST /api/auth/claim/finish` with `mode: "recover"`, CTM rotates the
   token hash on the existing device row, updates its scopes/label from the new
   claim, records `claim_recover`, and sets a fresh `ctm_token` cookie.
4. No new `ctm_device_tokens` row and no new `ctm_webauthn_credentials` row are
   created in this recovery path.
5. If no same-RP credential exists but the same phone profile has an active
   credential for another RP ID, CTM returns
   `phone_origin_rotation_required`. It does not create another passkey.
6. Only if there is no reusable same-phone identity does CTM return
   `mode: "register"` and create a new device/passkey after WebAuthn
   registration succeeds.

This preserves the user's expectation that re-pairing the same phone is a
reconnect, not an identity fork.

## Origin Constraint

WebAuthn credentials and browser cookies are origin/RP scoped. CTM cannot make a
passkey created for one tunnel host work on another tunnel host.

Examples:

- Reopening `https://ctm-a8f39c-3456.usw2.devtunnels.ms/m/` uses the same RP
  ID as the original pairing link from that host.
- `ctm-a8f39c-3456.usw2.devtunnels.ms` and
  `ctm-b91e02-3456.usw2.devtunnels.ms` are different RP IDs.

The product consequence is intentional:

- Same phone, same RP ID: reuse the existing passkey.
- Same phone, different RP ID: stop and ask the user to open the stable phone
  URL or explicitly revoke/replace the old phone pairing.

Stable Tailscale, Cloudflare, or persistent Dev Tunnel origins are therefore
part of the auth design, not just network convenience.

## Duplicate Cleanup

The recovery path prevents new duplicates, but older CTM versions may already
have created repeated phone rows. CTM handles those in two ways:

- After any successful new claim or claim recovery, CTM revokes active older
  rows in the same duplicate group and records `device_revoke_duplicate`.
- `Setup -> Access -> Paired phones` exposes a `Keep newest` or `Keep this
  phone` action for existing duplicate groups. It calls
  `POST /api/auth/device-duplicates/revoke` and keeps the selected device.

Revoked devices remain in the database for audit, but setup hides removed rows
behind a compact "removed connections" note.

## Short-Lived Artifact Retention

Pairing creates temporary rows that are not phone identities:

- `ctm_device_claims`
- `ctm_pairing_requests`
- `ctm_webauthn_challenges`

CTM prunes expired unclaimed claims, resolved/expired pairing requests, and old
WebAuthn challenges through `cleanupMobileAuthArtifacts()`. Claimed device
tokens, passkey credentials, and audit rows are not deleted by this cleanup.

## User-Facing UX

The phone claim page distinguishes the two flows:

- Registration: `Face ID & Pair`
- Recovery: `Face ID & Reconnect`

Settings distinguishes normal auth health from duplicate cleanup:

- Active paired phones show status, last seen time, origin, and duplicate notes.
- Duplicate actions revoke only the other active devices in the same phone
  profile group.
- Different-origin same-phone conflicts require an explicit user decision
  because CTM cannot prove the old RP credential on the new origin.

## Verification Contract

Tests should cover:

1. Same phone plus same RP returns `mode: "recover"` from
   `/api/auth/claim/begin-passkey`.
2. Recovery finishes by rotating the existing device token hash.
3. Recovery leaves `ctm_device_tokens` and `ctm_webauthn_credentials` counts
   unchanged.
4. Same phone plus different active RP returns
   `phone_origin_rotation_required`.
5. Manual duplicate cleanup revokes only the non-kept active devices.
6. Expired WebAuthn challenges are pruned without deleting device identities.
