[object Object]

← back to Sample Followup Sweep

Add fail-closed sample follow-up compliance gate

c74ebf76b7109a13ae4d8081f32d31a5d9dc0855 · 2026-08-28 11:44:03 -0700 · Steve Abrams

Files touched

Diff

commit c74ebf76b7109a13ae4d8081f32d31a5d9dc0855
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 28 11:44:03 2026 -0700

    Add fail-closed sample follow-up compliance gate
---
 README.md                  |  8 ++++++++
 lib/compose.js             |  2 ++
 lib/pre-send-gate.js       | 24 ++++++++++++++++++++++++
 scripts/scheduled-run.mjs  |  3 +++
 scripts/send-drafts.js     | 13 ++++++++++---
 server.js                  |  6 ++++++
 test/pre-send-gate.test.js | 30 ++++++++++++++++++++++++++++++
 7 files changed, 83 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 5720ae5..6fe84de 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,14 @@ Canary vendor: **Designers Guild @ Osborne And Little** (O&L USA, acct 1433803).
 | 5 Send | **human gate** — you batch-send from info@ Drafts | manual |
 | 6 Stamp | `2nd Request = today` via `fm_update_record` dry-run→commit | needs `API_SampleMemos` layout (else push the FileMaker button) |
 
+## Fail-closed pre-send compliance gate
+
+Every automated draft/send path calls `lib/pre-send-gate.js` before George. It
+blocks unless the sender resolves to `info@designerwallcoverings.com`, the body
+contains DW's physical postal address and a working opt-out link, and the subject
+is present without a misleading `Re:`/`Fwd:` prefix. A block performs no network
+request and returns `PRE_SEND_COMPLIANCE_BLOCKED` (HTTP 422 in the console API).
+
 ## The one FileMaker dependency (for hands-free phases 1 & 6)
 
 Create a Data-API-enabled list layout on **`WALLPAPER2`** named `API_SampleMemos`
diff --git a/lib/compose.js b/lib/compose.js
index dce694f..fff9a4a 100644
--- a/lib/compose.js
+++ b/lib/compose.js
@@ -46,6 +46,8 @@ ${esc(s.phone)}
 </p>
 <p>We truly appreciate the help.</p>
 <p>Best Regards,<br>Showroom Manager</p>
+<p style="font-size:11px;color:#666">Designer Wallcoverings · 15442 Ventura Blvd. #102 · Sherman Oaks, CA 91403<br>
+To stop receiving sample follow-up emails, <a href="mailto:info@designerwallcoverings.com?subject=Unsubscribe%20from%20sample%20follow-ups">unsubscribe here</a>.</p>
 </div>`;
 
   return { to, subject, html };
diff --git a/lib/pre-send-gate.js b/lib/pre-send-gate.js
new file mode 100644
index 0000000..257ce45
--- /dev/null
+++ b/lib/pre-send-gate.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const REQUIRED_FROM = 'info@designerwallcoverings.com';
+const ADDRESS_PATTERN = /15442\s+Ventura\s+Blvd(?:\.|\s)*(?:#|Suite)?\s*102[\s\S]*Sherman\s+Oaks,?\s*CA\s+91403/i;
+const UNSUBSCRIBE_PATTERN = /(?:mailto:|https?:\/\/)[^"'\s>]*(?:unsubscribe|opt[-_ ]?out)|(?:unsubscribe|opt\s*out)[\s\S]{0,160}(?:mailto:|https?:\/\/)/i;
+const MISLEADING_SUBJECT_PATTERN = /\b(?:re|fw|fwd)\s*:/i;
+
+function assertPreSendCompliance({ from, subject, body }) {
+  const errors = [];
+  if (String(from || '').trim().toLowerCase() !== REQUIRED_FROM) errors.push(`From must be ${REQUIRED_FROM}`);
+  if (!String(subject || '').trim()) errors.push('Subject is required');
+  if (MISLEADING_SUBJECT_PATTERN.test(String(subject || ''))) errors.push('Subject must not imply a reply or forward');
+  if (!ADDRESS_PATTERN.test(String(body || ''))) errors.push('Body must include the DW physical postal address');
+  if (!UNSUBSCRIBE_PATTERN.test(String(body || ''))) errors.push('Body must include a working unsubscribe or opt-out link');
+  if (errors.length) {
+    const error = new Error(`PRE-SEND COMPLIANCE GATE BLOCKED: ${errors.join('; ')}`);
+    error.code = 'PRE_SEND_COMPLIANCE_BLOCKED';
+    error.reasons = errors;
+    throw error;
+  }
+  return true;
+}
+
+module.exports = { assertPreSendCompliance, REQUIRED_FROM };
diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index 4d4d1c3..7de4ce1 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -27,6 +27,7 @@ const require = createRequire(import.meta.url);
 const __dir = dirname(fileURLToPath(import.meta.url));
 const ROOT = join(__dir, '..');
 const { compose } = require(join(ROOT, 'lib', 'compose.js'));
+const { assertPreSendCompliance, REQUIRED_FROM } = require(join(ROOT, 'lib', 'pre-send-gate.js'));
 
 const SEND = process.argv.includes('--send');
 // Steve 8/20: DRAFT mode — create a Gmail draft per vendor in info@ Drafts for human review,
@@ -108,6 +109,7 @@ function georgeCreds() {
   return { auth: 'Basic ' + Buffer.from(auth).toString('base64'), token: g('GEORGE_EXTERNAL_SEND_TOKEN') };
 }
 function georgeSend(payload) {
+  assertPreSendCompliance({ from: payload.account === 'info' ? REQUIRED_FROM : '', subject: payload.subject, body: payload.body });
   const { auth, token } = georgeCreds();
   const body = JSON.stringify(payload);
   return new Promise((resolve) => {
@@ -120,6 +122,7 @@ function georgeSend(payload) {
 }
 // Create a Gmail draft in info@ Drafts (POST /api/drafts — no send-approval token needed).
 function georgeDraft(payload) {
+  assertPreSendCompliance({ from: payload.account === 'info' ? REQUIRED_FROM : '', subject: payload.subject, body: payload.body });
   const { auth } = georgeCreds();
   const body = JSON.stringify(payload);
   return new Promise((resolve) => {
diff --git a/scripts/send-drafts.js b/scripts/send-drafts.js
index ecf4010..ea798c2 100644
--- a/scripts/send-drafts.js
+++ b/scripts/send-drafts.js
@@ -5,6 +5,7 @@
 const fs = require('fs');
 const http = require('http');
 const path = require('path');
+const { assertPreSendCompliance, REQUIRED_FROM } = require('../lib/pre-send-gate');
 
 const GENV = '/Users/macstudio3/Projects/george-gmail/.env';
 function env(k) { try { const m = fs.readFileSync(GENV, 'utf8').match(new RegExp('^' + k + '=(.+)$', 'm')); return m ? m[1].trim() : ''; } catch (e) { return ''; } }
@@ -19,13 +20,19 @@ const drafts = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'out', 'all
 
 function send(d) {
   return new Promise((res) => {
-    const payload = JSON.stringify({ account: 'info', to: d.to, subject: d.subject, body: d.body });
+    const payload = { account: 'info', to: d.to, subject: d.subject, body: d.body };
+    try {
+      assertPreSendCompliance({ from: payload.account === 'info' ? REQUIRED_FROM : '', subject: payload.subject, body: payload.body });
+    } catch (error) {
+      return res({ code: 0, body: JSON.stringify({ success: false, error: error.message }) });
+    }
+    const wirePayload = JSON.stringify(payload);
     const req = http.request({
       host: '127.0.0.1', port: 9850, path: '/api/send', method: 'POST',
-      headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), Authorization: basic, 'X-Send-Approval': token },
+      headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(wirePayload), Authorization: basic, 'X-Send-Approval': token },
     }, r => { let b = ''; r.on('data', x => b += x); r.on('end', () => res({ code: r.statusCode, body: b })); });
     req.on('error', e => res({ code: 0, body: e.message }));
-    req.write(payload); req.end();
+    req.write(wirePayload); req.end();
   });
 }
 
diff --git a/server.js b/server.js
index 8da9faf..de69a0f 100644
--- a/server.js
+++ b/server.js
@@ -9,6 +9,7 @@ const fs = require('fs');
 const path = require('path');
 const { spawn } = require('child_process');
 const { compose } = require('./lib/compose');
+const { assertPreSendCompliance, REQUIRED_FROM } = require('./lib/pre-send-gate');
 
 const ROOT = __dirname;
 const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
@@ -135,6 +136,11 @@ const server = http.createServer(async (req, res) => {
       const addrs = String(payload.to).split(',').map(a => a.trim().toLowerCase()).filter(a => a.includes('@'));
       if (addrs.length && addrs.every(a => be[a])) return send(res, 409, { error: 'already sent to this vendor — click again to force-resend', to: payload.to, alreadySent: true });
     }
+    try {
+      assertPreSendCompliance({ from: payload.account === 'info' ? REQUIRED_FROM : '', subject: payload.subject, body: payload.body });
+    } catch (error) {
+      return send(res, 422, { error: error.message, reasons: error.reasons || [] });
+    }
     // George's canonical creds live in the DW-MCP .env (the file George's server loads into `creds`);
     // GEORGE_EXTERNAL_SEND_TOKEN lives in george-gmail/.env. Search both, canonical first. (2026-08-15 fix:
     // george-gmail/.env has no GEORGE_BASIC_AUTH, so the old single-file read sent an empty pass → 401.)
diff --git a/test/pre-send-gate.test.js b/test/pre-send-gate.test.js
new file mode 100644
index 0000000..423093b
--- /dev/null
+++ b/test/pre-send-gate.test.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const assert = require('node:assert/strict');
+const { assertPreSendCompliance, REQUIRED_FROM } = require('../lib/pre-send-gate');
+const { compose } = require('../lib/compose');
+
+const compliant = compose({
+  name: 'Test Vendor',
+  account_number: '123',
+  sample_email: 'samples@example.com',
+  ship_to: {
+    name: 'Designer Wallcoverings',
+    line1: '15442 Ventura Blvd. #102',
+    city_state_zip: 'Sherman Oaks, CA 91403',
+    phone: '1-888-373-4564',
+  },
+}, [{ sku: 'DW-1', requested: '08/01/2026', mfr: 'ABC' }]);
+
+assert.equal(assertPreSendCompliance({ from: REQUIRED_FROM, subject: compliant.subject, body: compliant.html }), true);
+
+for (const mutation of [
+  { from: 'wrong@example.com', subject: compliant.subject, body: compliant.html },
+  { from: REQUIRED_FROM, subject: `Re: ${compliant.subject}`, body: compliant.html },
+  { from: REQUIRED_FROM, subject: compliant.subject, body: compliant.html.replace(/15442 Ventura Blvd\. #102/g, '') },
+  { from: REQUIRED_FROM, subject: compliant.subject, body: compliant.html.replace(/<a href="mailto:info@designerwallcoverings\.com\?subject=Unsubscribe[\s\S]*?<\/a>/, '') },
+]) {
+  assert.throws(() => assertPreSendCompliance(mutation), { code: 'PRE_SEND_COMPLIANCE_BLOCKED' });
+}
+
+console.log('pre-send compliance gate: PASS');

← 7dd3faf auto-data-snapshot: 2026-08-28T10:53:29 (1 data files) — dat  ·  back to Sample Followup Sweep  ·  auto-data-snapshot: 2026-08-28T14:34:59 (1 data files) — REA c70d49a →