openspec: scan-alert-hardening change — different-finder re-alert + fingerprint 24h block
This commit is contained in:
45
openspec/changes/scan-alert-hardening/design.md
Normal file
45
openspec/changes/scan-alert-hardening/design.md
Normal file
@@ -0,0 +1,45 @@
|
||||
## 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.)
|
||||
Reference in New Issue
Block a user