Skip to content

feat(sms): ledger, hashed single-use codes, learned breaker signals and the admin API - #307

Merged
roncodes merged 1 commit into
release/v1.6.69from
feature/sms-guard-phase-1
Oct 13, 2026
Merged

roncodes merged 1 commit into
release/v1.6.69from
feature/sms-guard-phase-1

Conversation

@roncodes

Copy link
Copy Markdown
Member

What

Phase 1 of the SMS guard, on top of #305 (phase 0).

  • Send ledger (sms_sends): every allowed send is reserved and settled with the provider outcome; every refusal is written with its reason. Rows carry tenant, purpose, source, client address, destination country and type, and the verification code they carried. sms:ledger:prune masks numbers after 30 days and deletes rows after 90 (configurable). Writes are best effort: a missing table never fails a send.
  • Verification codes gain company_uuid, identity, channel, ip_address, user_agent, sms_send_uuid, consumed_at (identity backfilled from meta for recent rows). generateSmsVerificationFor(..., ['hashed' => true]) stores an HMAC and gives the message callback the plain code; findActiveFor() / findActiveForSubject() return the newest live code (new column or legacy meta), check() verifies and locks, consume() uses it up. On a table without the columns everything degrades to meta, so extensions can adopt this before their own migrations.
  • Two learned breaker signals: a burst to a country the company has never texted (from the ledger; armed after 50 sends so "never" means something), and SMS codes issued in the last hour with almost none verified. Both pause and alert like the phase 0 signals.
  • SmsServiceChannel: a notification channel that delivers through SmsService, so notification texts (storefront order alerts) are guarded and ledgered too.
  • Admin API under int/v1/sms-guard: settings (mode, budgets, blocklists, alert recipients, per-company exceptions), reset, pauses (create and lift), usage per organization against its budget, and the ledger with filters. The console page follows in fleetbase/fleetbase.
  • The unrouted legacy Twilio-direct SMS login methods in AuthController are removed with their tests.

Merge order

This branch is based on feature/sms-guard so the diff shows only phase 1. Before merging #305, retarget this PR to release/v1.6.69 (gh pr edit --base release/v1.6.69); otherwise GitHub closes it when the base branch is deleted.

Tests

Run locally per file and in CI: SmsLedgerTest, SmsGuardLedgerTest (ledger flow, blocked rows, new-country and conversion trips), VerificationCodeTracingTest (columns, hashed codes, lookups, consume, legacy rows), SmsServiceChannelTest, SmsLedgerPruneTest, SmsGuardControllerTest (payload, validation, pauses, usage, ledger). Existing verification code, 2FA, user and routes tests still pass.

Docs

Env vars SMS_GUARD_LEDGER_RETENTION_DAYS, SMS_GUARD_LEDGER_MASK_AFTER_DAYS; command sms:ledger:prune (schedule it daily); admin endpoints. needs-docs.

…nd the admin API

Phase 1 of the SMS guard (phase 0 is the guard itself):

- sms_sends ledger: every allowed send is reserved and settled with the provider
  outcome, every refusal is written with its reason, with tenant, purpose, source,
  address, destination country and the code row it carried. sms:ledger:prune masks
  numbers after 30 days and deletes rows after 90 (both configurable).
- verification_codes gain company_uuid, identity, channel, ip_address, user_agent,
  sms_send_uuid and consumed_at (identity backfilled from meta for recent rows).
  generateSmsVerificationFor(['hashed' => true]) stores an HMAC and hands the
  message callback the plain code; findActiveFor()/findActiveForSubject() find the
  newest live code (new column or legacy meta), check() verifies, consume() uses
  it up. Everything degrades to meta-only on a table without the columns.
- Two breaker signals read from the data: a burst to a country the company has
  never texted (armed after 50 ledgered sends), and SMS codes issued with almost
  none verified in the last hour.
- SmsServiceChannel: a notification channel that sends through SmsService, so
  notification texts are guarded and ledgered too.
- Admin API under int/v1/sms-guard: settings (mode, budgets, blocklists, alert
  recipients, per-company exceptions), reset, pauses, usage per organization and
  the ledger with filters. The console page follows separately.
- The unrouted legacy Twilio-direct SMS login methods are removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant