sms-polish-proof.mjs 13 KB

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