← back to Costa Rica
costa-rica: harden boot guard per Cody gate — VERIFY_TOKEN + real-module tests — TK-10346
ddf9318975518fa0984d5714bbdec44583bea5f0 · 2026-08-08 08:18:40 -0700 · Steve
- Guard now also fails closed when WhatsApp is LIVE but WHATSAPP_VERIFY_TOKEN is missing/left at
the public default 'cr-verify-sandbox' (new whatsapp.verifyTokenSet getter). Verified end-to-end:
prod boot with live WhatsApp + default verify token now REFUSES to start.
- Added real env->module->guard INTEGRATION tests (fresh-require the actual tilopay/whatsapp modules,
assert liveMode/webhookSecretSet/verifyTokenSet reflect env, and that verifyWebhook rejects when
live+no-secret) — catches getter-rename/env-wiring regressions the pure-fake tests missed.
- Scoped the guard's comment honestly: it covers the webhook-secret/verify-token misconfig only,
NOT the live createCharge-ref / sig-encoding items (those stay in the go-live memo preflight).
- Cody HOLE-1 (runs after app.listen) was a PHANTOM: runPreflight is synchronous BEFORE app.listen;
if it throws the socket never binds. Dropped with evidence.
Suite 113/113.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M lib/preflight.jsM lib/whatsapp.jsM test/preflight.test.js
Diff
commit ddf9318975518fa0984d5714bbdec44583bea5f0
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Aug 8 08:18:40 2026 -0700
costa-rica: harden boot guard per Cody gate — VERIFY_TOKEN + real-module tests — TK-10346
- Guard now also fails closed when WhatsApp is LIVE but WHATSAPP_VERIFY_TOKEN is missing/left at
the public default 'cr-verify-sandbox' (new whatsapp.verifyTokenSet getter). Verified end-to-end:
prod boot with live WhatsApp + default verify token now REFUSES to start.
- Added real env->module->guard INTEGRATION tests (fresh-require the actual tilopay/whatsapp modules,
assert liveMode/webhookSecretSet/verifyTokenSet reflect env, and that verifyWebhook rejects when
live+no-secret) — catches getter-rename/env-wiring regressions the pure-fake tests missed.
- Scoped the guard's comment honestly: it covers the webhook-secret/verify-token misconfig only,
NOT the live createCharge-ref / sig-encoding items (those stay in the go-live memo preflight).
- Cody HOLE-1 (runs after app.listen) was a PHANTOM: runPreflight is synchronous BEFORE app.listen;
if it throws the socket never binds. Dropped with evidence.
Suite 113/113.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
lib/preflight.js | 21 +++++++++++++----
lib/whatsapp.js | 6 ++++-
test/preflight.test.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 82 insertions(+), 7 deletions(-)
diff --git a/lib/preflight.js b/lib/preflight.js
index e773e1a..8a31ff2 100644
--- a/lib/preflight.js
+++ b/lib/preflight.js
@@ -9,6 +9,11 @@
// In production this THROWS (a live money system that cannot verify webhooks must not
// serve — fail closed). In dev/sandbox it only warns. It is INERT when nothing is live,
// so it can never affect the current sandbox prod.
+//
+// SCOPE: this guards the webhook-secret / verify-token misconfig ONLY. It does NOT prove
+// the live charge path is correct — the go-live memo's LIVE-ONLY preflight (createCharge
+// providerRef=undefined guard, base64-vs-hex signature encoding) must still be checked
+// against the real provider. "Boot passed" != "safe to open bookings".
const SECRET_ENV = { tilopay: 'TILOPAY_WEBHOOK_SECRET', onvo: 'ONVO_WEBHOOK_SECRET' };
@@ -29,10 +34,18 @@ function checkWebhookSecrets({ getProvider, whatsapp } = {}) {
problems.push(`payment provider preflight could not resolve: ${e && e.message}`);
}
}
- if (whatsapp && whatsapp.liveMode && !whatsapp.webhookSecretSet) {
- problems.push(
- `WhatsApp is LIVE but WHATSAPP_APP_SECRET is missing — inbound webhooks would be silently rejected.`
- );
+ if (whatsapp && whatsapp.liveMode) {
+ if (!whatsapp.webhookSecretSet) {
+ problems.push(
+ `WhatsApp is LIVE but WHATSAPP_APP_SECRET is missing — inbound webhooks would be silently rejected.`
+ );
+ }
+ if (!whatsapp.verifyTokenSet) {
+ problems.push(
+ `WhatsApp is LIVE but WHATSAPP_VERIFY_TOKEN is missing or left at the public default ` +
+ `('cr-verify-sandbox') — set a real random token before registering the Meta webhook.`
+ );
+ }
}
return problems;
}
diff --git a/lib/whatsapp.js b/lib/whatsapp.js
index cafff53..8c834e5 100644
--- a/lib/whatsapp.js
+++ b/lib/whatsapp.js
@@ -150,7 +150,11 @@ async function handleInbound(body) {
}
module.exports = {
- get liveMode() { return LIVE; }, get webhookSecretSet() { return !!APP_SECRET; }, VERIFY_TOKEN,
+ get liveMode() { return LIVE; }, get webhookSecretSet() { return !!APP_SECRET; },
+ // true only when a REAL (non-default) verify token is configured — the hardcoded
+ // 'cr-verify-sandbox' fallback must not be used for a live WABA (public repo default).
+ get verifyTokenSet() { return !!(process.env.WHATSAPP_VERIFY_TOKEN && VERIFY_TOKEN !== 'cr-verify-sandbox'); },
+ VERIFY_TOKEN,
sendText, sendTemplate, sendButtons, sendList, sendImage, sendDocument, sendLocation, markRead,
verifyChallenge, verifySignature, handleInbound, contactByWaId,
};
diff --git a/test/preflight.test.js b/test/preflight.test.js
index 34f13a2..7dfecba 100644
--- a/test/preflight.test.js
+++ b/test/preflight.test.js
@@ -7,7 +7,7 @@ const { checkWebhookSecrets, runPreflight } = require('../lib/preflight');
const liveNoSecretPay = { name: 'tilopay', liveMode: true, webhookSecretSet: false };
const liveWithSecretPay = { name: 'tilopay', liveMode: true, webhookSecretSet: true };
const sandboxPay = { name: 'tilopay', liveMode: false, webhookSecretSet: false };
-const wa = (live, secret) => ({ liveMode: live, webhookSecretSet: secret });
+const wa = (live, secret, vtoken = true) => ({ liveMode: live, webhookSecretSet: secret, verifyTokenSet: vtoken });
test('checkWebhookSecrets: LIVE payment provider with NO webhook secret is flagged', () => {
const p = checkWebhookSecrets({ getProvider: () => liveNoSecretPay });
@@ -48,6 +48,64 @@ test('runPreflight: dev/sandbox only WARNS (returns problems, does not throw)',
assert.equal(out.length, 1); // problem surfaced (logged) but non-fatal off-prod
});
+test('checkWebhookSecrets: LIVE WhatsApp with the DEFAULT verify token is flagged', () => {
+ const p = checkWebhookSecrets({ whatsapp: { liveMode: true, webhookSecretSet: true, verifyTokenSet: false } });
+ assert.equal(p.length, 1);
+ assert.match(p[0], /WHATSAPP_VERIFY_TOKEN/);
+});
+
test('runPreflight: production with everything sandbox does NOT throw (guard is inert)', () => {
- assert.doesNotThrow(() => runPreflight({ getProvider: () => sandboxPay, whatsapp: wa(false, false), env: { NODE_ENV: 'production' } }));
+ assert.doesNotThrow(() => runPreflight({ getProvider: () => sandboxPay, whatsapp: { liveMode: false, webhookSecretSet: false, verifyTokenSet: false }, env: { NODE_ENV: 'production' } }));
+});
+
+// INTEGRATION: exercise the REAL env -> module property -> guard chain (not fakes), so a
+// getter rename (webhookSecretSet/liveMode) or an env-wiring regression is actually caught.
+function freshRequire(relPath, env) {
+ const p = require.resolve(relPath);
+ const saved = {}; for (const k of Object.keys(env)) { saved[k] = process.env[k]; process.env[k] = env[k]; }
+ delete require.cache[p];
+ // tilopay/onvo are pulled in by lib/payments/index — bust those too
+ for (const dep of ['../lib/payments/index', '../lib/payments/tilopay', '../lib/payments/onvo', '../lib/whatsapp']) {
+ try { delete require.cache[require.resolve(dep)]; } catch {}
+ }
+ const mod = require(relPath);
+ const restore = () => { for (const k of Object.keys(env)) { if (saved[k] === undefined) delete process.env[k]; else process.env[k] = saved[k]; } };
+ return { mod, restore };
+}
+
+test('INTEGRATION: real tilopay module — LIVE creds + webhook secret => guard clean', () => {
+ const { mod, restore } = freshRequire('../lib/payments/tilopay',
+ { TILOPAY_API_USER: 'u', TILOPAY_API_PASSWORD: 'p', TILOPAY_API_KEY: 'k', TILOPAY_WEBHOOK_SECRET: 's' });
+ try {
+ assert.equal(mod.liveMode, true, 'real module should read LIVE from env');
+ assert.equal(mod.webhookSecretSet, true, 'real module should report secret set');
+ assert.deepEqual(checkWebhookSecrets({ getProvider: () => mod }), []);
+ } finally { restore(); }
+});
+
+test('INTEGRATION: real tilopay module — LIVE creds + NO webhook secret => guard flags it (the exact original bug)', () => {
+ const { mod, restore } = freshRequire('../lib/payments/tilopay',
+ { TILOPAY_API_USER: 'u', TILOPAY_API_PASSWORD: 'p', TILOPAY_API_KEY: 'k', TILOPAY_WEBHOOK_SECRET: '' });
+ try {
+ assert.equal(mod.liveMode, true);
+ assert.equal(mod.webhookSecretSet, false);
+ // and the runtime symptom the guard exists to prevent: verifyWebhook rejects when live+no-secret
+ assert.equal(mod.verifyWebhook({}, '{}').ok, false, 'live + no secret must reject the webhook');
+ const p = checkWebhookSecrets({ getProvider: () => mod });
+ assert.equal(p.length, 1);
+ assert.match(p[0], /TILOPAY_WEBHOOK_SECRET/);
+ } finally { restore(); }
+});
+
+test('INTEGRATION: real whatsapp module — LIVE + default verify token => guard flags VERIFY_TOKEN', () => {
+ const { mod, restore } = freshRequire('../lib/whatsapp',
+ { WHATSAPP_TOKEN: 't', WHATSAPP_PHONE_ID: 'pid', WHATSAPP_APP_SECRET: 'sec' }); // no WHATSAPP_VERIFY_TOKEN
+ try {
+ assert.equal(mod.liveMode, true);
+ assert.equal(mod.webhookSecretSet, true);
+ assert.equal(mod.verifyTokenSet, false, 'default verify token must not count as set');
+ const p = checkWebhookSecrets({ whatsapp: mod });
+ assert.equal(p.length, 1);
+ assert.match(p[0], /WHATSAPP_VERIFY_TOKEN/);
+ } finally { restore(); }
});
← af93fb1 costa-rica: fail-closed boot guard — refuse prod boot if a l
·
back to Costa Rica
·
costa-rica: /yoloforever cycle 2 ledger — TK-10346 86510e3 →