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
32process.env.BRIVEN_AUTH_CORE_ENABLED = 'true';
33process.env.BRIVEN_ENV = 'development';
34process.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';
37process.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
41const { ensureBrivenEngineDatabase } = await import(
42 '../src/services/auth-core/ensure-db.ts'
43);
44const { initAuthCoreSdk } = await import('../src/services/auth-core/engine.ts');
45const {
46 getBrivenEngineProjectConfig,
47 setBrivenEngineMethodFlags,
48 setBrivenEngineSmsSecrets,
49} = await import('../src/services/auth-core/project-config.ts');
50const {
51 createPasswordlessCode,
52 consumePasswordlessCode,
53} = await import('../src/services/auth-core/passwordless.ts');
54const {
55 sendBrivenEngineSms,
56 sendBrivenEngineSmsTest,
57} = await import('../src/services/auth-core/delivery.ts');
58
59const projectId = `p_sms_polish_${Date.now().toString(36)}`;
60const phone = `+1555${String(Date.now()).slice(-7)}`;
61let failed = 0;
62
63function pass(label, detail) {
64 console.log(` ✔ ${label}`, detail ?? '');
65}
66
67function fail(label, detail) {
68 failed += 1;
69 console.error(` ✘ ${label}`, detail ?? '');
70}
71
72function 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. */
78function 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
94console.log('=== SMS polish proof ===');
95console.log({ projectId, phone });
96
97const ensured = await ensureBrivenEngineDatabase();
98if (!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}
105const inited = await initAuthCoreSdk();
106if (!inited) {
107 console.error('FAIL init auth-core');
108 process.exit(1);
109}
110
111// ── 1) Fresh project: SMS not configured ─────────────────────────────
112console.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 ──────────────────────────
128console.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 ───────────────────────────────
146console.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 ──────────────
173console.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 ─────────────────────────────────────
214console.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 ──────────────────────────────────────
235console.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 ────────────────────────────
246console.log('\n[7] mocked Twilio 401 → mode error');
247await 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 ────────────────────────────────
269console.log('\n[8] mocked Twilio 201 → mode provider');
270await 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 ─────────────────────────
287console.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) ─────────────────────────
301console.log('\n[10] optional live Twilio');
302const liveSid = process.env.BRIVEN_SMS_LIVE_SID?.trim();
303const liveToken = process.env.BRIVEN_SMS_LIVE_TOKEN?.trim();
304const liveFrom = process.env.BRIVEN_SMS_LIVE_FROM?.trim();
305const liveTo = process.env.BRIVEN_SMS_LIVE_TO?.trim();
306if (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 ──────────────────────────────────────────────────────────
327console.log('');
328if (failed > 0) {
329 console.error(`✘ SMS POLISH PROOF FAILED (${failed} assertion(s))`);
330 process.exit(1);
331}
332
333console.log('✔ SMS POLISH PROOF OK');
334console.log(' config ready only with SID+token+From');
335console.log(' method + secrets → ready; either missing → not ready');
336console.log(' no secrets → delivery ok=false mode=log');
337console.log(' Twilio fail → delivery ok=false mode=error');
338console.log(' Twilio ok (mocked) → delivery ok=true mode=provider');
339console.log(' passwordless create+consume works without real SMS');
340console.log(' test helper rejects non-E.164 phones');
341console.log('');
342console.log('Next: dashboard smoke (Providers + Security + test button),');
343console.log('then live Twilio when you say go, then deploy only with your OK.');
344process.exit(0);