Skip to content

refactor(roadflare): decouple ride-on-demand from key state + actually-filter mute #82

@variablefate

Description

@variablefate

Summary

Today the RoadFlare key handshake conflates two concerns:

  1. Privacy: who can decrypt my live location (Kind 30014).
  2. Function: who can effectively send me a ride offer.

These should be independent. The privacy gate is legitimate. The functional gate is an accidental consequence of the rider's UI showing a stale-key driver as offline. Independently, the lightweight mute (#80) does NOT actually filter offers — muted riders still appear in the driver's inbox. Combined effect: mute is too weak for what users expect, and stale-key state silently kills ride-on-demand.

This issue rolls two corrections into one refactor.

Background

Per ADR-0008, fare calculation no longer depends on driver location — fare_fiat_amount is computed by the rider from pickup→dropoff. So the driver's live location is purely a UX/proximity feature, not load-bearing for the ride flow. The strict key-required gate predates ADR-0008 and was sized for a stricter privacy model than the codebase actually maintains today.

After PR #81, two mute concepts coexist:

  • Heavyweight ("Remove") — adds to MutedRider, rotates RoadFlare key.
  • Lightweight ("Mute") — sets RoadflareFollower.mutedAt, suppresses key delivery.

Neither concept currently filters incoming Kind 3173 offers from the muted pubkey.

Design

Scope A: Mute filters at the receive side

Add a sender-pubkey check against both mute lists in:

Helper: RoadflareKeyManager.isAnyMuted(pubkey) returning true if pubkey is in state.muted OR any follower row has mutedAt != null for it. Replace ad-hoc combined checks across the codebase.

Scope B: Stale-key riders can still request rides

Today the rider detects stale-key via key_updated_at mismatch in Kind 30012 and treats the driver as offline. New behavior:

  • New rider-side state: LOCATION_STALE distinct from OFFLINE. UI shows the driver as available but with a "location pending" indicator.
  • Allow the request flow from LOCATION_STALE. Distance/ETA panel shows a placeholder until key recovers.
  • When sending the request, atomically also send a Kind 3188 with status="stale". Driver receives both — the offer goes into the inbox; the ack handler triggers a Kind 3186 re-delivery in the same beat. Rider has the new key by the time the ride starts.
  • If driver is offline entirely, the offer sits unanswered like any other no-response offer (no special UX needed).

Edge case: rider has no key at all (never received one — fresh approval, key share lost). Same flow works — Kind 3188 with status="missing" instead of "stale".

Out of scope (separate issues)

  • Android fiat fare wiring on receive side (drivestr) and roadflare-rider — see follow-up.
  • Verifying Android fare calc never needs driver location — see follow-up.
  • Renaming the heavyweight/lightweight mute APIs — naming refactor, deferred.

Acceptance criteria

  • Driver-side: muted rider's Kind 3173 offers don't appear in offer inbox; RoadflareWiringTest or new test pins this.
  • Driver-side: muted rider's Kind 3189 driver pings drop silently; rate-limiter dedup confirmed unaffected.
  • Single isAnyMuted(pubkey) helper used by all four handlers (3173, 3187, 3188, 3189).
  • Rider-side: LOCATION_STALE UI state shows driver as available but with location-pending indicator.
  • Rider-side: requestRide() succeeds from LOCATION_STALE; sends Kind 3188 stale-signal alongside the offer.
  • Driver-side: receiving an offer from a follower whose key_updated_at is behind triggers Kind 3186 re-delivery.
  • Regression tests: stale-key offer arrives at driver with same payload shape as fresh-key offer; muted-rider offer is dropped in driver's offer-receive callback (not just hidden in UI).
  • Manual: drivestr + 2 rider clients. Mute rider A. Have A send a request → A sees no response, B is unaffected. Then "Remove" A → A's stored key is invalidated by rotation, B continues normally.
  • Manual: drivestr offline for >12h, follower comes back online and tries to request → request succeeds, driver responds when next online, key recovery completes inline.

Privacy + UX caveats

  • Silent mute means the muted rider has no way to know they're muted. They keep retrying. This is the standard "soft block" UX (Twitter, Instagram). Documenting as a deliberate choice.
  • LOCATION_STALE showing as "available" gives slightly weaker offline-driver clarity. Mitigation: subtle UI hint ("location updating" sub-label). Definitely better than the current funnel-killing "offline."
  • The Kind 3188 stale-signal-with-offer combo means a request can leak the rider's pubkey to a driver whose key is rotated specifically to exclude them. But: the rider was already going to send that signal as soon as their app comes back online and detects staleness — this just batches it with the offer. No new privacy leak.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions