sms-polish-proof.mjs344 lines · main
| 1 | /** |
| 2 | * SMS polish proof (steps 1–5 of product polish). |
| 3 | * |
| 4 | * Checks, without requiring a real Twilio account: |
| 5 | * 1) Config: SMS “ready” only when SID + token + From are all saved |
| 6 | * 2) Methods: passwordlessSms on/off + ready = method AND secrets |
| 7 | * 3) Honest delivery: no secrets → ok:false mode:log |
| 8 | * 4) Honest delivery: bad Twilio → ok:false mode:error (mocked HTTP) |
| 9 | * 5) Success path → ok:true mode:provider (mocked HTTP 201) |
| 10 | * 6) Passwordless create+consume still works when SMS only logged |
| 11 | * 7) Test helper rejects bad phone numbers |
| 12 | * |
| 13 | * Optional live Twilio (step 6 of the plan — only if YOU set env): |
| 14 | * BRIVEN_SMS_LIVE_SID / BRIVEN_SMS_LIVE_TOKEN / BRIVEN_SMS_LIVE_FROM / BRIVEN_SMS_LIVE_TO |
| 15 | * When all four are set, one real test SMS is sent (costs Twilio credit). |
| 16 | * |
| 17 | * Run: |
| 18 | * cd apps/api |
| 19 | * BRIVEN_ENGINE_DATABASE_URL=postgres://postgres:devpass@127.0.0.1:5434/briven_engine?sslmode=disable \ |
| 20 | * BRIVEN_DATA_PLANE_URL=postgres://postgres:devpass@127.0.0.1:5434/postgres?sslmode=disable \ |
| 21 | * BRIVEN_AUTH_CORE_ENABLED=true BRIVEN_ENV=development \ |
| 22 | * bun scripts/sms-polish-proof.mjs |
| 23 | * |
| 24 | * Human checklist after this script is green: |
| 25 | * [ ] Dashboard → project → Providers → SMS card shows ready/not set |
| 26 | * [ ] Security → SMS block matches Providers |
| 27 | * [ ] Save Twilio secrets → SMS ready |
| 28 | * [ ] Send test SMS to your phone (or live env above) |
| 29 | * [ ] Deploy only when you say ship |
| 30 | */ |
| 31 | |
| 32 | process.env.BRIVEN_AUTH_CORE_ENABLED = 'true'; |
| 33 | process.env.BRIVEN_ENV = 'development'; |
| 34 | process.env.BRIVEN_ENGINE_DATABASE_URL = |
| 35 | process.env.BRIVEN_ENGINE_DATABASE_URL ?? |
| 36 | 'postgres://postgres:devpass@127.0.0.1:5434/briven_engine?sslmode=disable'; |
| 37 | process.env.BRIVEN_DATA_PLANE_URL = |
| 38 | process.env.BRIVEN_DATA_PLANE_URL ?? |
| 39 | 'postgres://postgres:devpass@127.0.0.1:5434/postgres?sslmode=disable'; |
| 40 | |
| 41 | const { ensureBrivenEngineDatabase } = await import( |
| 42 | '../src/services/auth-core/ensure-db.ts' |
| 43 | ); |
| 44 | const { initAuthCoreSdk } = await import('../src/services/auth-core/engine.ts'); |
| 45 | const { |
| 46 | getBrivenEngineProjectConfig, |
| 47 | setBrivenEngineMethodFlags, |
| 48 | setBrivenEngineSmsSecrets, |
| 49 | } = await import('../src/services/auth-core/project-config.ts'); |
| 50 | const { |
| 51 | createPasswordlessCode, |
| 52 | consumePasswordlessCode, |
| 53 | } = await import('../src/services/auth-core/passwordless.ts'); |
| 54 | const { |
| 55 | sendBrivenEngineSms, |
| 56 | sendBrivenEngineSmsTest, |
| 57 | } = await import('../src/services/auth-core/delivery.ts'); |
| 58 | |
| 59 | const projectId = `p_sms_polish_${Date.now().toString(36)}`; |
| 60 | const phone = `+1555${String(Date.now()).slice(-7)}`; |
| 61 | let failed = 0; |
| 62 | |
| 63 | function pass(label, detail) { |
| 64 | console.log(` ✔ ${label}`, detail ?? ''); |
| 65 | } |
| 66 | |
| 67 | function fail(label, detail) { |
| 68 | failed += 1; |
| 69 | console.error(` ✘ ${label}`, detail ?? ''); |
| 70 | } |
| 71 | |
| 72 | function assert(cond, label, detail) { |
| 73 | if (cond) pass(label, detail); |
| 74 | else fail(label, detail); |
| 75 | } |
| 76 | |
| 77 | /** Mock only api.twilio.com so proof works offline. */ |
| 78 | function withMockedTwilio(handler, run) { |
| 79 | const realFetch = globalThis.fetch; |
| 80 | globalThis.fetch = async (url, init) => { |
| 81 | const u = String(url); |
| 82 | if (u.includes('api.twilio.com')) { |
| 83 | return handler(u, init); |
| 84 | } |
| 85 | return realFetch(url, init); |
| 86 | }; |
| 87 | return Promise.resolve() |
| 88 | .then(run) |
| 89 | .finally(() => { |
| 90 | globalThis.fetch = realFetch; |
| 91 | }); |
| 92 | } |
| 93 | |
| 94 | console.log('=== SMS polish proof ==='); |
| 95 | console.log({ projectId, phone }); |
| 96 | |
| 97 | const ensured = await ensureBrivenEngineDatabase(); |
| 98 | if (!ensured.ok) { |
| 99 | console.error('FAIL ensure DB', ensured); |
| 100 | console.error( |
| 101 | 'Start local Doltgres (port 5434) then re-run. See compose.briven-engine.local.yml', |
| 102 | ); |
| 103 | process.exit(1); |
| 104 | } |
| 105 | const inited = await initAuthCoreSdk(); |
| 106 | if (!inited) { |
| 107 | console.error('FAIL init auth-core'); |
| 108 | process.exit(1); |
| 109 | } |
| 110 | |
| 111 | // ── 1) Fresh project: SMS not configured ───────────────────────────── |
| 112 | console.log('\n[1] config without secrets'); |
| 113 | { |
| 114 | const cfg = await getBrivenEngineProjectConfig(projectId); |
| 115 | assert( |
| 116 | cfg.delivery.sms.configured === false, |
| 117 | 'sms.configured is false', |
| 118 | cfg.delivery.sms, |
| 119 | ); |
| 120 | assert( |
| 121 | cfg.delivery.sms.provider === null, |
| 122 | 'sms.provider is null', |
| 123 | cfg.delivery.sms.provider, |
| 124 | ); |
| 125 | } |
| 126 | |
| 127 | // ── 2) Method on but no secrets → not ready ────────────────────────── |
| 128 | console.log('\n[2] passwordlessSms method vs secrets'); |
| 129 | { |
| 130 | await setBrivenEngineMethodFlags(projectId, { passwordlessSms: true }); |
| 131 | const cfg = await getBrivenEngineProjectConfig(projectId); |
| 132 | assert(cfg.methods.passwordlessSms === true, 'method passwordlessSms on'); |
| 133 | assert( |
| 134 | cfg.delivery.sms.configured === false, |
| 135 | 'still not configured without secrets', |
| 136 | ); |
| 137 | const chip = cfg.methodChips.find((c) => c.id === 'passwordless-sms'); |
| 138 | assert(chip?.enabled === true, 'chip enabled'); |
| 139 | assert(chip?.configured === false, 'chip configured=false (no Twilio)'); |
| 140 | const ready = |
| 141 | cfg.methods.passwordlessSms && cfg.delivery.sms.configured; |
| 142 | assert(ready === false, 'passwordlessSmsReady false'); |
| 143 | } |
| 144 | |
| 145 | // ── 3) Honest delivery without secrets ─────────────────────────────── |
| 146 | console.log('\n[3] delivery without secrets (honest fail)'); |
| 147 | { |
| 148 | const sent = await sendBrivenEngineSms({ |
| 149 | phoneNumber: phone, |
| 150 | userInputCode: '123456', |
| 151 | projectId, |
| 152 | type: 'PASSWORDLESS_LOGIN', |
| 153 | }); |
| 154 | assert(sent.ok === false, 'ok=false without secrets', sent); |
| 155 | assert(sent.mode === 'log', 'mode=log without secrets', sent.mode); |
| 156 | assert( |
| 157 | typeof sent.message === 'string' && |
| 158 | sent.message.toLowerCase().includes('sms not set'), |
| 159 | 'message mentions SMS not set', |
| 160 | sent.message, |
| 161 | ); |
| 162 | |
| 163 | const noProject = await sendBrivenEngineSms({ |
| 164 | phoneNumber: phone, |
| 165 | userInputCode: '123456', |
| 166 | type: 'PASSWORDLESS_LOGIN', |
| 167 | }); |
| 168 | assert(noProject.ok === false, 'ok=false without projectId', noProject); |
| 169 | assert(noProject.mode === 'log', 'mode=log without projectId', noProject.mode); |
| 170 | } |
| 171 | |
| 172 | // ── 4) Passwordless create still works; delivery honest ────────────── |
| 173 | console.log('\n[4] passwordless create+consume (delivery may be log)'); |
| 174 | { |
| 175 | const created = await createPasswordlessCode({ |
| 176 | phoneNumber: phone, |
| 177 | projectId, |
| 178 | flowType: 'USER_INPUT_CODE', |
| 179 | }); |
| 180 | assert(created.status === 'OK', 'create status OK', created); |
| 181 | assert( |
| 182 | created.status === 'OK' && created.channel === 'sms', |
| 183 | 'channel=sms', |
| 184 | created.status === 'OK' ? created.channel : null, |
| 185 | ); |
| 186 | assert( |
| 187 | created.status === 'OK' && created.delivery?.ok === false, |
| 188 | 'delivery.ok false (no Twilio yet)', |
| 189 | created.status === 'OK' ? created.delivery : null, |
| 190 | ); |
| 191 | assert( |
| 192 | created.status === 'OK' && Boolean(created.userInputCode), |
| 193 | 'dev userInputCode present', |
| 194 | ); |
| 195 | |
| 196 | if (created.status === 'OK' && created.userInputCode) { |
| 197 | const consumed = await consumePasswordlessCode({ |
| 198 | preAuthSessionId: created.preAuthSessionId, |
| 199 | deviceId: created.deviceId, |
| 200 | userInputCode: created.userInputCode, |
| 201 | projectId, |
| 202 | }); |
| 203 | assert(consumed.status === 'OK', 'consume OK even when SMS only logged', { |
| 204 | status: consumed.status, |
| 205 | user: |
| 206 | consumed.status === 'OK' |
| 207 | ? { id: consumed.user.id, phone: consumed.user.phone } |
| 208 | : null, |
| 209 | }); |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | // ── 5) Save secrets → configured ───────────────────────────────────── |
| 214 | console.log('\n[5] save Twilio-compatible secrets'); |
| 215 | { |
| 216 | await setBrivenEngineSmsSecrets(projectId, { |
| 217 | accountSid: 'ACffffffffffffffffffffffffffffffff', |
| 218 | authToken: 'test_auth_token_not_real', |
| 219 | fromNumber: '+15550001111', |
| 220 | }); |
| 221 | const cfg = await getBrivenEngineProjectConfig(projectId); |
| 222 | assert(cfg.delivery.sms.configured === true, 'sms.configured true', cfg.delivery.sms); |
| 223 | assert( |
| 224 | cfg.delivery.sms.provider === 'twilio-compatible', |
| 225 | 'provider twilio-compatible', |
| 226 | ); |
| 227 | const chip = cfg.methodChips.find((c) => c.id === 'passwordless-sms'); |
| 228 | assert(chip?.configured === true, 'chip configured=true'); |
| 229 | const ready = |
| 230 | cfg.methods.passwordlessSms && cfg.delivery.sms.configured; |
| 231 | assert(ready === true, 'passwordlessSmsReady true'); |
| 232 | } |
| 233 | |
| 234 | // ── 6) Bad phone on test helper ────────────────────────────────────── |
| 235 | console.log('\n[6] test helper rejects bad phone'); |
| 236 | { |
| 237 | const bad = await sendBrivenEngineSmsTest({ |
| 238 | projectId, |
| 239 | phoneNumber: '5551234', |
| 240 | }); |
| 241 | assert(bad.ok === false, 'bad phone ok=false', bad); |
| 242 | assert(bad.mode === 'error', 'bad phone mode=error', bad.mode); |
| 243 | } |
| 244 | |
| 245 | // ── 7) Mock Twilio failure → honest error ──────────────────────────── |
| 246 | console.log('\n[7] mocked Twilio 401 → mode error'); |
| 247 | await withMockedTwilio( |
| 248 | async () => |
| 249 | new Response(JSON.stringify({ message: 'Authenticate', code: 20003 }), { |
| 250 | status: 401, |
| 251 | headers: { 'content-type': 'application/json' }, |
| 252 | }), |
| 253 | async () => { |
| 254 | const sent = await sendBrivenEngineSmsTest({ |
| 255 | projectId, |
| 256 | phoneNumber: phone, |
| 257 | }); |
| 258 | assert(sent.ok === false, 'Twilio fail ok=false', sent); |
| 259 | assert(sent.mode === 'error', 'Twilio fail mode=error', sent.mode); |
| 260 | assert( |
| 261 | typeof sent.message === 'string' && sent.message.includes('401'), |
| 262 | 'message includes provider status', |
| 263 | sent.message, |
| 264 | ); |
| 265 | }, |
| 266 | ); |
| 267 | |
| 268 | // ── 8) Mock Twilio success → provider ──────────────────────────────── |
| 269 | console.log('\n[8] mocked Twilio 201 → mode provider'); |
| 270 | await withMockedTwilio( |
| 271 | async () => |
| 272 | new Response(JSON.stringify({ sid: 'SM_mock_success', status: 'queued' }), { |
| 273 | status: 201, |
| 274 | headers: { 'content-type': 'application/json' }, |
| 275 | }), |
| 276 | async () => { |
| 277 | const sent = await sendBrivenEngineSmsTest({ |
| 278 | projectId, |
| 279 | phoneNumber: phone, |
| 280 | }); |
| 281 | assert(sent.ok === true, 'Twilio success ok=true', sent); |
| 282 | assert(sent.mode === 'provider', 'Twilio success mode=provider', sent.mode); |
| 283 | }, |
| 284 | ); |
| 285 | |
| 286 | // ── 9) Method off with secrets → ready false ───────────────────────── |
| 287 | console.log('\n[9] method off → ready false even with secrets'); |
| 288 | { |
| 289 | await setBrivenEngineMethodFlags(projectId, { passwordlessSms: false }); |
| 290 | const cfg = await getBrivenEngineProjectConfig(projectId); |
| 291 | assert(cfg.delivery.sms.configured === true, 'secrets still saved'); |
| 292 | assert(cfg.methods.passwordlessSms === false, 'method off'); |
| 293 | const ready = |
| 294 | cfg.methods.passwordlessSms && cfg.delivery.sms.configured; |
| 295 | assert(ready === false, 'ready false when method off'); |
| 296 | // restore for any follow-on |
| 297 | await setBrivenEngineMethodFlags(projectId, { passwordlessSms: true }); |
| 298 | } |
| 299 | |
| 300 | // ── 10) Optional real Twilio (only with env) ───────────────────────── |
| 301 | console.log('\n[10] optional live Twilio'); |
| 302 | const liveSid = process.env.BRIVEN_SMS_LIVE_SID?.trim(); |
| 303 | const liveToken = process.env.BRIVEN_SMS_LIVE_TOKEN?.trim(); |
| 304 | const liveFrom = process.env.BRIVEN_SMS_LIVE_FROM?.trim(); |
| 305 | const liveTo = process.env.BRIVEN_SMS_LIVE_TO?.trim(); |
| 306 | if (liveSid && liveToken && liveFrom && liveTo) { |
| 307 | const liveProject = `p_sms_live_${Date.now().toString(36)}`; |
| 308 | await setBrivenEngineSmsSecrets(liveProject, { |
| 309 | accountSid: liveSid, |
| 310 | authToken: liveToken, |
| 311 | fromNumber: liveFrom, |
| 312 | }); |
| 313 | const live = await sendBrivenEngineSmsTest({ |
| 314 | projectId: liveProject, |
| 315 | phoneNumber: liveTo, |
| 316 | }); |
| 317 | assert(live.ok === true, 'LIVE test SMS sent', live); |
| 318 | assert(live.mode === 'provider', 'LIVE mode=provider', live.mode); |
| 319 | console.log(' (check phone', liveTo, 'for Briven Auth test message)'); |
| 320 | } else { |
| 321 | console.log( |
| 322 | ' · skipped (set BRIVEN_SMS_LIVE_SID, _TOKEN, _FROM, _TO to send a real text)', |
| 323 | ); |
| 324 | } |
| 325 | |
| 326 | // ── Summary ────────────────────────────────────────────────────────── |
| 327 | console.log(''); |
| 328 | if (failed > 0) { |
| 329 | console.error(`✘ SMS POLISH PROOF FAILED (${failed} assertion(s))`); |
| 330 | process.exit(1); |
| 331 | } |
| 332 | |
| 333 | console.log('✔ SMS POLISH PROOF OK'); |
| 334 | console.log(' config ready only with SID+token+From'); |
| 335 | console.log(' method + secrets → ready; either missing → not ready'); |
| 336 | console.log(' no secrets → delivery ok=false mode=log'); |
| 337 | console.log(' Twilio fail → delivery ok=false mode=error'); |
| 338 | console.log(' Twilio ok (mocked) → delivery ok=true mode=provider'); |
| 339 | console.log(' passwordless create+consume works without real SMS'); |
| 340 | console.log(' test helper rejects non-E.164 phones'); |
| 341 | console.log(''); |
| 342 | console.log('Next: dashboard smoke (Providers + Security + test button),'); |
| 343 | console.log('then live Twilio when you say go, then deploy only with your OK.'); |
| 344 | process.exit(0); |