Story-2863 · EPIC-2834 W3

The stolen-phone switch kills every outstanding channel link

A remote logout is an account-wide security ceremony, not an active-firm preference. Before Clerk sessions are revoked, the app calls POST /app/me/remote-logout/channels. The route accepts no firm or target-user input: the verified bearer is the target. It bumps the revocation epoch for every firm represented by that user's channel bindings, revokes those bindings and fetch tokens, and aborts pending confirmations in one transaction.

Three controls, three deliberately different blast radii

Control Blast radius Why
Remote logout (CC-45) Every binding firm for the authenticated user A stolen phone must lose every outstanding channel link, even with no active Clerk org.
Unbind (CC-40) One binding and its fetch tokens A second binding in the same firm must stay live. The pair-wide epoch is therefore not moved.
Offboard (CC-24) One firm+user pair Removal from Firm A must not disrupt the same counsel in Firm B.

Self-bounded RLS keeps the sweep atomic and O(1)

Ordinary channel writes are firm-scoped by PostgreSQL RLS. Remote logout starts before a firm is selected, so migration 0211 adds narrow self-service write policies for the affected channel tables. Each policy requires the authenticated pre-firm AppMe bracket and independently requires the row to belong to app.current_user_id. There is no BYPASSRLS or elevated function.

verified bearer
      │
      ▼
POST /app/me/remote-logout/channels
      │  no firm_id / no target user
      ▼
sweep_user_remote_logout(authenticated_user)
      ├── epoch UPSERT for all binding firms
      ├── revoke all active bindings
      ├── revoke all outstanding fetch tokens
      ├── abort pending confirmations
      └── delete sticky channel context
             FIXED bulk statements · ONE transaction

The function uses PostgreSQL statement_timestamp() for both the epoch and direct revocation stamps. Database time is the authority, so clock skew between Lambda and RDS cannot place the kill mark before a binding created moments earlier. Audit rows are validated through the typed emitter, buffered, and flushed once; one binding and three bindings both complete in the same nine request-level database calls.

Fail-closed proof

ProofResult
CC-45: org-less caller with bindings in two firms Both epochs move; all three bindings and tokens die; unrelated user's link stays live.
Epoch-only defense After direct revocation stamps are removed in the test, every swept link still returns DENY_EPOCH_DEAD.
CC-40 unbind Target binding/token die; sibling binding in the same firm remains active; no pair epoch is moved.
CC-24 offboard regression Existing firm-scoped epoch and artifact sweep remains green.
Planted database failure The endpoint returns 500 and leaves zero epochs, zero audits, and every binding/token live.
Query-count proof One binding and three bindings both report db_query_count=9.

JWT-expiry residual

The download-open Lambda verifies Clerk JWTs offline. A JWT already issued before remote logout therefore remains cryptographically valid until its normal expiry; this is the same accepted and documented tradeoff as Story-2859. The residual does not preserve the wrapper link: every open also checks the database revocation epoch, so the fetch token dies immediately even while that JWT has time left.