Skip to content

feat(sms): toll-free compliance before launch - #21

Merged
rvegajr merged 4 commits into
developfrom
cursor/sms-compliance-9b88
Oct 3, 2026
Merged

rvegajr merged 4 commits into
developfrom
cursor/sms-compliance-9b88

Conversation

@rvegajr

@rvegajr rvegajr commented Oct 1, 2026

Copy link
Copy Markdown
Member

Summary

  • Centralizes SMS consent and opt-out checks in sendText, including brand prefixing and relay 21610 handling.
  • Secures inbound SMS and delivery-status webhooks with relay HMAC signatures, Twilio form fields, and full STOP/START/HELP keyword support (TwiML replies).
  • Adds opt-in checkboxes on sign-in/booking/waitlist forms, roster double opt-in (YES), /privacy and /terms#sms pages, and site footer links.

Test plan

  • npm run typecheck
  • npm run lint
  • npm test (116/117 pass here; toolchain test expects Node ≥24 as in CI)
  • New coverage in test/sms-compliance.test.ts, test/legal-pages.test.ts, test/phone.test.ts
Open in Web Open in Cursor 

cursoragent and others added 2 commits October 1, 2026 20:48
…pages

Co-authored-by: Ricardo Vega <github@noctusoft.com>
Co-authored-by: Ricardo Vega <github@noctusoft.com>
@rvegajr

rvegajr commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Review (Claude Code, against the Noctusoft relay contract)

Passes:

  • The inbound endpoint was broken (it read lowercase field names) and unauthenticated. It now reads the raw body: /webhooks/sms and /webhooks/sms-status get express.raw() ahead of the global urlencoded and JSON parsers, the relay signature is checked, and Twilio's field casing is accepted.
  • Every send goes through sendText, which now checks consent and opt-outs. Session cancellation used to skip that check; it can't anymore.
  • Roster members: a coach adding a number triggers one "Reply YES" text (sendRosterConsentRequest). YES records consent with source reply-yes, and offers stay blocked until then. A test covers it.
  • STOP, START and HELP are handled through both the message text and OptOutType. 21610 from the relay and from status callbacks is handled. The /privacy and /terms#sms pages carry the required text.

Follow-up: toE164 falls back to accepting any 10 digits so the fictional 555 numbers in tests normalize. Consider limiting that fallback to tests, so a mistyped real number isn't accepted. Twilio would reject an invalid one anyway.

Deploy order:

  1. Relay PR noctusoft-relay #95 (signed forwards) is deployed on ns.
  2. The relay's product list routes Coachatron's number +1 833 364-5083 to https://coachatron.com/webhooks/sms and /webhooks/sms-status (a relay change still to come).
  3. On ns: sudo -u www-data node scripts/relay-keys.js inbound-secret --product coachatron --rotate, then set RELAY_INBOUND_SECRET in production.
  4. Run migration 0010_sms_consent, then merge and deploy this PR.
  5. Submit toll-free verification for +1 833 364-5083 before launch.

@rvegajr
rvegajr marked this pull request as ready for review October 2, 2026 01:53
Renumber sms_consent migration to 0011 after develop's 0010_start_link.
Landing page gets the Privacy/Terms footer; landing tests use signed
form webhooks, OTP consent, and TwiML. The stranger start-link test is
marked todo: the consent gate blocks that reply.

Co-authored-by: Ricardo Vega <github@noctusoft.com>
@cursor

cursor Bot commented Oct 2, 2026

Copy link
Copy Markdown

Merged develop (landing week box / start links, PR #20) in 82f6647.

Simple conflicts (resolved): src/config.ts (both appended), src/routes/coach.ts (develop's saved flag + consent check), src/routes/webhooks.ts imports, overflow-cascade HELP assertion, and migration number clash — sms_consent renumbered to 0011 since develop owns 0010_start_link. Landing page now carries the Privacy/Terms footer; landing tests moved to signed form webhooks, sms_consent on OTP, and TwiML.

Open intent conflicts (need a decision):

  1. A stranger texting their week gets no /start link: sendText refuses numbers without consent. Is an inbound text consent for that one reply, or should it go back as a TwiML <Message>? The stranger test is marked todo until decided.
  2. /start/:token creates a coach with no sms_consent row, so later non-OTP texts to that coach are refused.

…rsor/sms-compliance-9b88

Renumber sms_consent migration to 0012 after develop's 0011_email_signin.
One inbound signature check: develop's INBOUND_SMS_URL and relaySignature,
this branch's 503-in-production when the secret is unset. SMS consent is
required for a mobile sign-in and for the add-mobile card, not for email.
Develop's STOP-as-text and stranger start-link tests are marked todo:
both conflict with TwiML keyword replies and the consent gate.

Co-authored-by: Ricardo Vega <github@noctusoft.com>
@rvegajr
rvegajr merged commit cf26457 into develop Oct 3, 2026
3 checks passed
rvegajr added a commit that referenced this pull request Oct 4, 2026
Promote develop to staging: SMS compliance (#21), dev SMS capture (#29)
rvegajr added a commit that referenced this pull request Oct 4, 2026
Promote staging to production: SMS compliance (#21), dev SMS capture (#29)
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.

2 participants