Skip to content

TIGER-376: SMS opt-in + phone-as-login-identifier (Account → Security) (v1.40.0) - #360

Merged
WebTigers merged 2 commits into
mainfrom
feat/tiger-376-sms-optin
Oct 23, 2026
Merged

WebTigers merged 2 commits into
mainfrom
feat/tiger-376-sms-optin

Conversation

@WebTigers

Copy link
Copy Markdown
Owner

Summary

A self-service, TCPA/10DLC-compliant double opt-in on the profile Security tab (TIGER-376) — the TCR campaign blocker. A person adds a mobile number + ticks a consent box → a 6-digit code is texted → entering it marks the number verified and writes an auditable consent record. A verified number becomes a globally-unique login identifier (log in by phone → OTP/magic link) and an alert target.

Design note (phone uniqueness)

Honors the thin-user-row design — ARCHITECTURE §7/§7b keep phone-for-login as the user_credential sms factor (not a user column), and findByIdentifier() already resolves a verified sms factor → its user. The only gap was uniqueness, so migration 0056 adds a global active-only UNIQUE on a verified number (the migration-0053 IF(...) generated-column convention) — one number → exactly one account, the guarantee login-by-phone needs.

What's in it

  • Migrations — 0055 (sms_consent: who/when/IP/scopes/consent-text + STOP/START suppression) · 0056 (global unique verified sms number).
  • Engine — Tiger_Model_SmsConsent; Tiger_Service_Authentication::requestLoginCodeSms / verifyLoginCodeSms (reuse the channel-agnostic _completeCodeLogin; silent for unknown/STOP numbers — no enumeration).
  • UI — Profile_Service_Sms + Profile_Form_Sms + the "Phone & SMS" card on the Security tab + external profile.sms.js (house primitives, intl-tel-input, full en i18n, deny-by-default ACL). STOP/HELP/START wire through Tiger_Sms::onInbound (Sms_Bootstrap).
  • Fix (recurring footgun) — Tiger_View_Helper_Asset: asset('js/x.js', '<slug>') now resolves /_modules/<slug>/… instead of silently passing a relative path that 404'd. Fixes the email + sms admin views whose JS wasn't loading, with no change to them. Guarded by new AssetTest cases.

Testing

  • 34 new tests (service / model / auth / asset), all green on real MariaDB 12.3 — full enroll→verify→consent flow, global uniqueness (two accounts can't claim one number), login-by-phone OTP, STOP suppression, and the asset-helper module form.
  • 173 surrounding profile/auth/credential/sms tests green (no regressions). CAPABILITIES regenerated; VERSION → 1.40.0.

Follow-up (not in this PR)

  • Wiring the public /auth/otp screen for "text me a code" (engine + tests are here; password-by-phone already works).

🤖 Generated with Claude Code

https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ

WebTigers and others added 2 commits October 23, 2026 13:24
…curity (v1.40.0)

TIGER-376. A self-service, TCPA/10DLC-compliant double opt-in on the profile Security
tab: add a mobile number + tick consent → a texted 6-digit code → the number is
verified and an auditable consent record is written. A verified number becomes a
globally-unique login identifier (log in by phone → OTP/magic link) and an alert
target — honoring the thin-user-row design (phone stays the user_credential sms
factor, not a user column; ARCHITECTURE §7/§7b).

- migrations 0055 (sms_consent ledger: who/when/IP/scopes/consent text + STOP/START
  suppression) + 0056 (global active-only UNIQUE on a verified sms factor's number)
- Tiger_Model_SmsConsent; Tiger_Service_Authentication requestLoginCodeSms /
  verifyLoginCodeSms (reuse the channel-agnostic _completeCodeLogin)
- Profile_Service_Sms + Profile_Form_Sms + the Security-tab "Phone & SMS" card +
  external profile.sms.js (house primitives, intl-tel-input, full en i18n, ACL)
- STOP/HELP/START suppression via Tiger_Sms::onInbound (Sms_Bootstrap)
- fix: Tiger_View_Helper_Asset — asset('js/x.js', '<slug>') now resolves
  /_modules/<slug>/… instead of silently 404-ing (recurring footgun); fixes the
  email + sms admin views whose JS wasn't loading, no change to them
- 34 new tests (service/model/auth/asset), all green on MariaDB 12.3; CAPABILITIES
  regenerated; CHANGELOG + VERSION 1.40.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
_finishLogin() regenerates the session id (fixation guard); without an active PHP
session that emits a warning, which the testing env treats as a failure. Mirror
AuthenticationTest's setUp (Zend_Session::$_unitTestEnabled = true + a real auth
storage) so the passwordless-by-phone login test runs the production session path
cleanly. Passed locally only because a session happened to be active; the full CI
suite ran it without one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASauLLscjqdsNqBNsx2Typ
@WebTigers
WebTigers merged commit a3cea00 into main Oct 23, 2026
14 checks passed
@WebTigers
WebTigers deleted the feat/tiger-376-sms-optin branch October 23, 2026 17:31
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