diff --git a/.build.hash b/.build.hash
index deeecf0..b81397b 100644
--- a/.build.hash
+++ b/.build.hash
@@ -1 +1 @@
-b5aad6117f63426726be6ae9a07e5aaa938f14ff-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
+33fb91d4b60b7f14d236f83e44c9db42fa1d440f-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
diff --git a/backend/server1.js b/backend/server1.js
index 9e5277f..9ea454e 100755
--- a/backend/server1.js
+++ b/backend/server1.js
@@ -811,14 +811,21 @@ app.post('/api/auth/verify/email/confirm', requireAuth, verifyConfirmLimiter, as
------------------------------------------------------------------ */
app.post('/api/auth/verify/phone/send', requireAuth, verifySendLimiter, async (req, res) => {
try {
- const { phone_e164 } = req.body || {};
+ const { phone_e164, consent } = req.body || {};
if (!phone_e164 || !/^\+1\d{10}$/.test(phone_e164)) {
return res.status(400).json({ error: 'Phone must be +1 followed by 10 digits' });
}
- // persist/overwrite phone on file
- await (pool.raw || pool).query('UPDATE user_profile SET phone_e164=? WHERE id=?', [phone_e164, req.userId]);
+ if (!consent) return res.status(400).json({ error: 'consent_required' });
+ /// persist phone + record explicit consent using existing flag
+ await (pool.raw || pool).query(
+ 'UPDATE user_profile SET phone_e164=?, sms_opt_in=1 WHERE id=?',
+ [phone_e164, req.userId]
+ );
const code = String(Math.floor(100000 + Math.random() * 900000));
- await sendSMS({ to: phone_e164, body: `AptivaAI security code: ${code}. Expires in 10 minutes.` });
+ await sendSMS({
+ to: phone_e164,
+ body: `AptivaAI code: ${code}. Expires in 10 minutes. Reply STOP to cancel, HELP for help.`
+ });
// store short-lived challenge in HttpOnly cookie (10 min)
const tok = jwt.sign({ sub: String(req.userId), prp: 'verify_phone', code }, JWT_SECRET, { expiresIn: '10m' });
@@ -852,7 +859,11 @@ app.post('/api/auth/verify/phone/confirm', requireAuth, verifyConfirmLimiter, as
res.clearCookie('aptiva_phone_vc', sessionCookieOptions());
return res.status(200).json({ ok: !!r?.affectedRows });
} catch (e) {
- console.error('[verify/phone/confirm]', e?.message || e);
+ if (String(e?.code) === '21610') { // Twilio: user replied STOP
+ try { await (pool.raw || pool).query('UPDATE user_profile SET sms_opt_in=0 WHERE id=?', [req.userId]); } catch {}
+ return res.status(409).json({ error: 'user_opted_out' });
+ }
+ console.error('[verify/phone/send]', e?.message || e);
return res.status(500).json({ error: 'Failed to confirm phone' });
}
});
diff --git a/backend/server3.js b/backend/server3.js
index 6903bcf..afda958 100644
--- a/backend/server3.js
+++ b/backend/server3.js
@@ -208,6 +208,9 @@ const EXEMPT_PATHS = [
// server3
/^\/api\/premium\/resume\/optimize$/, // multer (multipart/form-data)
/^\/api\/premium\/stripe\/webhook$/, // Stripe (express.raw)
+ // Twilio webhooks (form-encoded)
+ /^\/api\/auth\/sms\/inbound$/,
+ /^\/api\/auth\/sms\/status$/
// add others if truly needed
];
@@ -681,6 +684,30 @@ app.use(helmet({ contentSecurityPolicy:false, crossOriginEmbedderPolicy:false })
app.use(express.json({ limit: '5mb' }));
+// --- Twilio webhooks ---
+const twilioForm = express.urlencoded({ extended: false });
+
+app.post('/api/auth/sms/inbound', twilioForm, async (req, res) => {
+ const body = String(req.body?.Body || '').trim().toUpperCase();
+ if (body === 'HELP') {
+ const twiml = `AptivaAI: Help with SMS. Email admin@aptivaai.com. Msg&Data rates may apply. Reply STOP to cancel. `;
+ return res.type('text/xml').send(twiml);
+ }
+ return res.type('text/xml').send(` `);
+});
+
+app.post('/api/auth/sms/status', twilioForm, async (req, res) => {
+ try {
+ if (String(req.body?.ErrorCode || '') === '21610' && req.body?.To) {
+ await (pool.raw || pool).query('UPDATE user_profile SET sms_opt_in=0 WHERE phone_e164=?', [req.body.To]);
+ }
+ } catch (e) {
+ console.error('[sms/status]', e?.message || e);
+ }
+ res.sendStatus(204);
+});
+
+
//*PremiumOnboarding draft
// GET current user's draft
app.get('/api/premium/onboarding/draft', authenticatePremiumUser, async (req, res) => {
diff --git a/nginx.conf.bak b/nginx.conf.bak
index 201fae2..b25bc0a 100644
--- a/nginx.conf.bak
+++ b/nginx.conf.bak
@@ -1,10 +1,30 @@
-events {}
+worker_rlimit_nofile 131072;
+events { worker_connections 16384;
+ }
http {
- upstream backend5000 { server server1:5000; }
- upstream backend5001 { server server2:5001; }
- upstream backend5002 { server server3:5002; }
+ keepalive_requests 10000;
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+ resolver 127.0.0.11 ipv6=off;
+ limit_conn_zone $binary_remote_addr zone=perip:10m;
+ limit_req_zone $binary_remote_addr zone=reqperip:10m rate=100r/s;
+ set_real_ip_from 130.211.0.0/22;
+ set_real_ip_from 35.191.0.0/16;
+ real_ip_header X-Forwarded-For;
+ real_ip_recursive on;
+ # ───────────── upstreams to Docker services ─────────────
+ upstream backend5000 { server server1:5000; } # auth & free
+ upstream backend5001 { server server2:5001;
+ keepalive 1024;} # onet, distance, etc.
+ upstream backend5002 { server server3:5002; } # premium
+ upstream gitea_backend { server gitea:3000; } # gitea service (shared network)
+ upstream woodpecker_backend { server woodpecker-server:8000; }
+
+ ########################################################################
+ # 1. HTTP → HTTPS redirect for the main site
+ ########################################################################
server {
listen 80;
listen [::]:80;
@@ -12,78 +32,235 @@ http {
return 301 https://$host$request_uri;
}
+ ########################################################################
+ # 2. Main virtual host (dev1.aptivaai.com) on :443
+ ########################################################################
server {
listen 443 ssl;
+ http2 on;
+ http2_max_concurrent_streams 2048;
server_name dev1.aptivaai.com;
ssl_certificate /etc/letsencrypt/live/dev1.aptivaai.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dev1.aptivaai.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
- ssl_prefer_server_ciphers off;
- ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256';
- ssl_session_cache shared:SSL:10m;
- ssl_session_timeout 1h;
- error_log /var/log/nginx/error.log debug;
- access_log /var/log/nginx/access.log;
+ # ==== RUNTIME PROTECTIONS ====
+ server_tokens off;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+ add_header X-Content-Type-Options nosniff always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ add_header X-Frame-Options SAMEORIGIN always;
- # ---------- server1 (5000) ----------
- location /api/register { proxy_pass http://server1:5000/api/register; }
- location /api/check-username { proxy_pass http://server1:5000/api/check-username; }
- location /api/signin { proxy_pass http://server1:5000/api/signin; }
- location /api/login { proxy_pass http://server1:5000/api/login; }
- location /api/user-profile { proxy_pass http://server1:5000/api/user-profile; }
- location /api/areas { proxy_pass http://server1:5000/api/areas; }
- location /api/activate-premium{ proxy_pass http://server1:5000/api/activate-premium; }
+ client_max_body_size 10m;
+ large_client_header_buffers 4 8k;
- # ---------- server2 (5001) ----------
- location /api/onet/ { proxy_pass http://server2:5001; }
- location /api/onet/career-description/ { proxy_pass http://server2:5001; }
- location /api/job-zones { proxy_pass http://server2:5001/api/job-zones; }
- location /api/salary { proxy_pass http://server2:5001/api/salary; }
- location /api/cip/ { proxy_pass http://server2:5001/api/cip/; }
- location /api/tuition/ { proxy_pass http://server2:5001/api/tuition/; }
- location /api/projections/ { proxy_pass http://server2:5001/api/projections/; }
- location /api/skills/ { proxy_pass http://server2:5001/api/skills/; }
- location = /api/ai-risk { proxy_pass http://server2:5001/api/ai-risk; }
- location /api/ai-risk/ { proxy_pass http://server2:5001/api/ai-risk/; }
+ client_header_timeout 30s;
+ client_body_timeout 30s;
+ send_timeout 35s;
+ keepalive_timeout 65s;
- location /api/chat/ {
- proxy_pass http://server2:5001;
- proxy_http_version 1.1;
- proxy_buffering off;
- }
+ proxy_set_header X-Request-ID $request_id;
+ add_header X-Request-ID $request_id always;
+ proxy_request_buffering off;
+ proxy_max_temp_file_size 0;
+ proxy_buffer_size 16k;
+ proxy_buffers 8 16k;
+ proxy_busy_buffers_size 32k;
- location ^~ /api/maps/distance { proxy_pass http://server2:5001; }
- location /api/schools { proxy_pass http://server2:5001/api/schools; }
+ if ($request_method !~ ^(GET|POST|PUT|PATCH|DELETE|OPTIONS)$) { return 405; }
- # ---------- server3 (5002) ----------
- location ^~ /api/premium/ { proxy_pass http://server3:5002; }
- location /api/public/ { proxy_pass http://server3:5002/api/public/; }
+ if ($host !~* ^(dev1\.aptivaai\.com)$) { return 444; }
- # ---------- static React build ----------
+ location ~ /\.(?!well-known/) { deny all; }
+
+ location ~* \.(?:env|ini|log|sql|sqlite|db|db3|bak|old|orig|swp)$ { deny all; }
+
+ # ───── React static assets ─────
root /usr/share/nginx/html;
index index.html;
- location / { try_files $uri /index.html; }
+ # ======= REVIEW-ONLY SURFACE =======
+ # 1) Send root to the public A2P page
+ location = / { return 302 /a2p/; }
+
+ # 2) Allowlist public docs
+ location ^~ /a2p/ { try_files $uri $uri/ =404; } # /public/a2p/index.html
+ location = /sms { return 302 /sms/; }
+ location ^~ /sms/ { try_files $uri $uri/ =404; } # /public/sms/index.html
+ location ^~ /legal/ { try_files $uri $uri/ =404; } # /public/legal/.../index.html
+
+ # 3) Keep health + SMS webhooks working
+ location = /healthz { return 200 'ok'; add_header Content-Type text/plain; }
+ # (Your existing /api/auth/ proxy already covers /api/auth/sms/*)
+
+ # 4) TEMP: block SPA fallback so deep links don’t expose the app
+ location / {
+ try_files $uri $uri/ =404;
+ }
+
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg)$ {
expires 6M;
access_log off;
- add_header Cache-Control "public, max-age=31536000, immutable";
}
+ # ───── API reverse‑proxy rules ─────
+ location ^~ /api/onet/ {
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_read_timeout 90s;
+ proxy_connect_timeout 15s;
+ proxy_pass http://backend5001;
+ }
+ location ^~ /api/chat/ {
+ limit_conn perip 10;
+ limit_req zone=reqperip burst=20 nodelay;
+ proxy_pass http://backend5001;
+ proxy_http_version 1.1;
+ proxy_buffering off;
+ proxy_set_header Connection "";
+ }
+ location ^~ /api/job-zones {
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_read_timeout 90s;
+ proxy_connect_timeout 15s;
+ proxy_pass http://backend5001;
+ }
+ location ^~ /api/salary {
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_read_timeout 90s;
+ proxy_connect_timeout 15s;
+ proxy_pass http://backend5001;
+ }
+ location ^~ /api/cip/ {
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_read_timeout 90s;
+ proxy_connect_timeout 15s;
+ proxy_pass http://backend5001;
+ }
+ location ^~ /api/projections/ {
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_read_timeout 90s;
+ proxy_connect_timeout 15s;
+ proxy_pass http://backend5001;
+ }
+
+ location ^~ /api/tuition/ { proxy_pass http://backend5001; }
+ location ^~ /api/skills/ { proxy_pass http://backend5001; }
+ location ^~ /api/maps/distance { proxy_pass http://backend5001; }
+ location ^~ /api/schools { proxy_pass http://backend5001; }
+ location ^~ /api/support {
+ limit_conn perip 5;
+ limit_req zone=reqperip burst=10 nodelay;
+ proxy_pass http://backend5001;
+ }
+ location ^~ /api/data/ { proxy_pass http://backend5001; }
+ location ^~ /api/careers/ { proxy_pass http://backend5001; }
+ location ^~ /api/programs/ { proxy_pass http://backend5001; }
+
+ location ^~ /api/premium/ {
+ limit_conn perip 10;
+ limit_req zone=reqperip burst=20 nodelay;
+ proxy_pass http://backend5002;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ }
+ location ^~ /api/premium/stripe/webhook {
+ proxy_pass http://backend5002;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ }
+ location ^~ /api/public/ { proxy_pass http://backend5002; }
+ location ^~ /api/ai-risk { proxy_pass http://backend5002; }
+
+ location = /api/signin { limit_conn perip 5;
+ limit_req zone=reqperip burst=10 nodelay;
+ proxy_pass http://backend5000; }
+ location = /api/register { limit_conn perip 3;
+ limit_req zone=reqperip burst=5 nodelay;
+ proxy_pass http://backend5000; }
+ location ^~ /api/auth/ { limit_conn perip 5;
+ limit_req zone=reqperip burst=10 nodelay;
+ proxy_pass http://backend5000; }
+ location = /api/user-profile { limit_conn perip 5;
+ limit_req zone=reqperip burst=10 nodelay;
+ proxy_pass http://backend5000; }
+
+ # General API (anything not matched above) – rate-limited
+ location ^~ /api/ { proxy_pass http://backend5000; }
+
+ # shared proxy headers
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
error_page 502 503 504 /50x.html;
location = /50x.html { root /usr/share/nginx/html; }
}
-}
-http {
- upstream backend5000 { server server1:5000; }
- upstream backend5001 { server server2:5001; }
- upstream backend5002 { server server3:5002; }
- server {
- listen 80;
- location /api1/ { proxy_pass http://backend5000/; }
- location /api2/ { proxy_pass http://backend5001/; }
- location /api3/ { proxy_pass http://backend5002/; }
- }
+ ########################################################################
+ # 3. Gitea virtual host (HTTPS) gitea.dev1.aptivaai.com
+ ########################################################################
+ server {
+ listen 443 ssl;
+ http2 on;
+ server_name gitea.dev1.aptivaai.com;
+ client_max_body_size 1024m;
+
+ ssl_certificate /etc/letsencrypt/live/gitea.dev1.aptivaai.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/gitea.dev1.aptivaai.com/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ location / {
+ proxy_pass http://gitea_backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ }
+ }
+
+ ########################################################################
+ # 4. Gitea HTTP → HTTPS redirect
+ ########################################################################
+ server {
+ listen 80;
+ server_name gitea.dev1.aptivaai.com;
+ return 301 https://$host$request_uri;
+ }
+ ########################################################################
+# 5. Woodpecker CI – HTTPS ci.dev1.aptivaai.com
+########################################################################
+
+server {
+ listen 443 ssl;
+ http2 on;
+ server_name ci.dev1.aptivaai.com;
+
+ ssl_certificate /etc/letsencrypt/live/ci.dev1.aptivaai.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/ci.dev1.aptivaai.com/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ location / {
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_pass http://woodpecker_backend;
+ }
}
+
+########################################################################
+# 6. Woodpecker – HTTP → HTTPS redirect
+########################################################################
+server {
+ listen 80;
+ server_name ci.dev1.aptivaai.com;
+ return 301 https://$host$request_uri;
+}}
\ No newline at end of file
diff --git a/public/a2p/index.html b/public/a2p/index.html
new file mode 100644
index 0000000..cd4cc51
--- /dev/null
+++ b/public/a2p/index.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ AptivaAI – A2P 10DLC Campaign Verification
+
+
+
+ AptivaAI – A2P 10DLC Campaign Verification
+ Last updated: Sep 13, 2025
+
+ Program & Use Case
+
+
Program name: AptivaAI Reminders
+
Use case: Two-Factor Authentication (2FA) / account security only. No marketing or promotional content.
+
+ Frequency: varies by user activity
+ Fees: Message & data rates may apply
+ Opt-out: Reply STOP to cancel; HELP for help
+ Contact: support@aptivaai.com
+
+
+
+ Call to Action (CTA) & Consent
+ Users opt in inside their account by entering a mobile number, checking a non-prechecked consent box linking to our
+ SMS Terms , Privacy Policy , and
+ Terms , then requesting an SMS code. We verify number ownership with a one-time passcode before enabling SMS.
+
+
+
+
+ In-app CTA and consent (verify screen)
+
+
+
+ Public SMS Terms: fees, frequency, STOP/HELP, contact, links
+
+
+
+ Message Flow (2FA)
+
+ User toggles SMS and requests a code (after checking consent).
+ We send a one-time passcode; user enters it to verify the number.
+ User may reply STOP any time to opt out; START re-subscribes; HELP returns support info.
+
+
+ Sample Messages
+
+
All samples include brand and opt-out/help language; no shortened links or phone numbers in 2FA content.
+
+ AptivaAI code: 123456. Expires in 10 minutes. Reply STOP to cancel, HELP for help.
+ AptivaAI sign-in code: 654321. If you didn’t request this, change your password.
+ AptivaAI password reset code: 112233. Expires in 10 minutes. Reply STOP to cancel.
+
+
+
+ STOP/HELP Handling
+
+ STOP: Delivery is blocked and we mark the user opted-out. Users can re-enable via START or account settings.
+ HELP: We respond with: “AptivaAI: For help email support@aptivaai.com. Msg&Data rates may apply. Reply STOP to cancel.”
+
+
+ Public Policies
+
+
+ Screenshots for Vetting
+ High-resolution screenshots are hosted here for reviewers:
+
+
+
+ Consent checkbox (non-prechecked) linking to policies
+
+
+
+ Example OTP SMS
+
+
+
+ Carriers are not liable for delayed or undelivered messages.
+
+
diff --git a/public/legal/privacy/index.html b/public/legal/privacy/index.html
new file mode 100644
index 0000000..ac5d9a9
--- /dev/null
+++ b/public/legal/privacy/index.html
@@ -0,0 +1,14 @@
+
+
+
+Privacy – AptivaAI
+
+Privacy Policy
+We collect account information (name, email), an optional phone number to enable SMS, and usage data. We use this information to provide and secure the service, including sending SMS you enable (e.g., verification codes). We share data with processors under contracts that limit use to our instructions. You can opt out of SMS by replying STOP .
+Contact: support@aptivaai.com
+Last updated: Sep 13, 2025
diff --git a/public/legal/terms/index.html b/public/legal/terms/index.html
new file mode 100644
index 0000000..eeb5995
--- /dev/null
+++ b/public/legal/terms/index.html
@@ -0,0 +1,13 @@
+
+
+
+Terms – AptivaAI
+
+Terms of Service
+The service is provided “as is.” Liability is limited to fees paid in the prior 12 months. If you enable SMS, you consent to receive 2FA/security texts as described in the SMS Terms . Message & data rates may apply. Reply STOP to cancel, HELP for help.
+Last updated: Sep 13, 2025
diff --git a/public/sms-consent.html b/public/sms-consent.html
deleted file mode 100644
index 969ca10..0000000
--- a/public/sms-consent.html
+++ /dev/null
@@ -1,15 +0,0 @@
-SMS Consent & Opt-In Terms
-
-
-When you check the box “Send me SMS task-reminder texts (standard rates apply)” during Premium
-onboarding, you agree to receive recurring, automated text messages from AptivaAI at the phone number
-you provided. Message frequency depends on your task schedule. Message and data rates may apply.
-
-
-
-Reply STOP at any time to cancel, or HELP for help. You can also toggle SMS
-reminders off inside your account settings. For more details, see our
-Privacy Policy and Terms of Service .
-
-
-Questions? Email support@aptiva.com.
diff --git a/public/sms/index.html b/public/sms/index.html
new file mode 100644
index 0000000..67ff8ad
--- /dev/null
+++ b/public/sms/index.html
@@ -0,0 +1,20 @@
+
+
+
+AptivaAI SMS Terms
+
+SMS Consent & Opt-In Terms
+When you check the box “Send me SMS texts” in your account, you agree to receive account security and verification messages from AptivaAI at the number you provide. Message frequency varies. Message & data rates may apply.
+
+ Opt-in: Enable SMS in your account and verify your number.
+ Opt-out: Reply STOP to cancel. Reply HELP for help.
+ Support: support@aptivaai.com
+ Carriers: Not liable for delayed or undelivered messages.
+
+See our Privacy Policy and Terms of Service .
+Last updated: Sep 13, 2025
diff --git a/src/components/Verify.js b/src/components/Verify.js
index 53aca63..8bd46f9 100644
--- a/src/components/Verify.js
+++ b/src/components/Verify.js
@@ -45,10 +45,15 @@ export default function Verify() {
if (sendingSms) return;
setSendingSms(true);
try {
- await api.post('/api/auth/verify/phone/send', { phone_e164: phone });
+ await api.post('/api/auth/verify/phone/send', { phone_e164: phone, consent: smsConsent });
setMsg('SMS code sent.');
- } catch {
- setMsg('Could not send SMS code.');
+ } catch (e) {
+ const err = e?.response?.data?.error || '';
+ setMsg(err === 'consent_required'
+ ? 'Please check the consent box first.'
+ : err === 'user_opted_out'
+ ? 'This number opted out by replying STOP. Text START to re-enable, then try again.'
+ : 'Could not send SMS code.');
} finally {
// re-enable after 30s; avoids hammering the endpoint
setTimeout(() => setSendingSms(false), 30000);
@@ -120,10 +125,14 @@ export default function Verify() {
checked={smsConsent}
onChange={e => setSmsConsent(e.target.checked)}
/>
-
- By requesting a code, you agree to receive one-time security texts from AptivaAI to verify your account.
- Reply STOP to opt out. Msg & data rates may apply.
+
+ By requesting a code, you agree to the{' '}
+ SMS Terms ,{' '}
+ Privacy Policy ,{' '}
+ and Terms .{' '}
+ Reply STOP to opt out. Msg & data rates may apply.
+