Skip to content

[needs-human] feat: Scholarmancy SMS toll-free compliance (relay + consent) - #28

Merged
rvegajr merged 5 commits into
mainfrom
cursor/sms-compliance-289d
Oct 3, 2026
Merged

rvegajr merged 5 commits into
mainfrom
cursor/sms-compliance-289d

Conversation

@rvegajr

@rvegajr rvegajr commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements Scholarmancy toll-free SMS compliance per the attached plan: Noctusoft relay outbound (NOCTUSOFT_API_KEY), sms_consents gate, relay-signed inbound webhooks (STOP/START/HELP/YES + OptOutType, status 21610), web opt-in + legal pages, co-parent double opt-in, and Scholarmancy branding on SMS copy.

Highlights

Area Implementation
contracts smsCompliance.ts — brand, purpose, webhook URLs, SMS_OPT_IN_TEXT_VERSION, ISmsConsentRecord
database SmsConsentRepository + indexes
agents NoctusoftSmsRelayClient, GuardedSmsSender, createSmsStack, libphonenumber-js normalizePhoneE164, smsSendSurface guard
api Relay signature + raw body, webhook handlers, auth/settings/students consent writes, MagicLinkSender → guarded SMS
workers Digest via GuardedSmsSender; no Twilio SDK
web SmsOptInCheckbox, register/settings, privacy/terms + tests

CI fix (latest commit)

CI / Build and E2E Tests / Playwright E2E both failed at packages/api tsc --build with TS2352 in relay-signature.middleware.ts (casting Request straight to { rawBody: string }). The fix reads rawBody through Request & { rawBody?: unknown } and narrows it with typeof. This error was missed locally because pnpm type-check stopped at an earlier workers failure before it reached api.

Verified

pnpm build                                                # exit 0 (full monorepo, as CI Build runs it)
pnpm type-check                                           # no errors
pnpm --filter @scholaracle/api lint:strict && pnpm --filter @scholaracle/api format:check
pnpm --filter @scholaracle/api test -- --testPathPattern='twilio-webhook|verifyRelay'   # 15 tests
pnpm --filter @scholaracle/agents test                    # 331 tests (earlier commit)
pnpm --filter @scholaracle/workers test
pnpm --filter @scholaracle/database test -- --testPathPattern=SmsConsent
pnpm --filter @scholaracle/web test -- --testPathPattern='privacy|terms'

Pre-existing on origin/main (not fixed here)

  • pnpm -r lint fails in packages/scraper-core (prettier/eslint).
  • packages/connector test Jest heap OOM after contracts build.

Gate

Touches pnpm-lock.yaml and package.json → human merge / needs-human per repo policy (owner-approved libphonenumber-js).

Not verified here

  • Full monorepo pnpm -r test
  • Playwright E2E itself (runs in CI; previously blocked by the build error)
Open in Web Open in Cursor 

cursoragent and others added 3 commits October 1, 2026 19:16
- Guarded SMS send with consent records, relay client, and brand prefix
- Relay inbound webhook signatures; STOP/START/HELP/YES and 21610 handling
- Opt-in UI on register/settings; privacy/terms updates; co-parent YES flow

Co-authored-by: Ricardo Vega <github@noctusoft.com>
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 on the security-critical parts:

  • verifyRelayInboundSignature matches the relay's scheme exactly: base64 HMAC-SHA256 over (public URL + raw body), with a constant-time compare. The URL comes from config, not request headers.
  • The raw body is captured correctly: the router-level express.urlencoded({ verify }) runs, the global JSON parser skips /api/webhooks/twilio, and no site-wide form parser runs before it.
  • If RELAY_INBOUND_SECRET is unset, production returns 503; with it set, a missing or wrong signature is a 401.
  • GuardedSmsSender checks consent, normalizes to E.164, adds the brand prefix, logs, and records an opt-out on 21610. A test fails if anything else sends.
  • STOP, START, HELP and YES are handled, each returning correctly, with OptOutType tests. A status ErrorCode 21610 records an opt-out for the To number.

Deploy order (required, or inbound texts break):

  1. Relay PR noctusoft-relay #95 (signed forwards) is deployed on ns.
  2. On ns: sudo -u www-data node scripts/relay-keys.js inbound-secret --product scholarmancy --rotate, then set the printed secret as RELAY_INBOUND_SECRET on Scholarmancy's production API.
  3. Merge and deploy this PR.
  4. Switch Twilio Messaging Service MG902c609cf2c89b5e8b455c9fa3d4efb7 to "use inbound webhook on number" (the number already points at the relay). Until then Twilio posts straight to the app with no relay signature, and the app returns 401.

The pnpm -r lint (scraper-core) and pnpm -r test (connector out of memory) failures also happen on main without this PR.

Co-authored-by: Ricardo Vega <github@noctusoft.com>
@rvegajr
rvegajr marked this pull request as ready for review October 2, 2026 01:53
@github-actions github-actions Bot added the needs-human Touches build/deploy config or guardrails; a human must merge label Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Auto-merge is off for this PR — a human must merge it.

title is marked [needs-human]
touches build/deploy-config paths:

  • pnpm-lock.yaml

Why: a green PR deploys to production and OTA-publishes to the TestFlight preview channel. Those are fine unattended. Changes to CI/deploy workflows, EAS config, native/app config, dependencies, Dockerfiles, Railway config, or the agent guardrails can trigger EAS builds (which burn build credits) or change where code ships, so they wait for a person. Review, then merge manually.

Co-authored-by: Ricardo Vega <github@noctusoft.com>
@rvegajr
rvegajr merged commit b0f795a into main Oct 3, 2026
17 checks passed
@rvegajr
rvegajr deleted the cursor/sms-compliance-289d branch October 3, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-human Touches build/deploy config or guardrails; a human must merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants