[object Object]

← back to Dw Marketing Reels

Safety-when-on: ARMED gate (no live post until SOCIAL_LIVE_ARMED=1) + private-label leak filter in captions (contrarian fixes)

2f74ee0f41df171174146080b4346099312f53bc · 2026-07-12 12:00:19 -0700 · Steve Abrams

Files touched

Diff

commit 2f74ee0f41df171174146080b4346099312f53bc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 12 12:00:19 2026 -0700

    Safety-when-on: ARMED gate (no live post until SOCIAL_LIVE_ARMED=1) + private-label leak filter in captions (contrarian fixes)
---
 data/reels.json            | 39 +++++++++++++++++++++++++++++++++++++++
 public/index.html          |  3 ++-
 scripts/build-reel.mjs     | 13 ++++++++++---
 scripts/publish-social.mjs |  6 ++++++
 4 files changed, 57 insertions(+), 4 deletions(-)

diff --git a/data/reels.json b/data/reels.json
index eecfa15..65a1e2b 100644
--- a/data/reels.json
+++ b/data/reels.json
@@ -1,4 +1,43 @@
 [
+  {
+    "file": "new-arrivals-2026-07-12-19-00.mp4",
+    "created_at": "2026-07-12T19:00:19.205Z",
+    "products": 6,
+    "seconds": 19,
+    "titles": [
+      "Katamari / Pewter",
+      "Tenaya / Beige",
+      "Katamari / Dark Gold",
+      "Tenaya / Cream",
+      "Tenaya / Spa Blue",
+      "Tenaya / Sage"
+    ],
+    "caption": "New arrivals just landed ✨ Katamari, Tenaya from Thibaut. Discover the latest wallcoverings — shop the collection at designerwallcoverings.com 🔗\n\n#DesignerWallcoverings #NewArrivals #Wallpaper #Wallcovering #InteriorDesign #InteriorDecor #HomeDesign #LuxuryInteriors #DesignInspo #Thibaut",
+    "hashtags": [
+      "#DesignerWallcoverings",
+      "#NewArrivals",
+      "#Wallpaper",
+      "#Wallcovering",
+      "#InteriorDesign",
+      "#InteriorDecor",
+      "#HomeDesign",
+      "#LuxuryInteriors",
+      "#DesignInspo",
+      "#Thibaut"
+    ],
+    "publish": {
+      "instagram": {
+        "status": "ready-pending-arm",
+        "note": "IG creds present; set SOCIAL_LIVE_ARMED=1 (Steve arm) to enable the first live post",
+        "at": "2026-07-12T19:00:19.278Z"
+      },
+      "tiktok": {
+        "status": "pending-reconnect",
+        "note": "TikTok channel not connected",
+        "at": "2026-07-12T19:00:19.278Z"
+      }
+    }
+  },
   {
     "file": "new-arrivals-2026-07-12-17-17.mp4",
     "created_at": "2026-07-12T17:17:27.415Z",
diff --git a/public/index.html b/public/index.html
index 2dd836b..7d7f8e3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -87,7 +87,8 @@ const fmt = iso => { try { return new Date(iso).toLocaleString(undefined,{year:'
 const escapeHtml = s => String(s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
 const escapeAttr = s => String(s||'').replace(/"/g,'&quot;').replace(/\n/g,'&#10;');
 const STMAP = { posted:['✓ posted','ok'], simulated:['◐ simulated','sim'], 'pending-creds':['◷ needs creds','wait'],
-  'pending-reconnect':['◷ reconnect','wait'], pending:['◷ pending','wait'], error:['✗ error','err'], skipped:['– skipped','wait'] };
+  'pending-reconnect':['◷ reconnect','wait'], 'ready-pending-arm':['🔒 armed?','sim'], 'pending-review':['👁 review','sim'],
+  pending:['◷ pending','wait'], error:['✗ error','err'], skipped:['– skipped','wait'] };
 function pubBadges(pub){
   if(!pub) return '';
   const ch = { instagram:'IG', tiktok:'TT' };
diff --git a/scripts/build-reel.mjs b/scripts/build-reel.mjs
index 5e4b583..86175fc 100644
--- a/scripts/build-reel.mjs
+++ b/scripts/build-reel.mjs
@@ -15,10 +15,15 @@ const INTRO = 2.0, OUTRO = 2.6;
 
 const esc = s => String(s || '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
 
+// Private-label / upstream names that must NEVER surface publicly (mirrors dw-leak-scanner).
+// Captions pull from the LIVE storefront, so defend against any leak reaching IG/TikTok.
+const LEAK_DENY = /greenland|wallquest|chesapeake|nextwall|seabrook|brewster|command\s*54|momentum|versa|desima|carlsten|nicolette\s*mayer|york\s*wall/i;
+const safe = s => s && !LEAK_DENY.test(s);
+
 // On-brand social caption + hashtags from the reel's products ($0 local, deterministic).
 function captionFor(items) {
-  const pats = [...new Set(items.map(i => i.pattern).filter(Boolean))];
-  const vendors = [...new Set(items.map(i => i.vendor).filter(Boolean))];
+  const pats = [...new Set(items.map(i => i.pattern).filter(safe))];
+  const vendors = [...new Set(items.map(i => i.vendor).filter(safe))];
   const lead = pats.slice(0, 3).join(', ');
   const body = `New arrivals just landed ✨  ${lead}${pats.length > 3 ? ' & more' : ''}`
     + `${vendors.length ? ` from ${vendors.slice(0, 3).join(', ')}` : ''}.`
@@ -29,7 +34,9 @@ function captionFor(items) {
     '#InteriorDesign', '#InteriorDecor', '#HomeDesign', '#LuxuryInteriors', '#DesignInspo',
     ...vendors.slice(0, 3).map(tagify),
   ])].slice(0, 14);
-  return { text: `${body}\n\n${tags.join(' ')}`, hashtags: tags };
+  // final safety net: never emit a denylisted token even if one slipped through a field
+  const clean = s => s.split(/\s+/).filter(w => !LEAK_DENY.test(w)).join(' ');
+  return { text: `${clean(body)}\n\n${tags.join(' ')}`, hashtags: tags };
 }
 
 async function dl(url, dest) {
diff --git a/scripts/publish-social.mjs b/scripts/publish-social.mjs
index 630d21a..94e37f6 100644
--- a/scripts/publish-social.mjs
+++ b/scripts/publish-social.mjs
@@ -18,6 +18,10 @@ import { dirname, join } from 'node:path';
 const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
 const MAN = join(ROOT, 'data', 'reels.json');
 const AUTOPOST = process.env.SOCIAL_AUTOPOST === '1';
+// ARMED gate (Steve's standing pattern): even WITH valid creds, no real post fires until
+// SOCIAL_LIVE_ARMED=1 — so the first live post is a deliberate, supervised act, never an
+// accident of cred-landing. Until armed, a cred'd channel reports 'ready-pending-arm'.
+const ARMED = process.env.SOCIAL_LIVE_ARMED === '1';
 const CHANNELS = (process.env.SOCIAL_CHANNELS || 'instagram,tiktok').split(',').map(s => s.trim());
 const IG_URL = process.env.IG_AGENT_URL || 'http://127.0.0.1:9810';
 const IG_AUTH = process.env.IG_AGENT_AUTH || '';
@@ -30,6 +34,7 @@ async function withTimeout(p, ms = 12000) {
 
 async function postInstagram(reel) {
   if (!IG_AUTH) return { status: 'pending-creds', note: 'IG_AGENT_AUTH not set (route via secrets skill)', at: now() };
+  if (!ARMED) return { status: 'ready-pending-arm', note: 'IG creds present; set SOCIAL_LIVE_ARMED=1 (Steve arm) to enable the first live post', at: now() };
   const mediaUrl = `${PUBLIC_BASE}/reels/${encodeURIComponent(reel.file)}`;
   try {
     const res = await withTimeout(fetch(`${IG_URL}/api/skill/reel`, {
@@ -49,6 +54,7 @@ async function postTikTok(reel) {
   // The tiktok skill needs the DW TikTok channel reconnected (OAuth) before the
   // Content Posting API will accept uploads. Until TIKTOK_ACCESS_TOKEN is present, queue.
   if (!process.env.TIKTOK_ACCESS_TOKEN) return { status: 'pending-reconnect', note: 'TikTok channel not connected', at: now() };
+  if (!ARMED) return { status: 'ready-pending-arm', note: 'TikTok token present; set SOCIAL_LIVE_ARMED=1 to enable the first live post', at: now() };
   return { status: 'pending-reconnect', note: 'connected-token present but uploader wiring is a follow-up', at: now() };
 }
 

← 1584e5c Public media exemption: GET /reels/*.mp4 unauthenticated (co  ·  back to Dw Marketing Reels  ·  Themeable reels + Phillipe Romano cork reel (env-driven copy 78da8fd →