[object Object]

← back to Dw Pitch Followup

pitch: enable Send-now over the public tunnel via ALLOW_TUNNEL_SEND flag (Steve-approved) + Send button now shows 'send cancelled' so a dismissed confirm never looks dead

56a35bd6abf48679f36f254540f672221b4df0f6 · 2026-07-30 14:38:52 -0700 · steve

Files touched

Diff

commit 56a35bd6abf48679f36f254540f672221b4df0f6
Author: steve <steve@designerwallcoverings.com>
Date:   Thu Jul 30 14:38:52 2026 -0700

    pitch: enable Send-now over the public tunnel via ALLOW_TUNNEL_SEND flag (Steve-approved) + Send button now shows 'send cancelled' so a dismissed confirm never looks dead
---
 public/index.html |  2 +-
 server.js         | 10 +++++++++-
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/public/index.html b/public/index.html
index 5df8d61..46cd17c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -629,7 +629,7 @@ async function sendActive(btn){
   const stat=document.getElementById('pSendStat');
   if(!to){stat.style.color='var(--red)';stat.textContent=list==='list3'?'no vendor email on file':'no client email on file';return}
   if(!subj||!body){stat.style.color='var(--amber)';stat.textContent='generate a letter first';return}
-  if(!confirm(`Send this letter NOW?\n\nFROM:  info@designerwallcoverings.com\nTO:    ${to}\nSUBJ:  ${subj}\n\nThis emails the recipient immediately.`))return;
+  if(!confirm(`Send this letter NOW?\n\nFROM:  info@designerwallcoverings.com\nTO:    ${to}\nSUBJ:  ${subj}\n\nThis emails the recipient immediately.`)){stat.style.color='var(--amber)';stat.textContent='send cancelled';return}
   const old=btn.textContent; btn.disabled=true; btn.textContent='sending…'; stat.style.color='var(--mut)'; stat.textContent='';
   try{
     const payloadBase={to,subject:subj,body,samples:Array.isArray(r.samples_sent)?r.samples_sent:[],list,account:r.account,company:(list==='list3'?r.vendor:r.company)};
diff --git a/server.js b/server.js
index 280cf25..d6a3c9e 100644
--- a/server.js
+++ b/server.js
@@ -97,6 +97,13 @@ function envLocal(key) {
 const BASIC_AUTH = process.env.BASIC_AUTH || envLocal('BASIC_AUTH') || 'admin:DW2024!';
 const AUTH_HEADER = 'Basic ' + Buffer.from(BASIC_AUTH).toString('base64');
 const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
+// Allow /api/send over the PUBLIC cloudflared tunnel (Steve-approved 2026-07-30). Default OFF:
+// the tunnel-origin guard normally restricts send-as-info@ to a local-loopback operator because
+// Basic Auth alone is a weak gate for a send button on the open internet. Set ALLOW_TUNNEL_SEND=1
+// in .env to permit AUTHENTICATED remote sends — still behind Basic Auth + the brute-force lockout,
+// and every other send guard (suppression list, 30-day frequency, George external-send token,
+// recipient validation) is unchanged. Flip the .env flag back to '0' to re-lock instantly.
+const ALLOW_TUNNEL_SEND = (process.env.ALLOW_TUNNEL_SEND || envLocal('ALLOW_TUNNEL_SEND')) === '1';
 
 // Brute-force lockout (contrarian FIX FIRST #2): the public tunnel exposes Basic Auth,
 // which without a lockout is guessable at line speed. Track failed auths per client IP;
@@ -635,7 +642,8 @@ app.post('/api/send', async (req, res) => {
     // arrive from cloudflared (non-loopback socket + cf-connecting-ip); loopback callers
     // never carry those proxy headers. Header check kept as belt-and-suspenders.
     const sendRemote = req.socket && req.socket.remoteAddress;
-    if (!LOOPBACK.has(sendRemote) || req.get('cf-connecting-ip') || req.get('x-forwarded-for')) {
+    const isTunnelOrigin = !LOOPBACK.has(sendRemote) || req.get('cf-connecting-ip') || req.get('x-forwarded-for');
+    if (isTunnelOrigin && !ALLOW_TUNNEL_SEND) {
       return res.status(403).json({ error: 'Sending is disabled over the public tunnel. Open this tool locally to send — drafting and preview still work remotely.', tunnelBlocked: true });
     }
     const { to, subject, body, samples, list, account: acctNo, company } = req.body || {};

← 6330e75 pitch: signpost 9768 (own listen port) vs 9850 (GEORGE clien  ·  back to Dw Pitch Followup  ·  pitch: package.json description now reflects reality — notes b50e31f →