Files

2.9 KiB

Context

Phase 2's alert throttle (250 m / 10-min window) blocks repeat alerts from the same spot but doesn't distinguish finders — multiple people at the same spot only get one alert, and one device can re-alert by moving around. This change adds (A) a different-finder re-alert rule and (B) a 24 h per-device block via browser fingerprinting. Anti-spam motivation mirrors the 2014 system (which tracked fingerprint + IP on every alert).

Goals / Non-Goals

Goals:

  • Every distinct finder who leaves a phone number reaches the owner (within the window).
  • One device cannot spam the owner more than once per 24 h.
  • Minimal dependency footprint: hand-rolled fingerprint hash, no third-party JS.

Non-Goals:

  • Identity-grade fingerprinting (this is anti-spam, not authentication; a determined attacker can fake signals).
  • IP-based blocking (privacy + NAT false positives) — fingerprint only.
  • Rate limits tied to plans/billing (Phase 5).

Decisions

  1. Fingerprint = lightweight hand-rolled hash of navigator.userAgent, language, timezoneOffset, screen WxH, platform, hardwareConcurrency. JS computes a 32-bit hash → base36 string. Alternative: fingerprintjs-lite (heavier, more robust) — deferred; the hand-rolled version is dependency-free and adequate for anti-spam; upgradeable later.

  2. Fingerprint block is checked FIRST in shouldAlert (before the 10-min throttle): a fingerprint seen in the last 24 h ⇒ record-only. Rationale: the same device is the same person regardless of which location/phone they present.

  3. Different-finder rule lives inside the 10-min window branch: within the window, re-alert if moved > 250 m OR (newPhone != "" AND newPhone != lastAlert.scanner_phone). Outside the window, alert as before.

  4. Contact path dedup: FinderContact queries the last alerted scan; within the window and same phone ⇒ store-only (no SMS). Different phone ⇒ SMS. Fingerprint present and seen within 24 h ⇒ store-only.

  5. Schema: ALTER TABLE scans ADD COLUMN IF NOT EXISTS fingerprint TEXT; (idempotent, nullable). New sqlc query GetRecentScanByFingerprint (WHERE fingerprint = $1 AND scanned_at > now() - interval '24 hours' ORDER BY scanned_at DESC LIMIT 1).

  6. ScanRequest gains optional phone + fingerprint; both flow into InsertScan.

Risks / Trade-offs

  • [Fingerprint is spoofable] → accepted (anti-spam, not security); document as such.
  • [Different-phone rule could alert twice from one person with two numbers] → acceptable; the owner sees both contacts and the 24 h fingerprint block caps same-device repeats.
  • [Fingerprint column grows] → TEXT, nullable, negligible.

Migration Plan

  1. make db-up (idempotent ALTER) → scans.fingerprint.
  2. sqlc make generate (new query + column in model).
  3. Deploy code + JS; no data migration needed.

Open Questions

  • None blocking. (24 h window is a constant; adjustable later.)