Phone-ban timing and reclamation (Phase 2) โ
Status: Draft for review (2026-07-01). Owner: Mechelle. Phase 2 of the sealed-account ban work. Phase 1 (login-harvest arming, the token + pairing check) is SEALED_BAN_RECONCILIATION_SPEC.md; this builds on the same hash-only blocklist and the in-portal appeal flow (Phase 1 ยง6.1). Framing: sealed-only. US launch for the interim.
How to read this: each section opens with an ELI5. The whole idea in one line: a phone number is a lease, not an identity, so number-bans must be timers (with an appeal escape hatch), never tombstones.
1. The core problem โ
ELI5: Phone companies recycle numbers to new people. If we block a number forever, we eventually block an innocent stranger who was handed that recycled number, and they can never sign up. Number-bans have to expire.
A phone number is rented, not owned. US carriers age a disconnected number at least 45 days, then reassign it. A permanent phone-hash ban (expiresAt: null) therefore does two bad things at once: it's evadable (the actual bad actor just gets a new number) and harmful (it traps the next innocent owner of the recycled number, invisibly and unappealably). The whole point of Phase 2 is to make number-bans time-bounded and to give the innocent-newcomer case a way through.
2. Goals and non-goals โ
ELI5: Make number-bans expire on a sensible timer, and when we do wrongly block a newcomer, give them a door instead of a wall.
Goals
- Severity-aware, time-boxed phone bans (ยง3). Today the gate is active-vs-expired only; severity tiers exist but aren't consulted.
- Route a recycled-number collision into the appeal flow (ยง4), not a dead-end 403.
- Sealed-aware reclaim (ยง6): today reclaim is legacy-only.
Non-goals
- Reassignment-detection tooling (FCC RND / commercial phone-intelligence). Considered and deferred, see ยง5.
- International. US launch; international reassignment signals don't meaningfully exist (ยง5).
- Permanent-ban aging reaper. Design-only for now (ยง3), noted for later.
3. Time-boxed bans โ
ELI5: How long a number stays blocked is a dial: too short and the bad actor just waits it out, too long and we start trapping innocent newcomers. We pick a middle, by severity, and never set it to "forever" silently.
The coupling to know: ban length and false-positive risk move together.
- Under ~45 days: the number physically can't have been reassigned yet, so near-zero innocent-newcomer risk, but the bad actor can wait it out.
- Past the aging window: stronger deterrent, but reassigned-number newcomers start hitting the ban.
There is no length that is both a strong deterrent and zero false positives; they trade off. So:
Severity tiers (mirror the userId ban's existing 7days / 30days / permanent picker):
| Severity | Duration | Notes |
|---|---|---|
| minor | 7-30 days | short; near-zero false positives |
| standard | 90 days | just past the aging floor: real deterrent, bounded false positives |
| severe / repeat | long but finite, appeal-gated | never a silent expiresAt: null forever-ban; the appeal path (ยง4) is how an innocent reassigned owner gets through while the bad actor hits a reviewer |
Two concrete changes:
- Consult severity at the gate.
isPhoneBannedis currently active-vs-expired only, sowarningandshadowblock signup identically topermanent. Make the gate severity-aware so non-permanenttiers behave as designed. - Default an expiry by severity on
ban-phone/ arming, instead ofexpiresAt: null. The admin phone-ban affordance should default to a duration and reserve/gate "permanent."
Deferred: an automatic aging reaper that eventually deletes long-lived hash rows (the design-only "1-year hashed-identifier deletion" in SAFETY_MECHANICS.md, currently unimplemented). Not in Phase 2; noted so the appeal path (ยง4) is the interim release valve.
4. The recycled-number false-positive routes to the appeal flow โ
ELI5: If a newcomer who just got a recycled number hits a ban at signup, don't slam a door in their face, hand them the appeal flow. They prove they hold the number now; a human sorts it out. The actual banned person would have to lie to that human.
A signup blocked by a phone ban should not be a flat 403. It routes into the in-portal appeal flow (Phase 1 ยง6.1): the newcomer proves possession of the number by OTP, the appeal is queued (hash-only, no plaintext, no email), and a moderator adjudicates. The genuine newcomer gets in; the actual bad actor hits a reviewer instead of walking around a wall.
This is the same appeal machinery reinstatement uses, so recycled-number rescue and ban-appeal are one flow.
4.1 Reclaim a number is not the same as reinstate an account โ
ELI5: Freeing a recycled number for its new owner is a different act from forgiving the person we banned. The number is a lease; the account is an identity. Lifting the number-ban for a newcomer must never un-ban the old account. The old user stays locked out.
One appeal, but the outcome acts on different axes (Phase 1 ยง2's two independent controls):
- Reclaim the number (recycled line, NEW owner). A different human was handed the recycled number. Lift the phone axis only (
unban-phonebyphoneHash) so the newcomer can register / sign in. Do NOT touch the old account's userId ban (users/{oldUid}.banned+ Authdisabledstay set). The old banned user remains locked out. - Reinstate the account (SAME person, overturned on the merits). Lift the userId axis (
/unban) and clear the phone axis via Option A / the login-harvest self-clear (Phase 1 ยง6). The same owner returns. - Uphold. Nothing changes.
Safe by construction. The two controls are independent records (Phase 1 ยง2): a phone-axis clear is scoped to the phoneHash (the lease); a userId ban lives on users/{oldUid} (the identity). Overturning the banned_accounts row never reads or writes the old user doc, so reclaiming a number can never reinstate an account. Same decoupling that lets a sealed userId-ban arm a hash-only phone row with no back-link.
The old account cannot re-grab the number, either. Once the newcomer seals on the reclaimed line, auth_lookup/{phoneHash} is repointed to the new owner. The old banned userId, even scripting /token, cannot pair against it: the sealed pairing check (verifyProof(proof, auth_lookup.authProofHash)) now matches the new owner's proof, not the old one, so the old user can neither re-arm the number nor recover their sealed userId. The old ban is maintained; the new owner is unaffected.
Does reclaiming reopen evasion for the old user? No, because possession is the real gate. Registration requires OTP possession of the line, and a genuinely recycled number is held by the newcomer, not the old user, who lost it when the carrier reassigned it. The phone ban is only a secondary block layered on top of possession; lifting it for the current holder does not let the old user register on a number they can no longer receive an OTP for. If the old user still holds the line it is not a recycled case: the moderator denies (a "recycled" claim inside the ~45-day aging floor, ยง3, is the red flag, since the number cannot physically have been reassigned yet), and the userId ban blocks the old account regardless. The one unavoidable residual is unchanged by reclaim: the old user can always get a different number and a fresh account, which no number-scoped ban ever prevented.
flowchart TD
Blocked(["Signup or login blocked by a phone ban"]) --> Appeal["In-portal appeal (Phase 1 6.1):<br/>prove possession by OTP,<br/>hash-only, no plaintext, no email"]
Appeal --> Review{"Moderator adjudicates<br/>(number-blind: sees the ban reason,<br/>never the number)"}
Review -->|"Recycled: a NEW owner holds the line"| Reclaim["RECLAIM the number:<br/>unban-phone by phoneHash<br/>(PHONE axis only)"]
Review -->|"Same person, overturned on merits"| Reinstate["REINSTATE the account:<br/>/unban (userId axis)<br/>+ phone via Option A / self-clear"]
Review -->|"Upheld"| Uphold(["Stays banned"])
Reclaim --> Keep["OLD userId ban PERSISTS:<br/>users/oldUid.banned + Auth disabled untouched.<br/>Old account still cannot log in."]
Keep --> NewOwner(["New owner registers / signs in on the number"])
Reinstate --> SameOwner(["The forgiven owner logs in again"])4.2 How strong must the "recycled" proof be? (it scales with ban severity) โ
ELI5: We can't perfectly tell a genuine new owner from the banned person lying about it, so we scale how hard the proof is by how bad the ban is. A short ban expires on its own, so a light check is fine. A permanent ban has no expiry safety net, so a newcomer has to actually prove the number is newly theirs.
The honest limit (from ยง4.1): OTP possession proves the appellant holds the line now, not that they are a different human. With no reassignment data (ยง5) and anonymity by design, there is no clean technical test for "new person." So the evidentiary bar is a policy dial, scaled to the stakes:
| Ban tier | Time-expiry safety net? | Reclaim bar |
|---|---|---|
| minor / standard (7-90d) | Yes (auto-unblocks on expiry) | Light: OTP possession + the ~45-day time gate + moderator judgment. A "recycled" claim inside the aging floor is auto-denied. |
| severe / permanent | No | High: on top of possession, the appellant must prove the number was newly issued to them (documentary proof of new service, i.e. the line started after the ban date). |
Why the perma tier needs the higher bar. A short ban clears itself: a wrong "deny" is only temporary and a wrong "allow" is bounded. A permanent ban has neither safety net. Err toward "deny" and you trap an innocent newcomer forever; err toward "allow" and a severe actor is back. Neither error self-corrects, so the perma tier demands real evidence, not just the passage of time.
Handling the documentation without breaking the seal. The proof (a carrier bill / contract / service-start record) is reviewed by the moderator as a transient artifact, exactly like the plaintext number: it transits for human review and is discarded, never stored on or linked to the hash-keyed appeal record. Only a verified / not-verified result persists: no name, address, or number is kept. (Auto-redacting the doc is a fragile stopgap; review-then-discard is cleaner.)
Honest caveats. Documentary proof is stronger, not bulletproof: it can be forged (though a forged submission is itself a strong bad-faith signal and an accountability lever), "new issuance" evidence is region-dependent, and review is manual. That friction is acceptable precisely because a perma-ban reclaim is rare and high-stakes by definition.
5. Reassignment detection: considered, deferred โ
ELI5: We looked hard for a way to know a number got recycled. In the US there's one database, but its rules forbid our use; internationally there's nothing but privacy-invasive data brokers. So we don't wire any of it, we lean on the timer and the appeal instead.
Findings (researched 2026-07-01):
- Time is itself the signal. The 45-day aging floor means a re-signup during a short ban is almost certainly the same person; long after, it's more likely a newcomer. Time-boxing (ยง3) already captures most of this, for free.
- FCC Reassigned Numbers Database (RND): US-only, cheap ($8/mo entry), real-time API, and semantically it fits (query with the ban date, get "reassigned since then?"). But its Terms of Use restrict queries to "callers" using the data solely for TCPA (robocall) compliance, and specifically prohibit any other use. Ban/signup vetting is not a TCPA use, so it's off-limits without counsel sign-off. It also only knows "permanently disconnected," not "who holds it," so a same-line bad actor returns "No." Not wired.
- International: there is no government/regulator reassigned-number database anywhere (confirmed absent in the UK, EU, Canada, Australia). The only options are commercial phone-intelligence vendors (Twilio, Telesign, Prove, Vonage, even Prelude's Watch API) selling SIM-swap / tenure signals, which are data-broker products: they de-anonymize the subscriber, need a GDPR lawful basis, have patchy carrier coverage where "no data" silently reads as "clean," and run against the anonymity thesis and the EU-residency posture. Not wired.
Decision: rely on time-boxing (ยง3) + the appeal path (ยง4). Do not build reassignment detection. (If Lantern ever runs consented SMS campaigns, the RND becomes legitimately usable for that, unrelated to abuse-gating.)
Privacy bar if ever revisited. Any future reassignment check must clear the sealed bar, not just the legal one: the number transits once (it is already in hand for the OTP possession proof), the check writes only a yes/no onto the hash-keyed appeal record, the plaintext number is discarded, and no second third party is added beyond the OTP provider already in the loop. RND fails the legal bar for abuse-gating regardless (ToU above); commercial tenure vendors fail the anonymity / GDPR bar (above). The bar is high on purpose.
6. Sealed-number reclaim โ
ELI5: Our existing "I got a recycled number, let me claim it" flow only works for old-style accounts; it can't handle a sealed one because it can't look up who owns a sealed number. A sealed-aware version uses the same prove-you-hold-it appeal instead.
Today the reclaim flow (phoneRecycling.js) is legacy-only: it resolves the old account via users.where(phoneHash), which sealed rows lack. A sealed-aware reclaim can't do a server-side resolution either, so it uses the same possession-proof appeal as ยง4 / Phase 1 ยง6.1 rather than a lookup. In practice this collapses into the appeal flow: prove you hold the number, a moderator (or an automated dormancy check) frees it.
7. Implementation sketch (order) โ
ELI5: The build order, smallest first.
- Severity-aware gate: teach
isPhoneBanned/ the signup gate to consult severity, so non-permanenttiers behave as designed. (Small, backend-only.) - Default expiry by severity on
ban-phoneand on Phase 1 arming (carryusers.banDurationintoexpiresAt). Reserve/gate "permanent" in the admin affordance. - Signup-blocked routes to appeal: when
createUser/verify-otphits a banned phoneHash, return an appeal affordance instead of a dead-end 403; wire it to the Phase 1 ยง6.1 appeal flow. - (Later) sealed-aware reclaim + the aging reaper.
8. Open questions โ
ELI5: What's still to decide.
- Standard duration: 90 days confirmed as the default? (Recommended.)
- "Permanent" for phone bans: allow it at all (gated, human-reviewed), or cap every phone ban at a finite max? Spec leans toward "no silent permanent; long-but-finite + appeal." If permanent is allowed, its reclaim requires documentary proof of new issuance, not just time or possession (ยง4.2).
- Signup-blocked UX: appeal affordance inline at signup, or a separate "appeal" entry point?
9. Code map โ
| Concern | Location |
|---|---|
| phoneHash membership + active check (add severity) | bannedAccounts.service.js isPhoneBanned, isHashBanned |
Signup gates that call isPhoneBanned | phoneCreateUser.js, phoneOtp.js |
| Ban-phone (severity + default expiry) | moderation.js /ban-phone |
| Existing (legacy) reclaim | phoneRecycling.js |
| Appeal flow (shared with Phase 1) | SEALED_BAN_RECONCILIATION_SPEC.md ยง6.1 |