[object Object]

← back to Marketing Command Center

Image Bank: gated STAGE-ONLY /repost endpoint (rights-confirm + Kravet-block + auto-credit + wallpaper-word-ban + #ad) + /reposts list — UI button pending

c04d2b72dc97a2cbbeb65e5582c971ba11e37f18 · 2026-06-13 09:32:34 -0700 · Steve Abrams

Files touched

Diff

commit c04d2b72dc97a2cbbeb65e5582c971ba11e37f18
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jun 13 09:32:34 2026 -0700

    Image Bank: gated STAGE-ONLY /repost endpoint (rights-confirm + Kravet-block + auto-credit + wallpaper-word-ban + #ad) + /reposts list — UI button pending
---
 modules/assets/index.js | 42 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/modules/assets/index.js b/modules/assets/index.js
index 8b9f461..210b277 100644
--- a/modules/assets/index.js
+++ b/modules/assets/index.js
@@ -387,6 +387,48 @@ module.exports = {
       } catch (e) { res.status(502).json({ error: e.message }); }
     });
 
+    // ── Repost (gated, STAGE-ONLY) ──────────────────────────────────────────────
+    // Per the compliance + commerce ruling: reposting third-party content under the
+    // DW account is BLOCKED-pending-per-post-approval. This endpoint enforces the
+    // hard code-gates and STAGES a draft — it never publishes (publish needs the
+    // settlement image gate + Steve's approval + a connected Meta token).
+    const REPOSTS = path.join(DATA_DIR, 'reposts.json');
+    const KRAVET_RE = /kravet|lee\s*jofa|groundworks|brunschwig|cole.*son|gp.*baker|colefax|clarke|mulberry|baker lifestyle|andrew martin|nicolette mayer|barclay butera|thom filicia/i;
+    const deWallpaper = s => String(s || '').replace(/\bwallpapers\b/gi, 'wallcoverings').replace(/\bwallpaper\b/gi, 'wallcovering');
+    function readReposts() { try { return JSON.parse(fs.readFileSync(REPOSTS, 'utf8')); } catch { return []; } }
+
+    router.post('/repost', (req, res) => {
+      const { id, caption, rightsConfirmed, sourceHandle, paid } = req.body || {};
+      const store = readStore();
+      const asset = store.find(a => a.id === id);
+      if (!asset) return res.status(404).json({ error: 'asset not found' });
+      // GATE 1 — rights confirmation (fail-closed)
+      if (rightsConfirmed !== true) return res.status(400).json({ gate: 'rights', error: 'Rights not confirmed — tick “I have the right to repost this image” first.' });
+      // resolve credit handle (explicit, else an @tag on the asset)
+      let handle = (sourceHandle || (asset.tags || []).find(t => /^@/.test(t)) || '').trim();
+      if (handle && !handle.startsWith('@')) handle = '@' + handle;
+      // GATE 2 — brand-controlled source hard-block (Kravet family etc.)
+      if (handle && KRAVET_RE.test(handle)) return res.status(403).json({ gate: 'blocked-vendor', error: `${handle} is brand-controlled (Kravet family) — reposting is blocked. Use a licensed brand asset instead.` });
+      // GATE 3 — auto-credit required
+      if (!handle) return res.status(400).json({ gate: 'credit', error: 'No source @handle to credit — add the source handle (auto-credit is mandatory).' });
+      // build caption: "Wallpaper" word-ban + ensure credit + #ad if paid (GATE 4)
+      let cap = deWallpaper(caption || '');
+      if (!cap.toLowerCase().includes(handle.toLowerCase())) cap += `${cap ? '\n\n' : ''}via ${handle}`;
+      if (paid === true && !/#ad\b/i.test(cap)) cap += ' #ad';
+      // STAGE — never auto-publish; settlement gate + approval + token still required
+      const draft = {
+        id: 'repost-' + newId(), assetId: asset.id, imageSrc: withSrc(asset).src,
+        caption: cap, credit: handle, paid: !!paid,
+        settlement: 'REQUIRED-before-publish', status: 'staged-pending-approval',
+        created_at: new Date().toISOString(),
+      };
+      const arr = readReposts(); arr.push(draft);
+      try { fs.writeFileSync(REPOSTS, JSON.stringify(arr, null, 2)); } catch (e) { return res.status(500).json({ error: e.message }); }
+      res.json({ ok: true, staged: true, draft, note: 'Repost STAGED — it will NOT post. The settlement image gate, your final approval, and a connected Meta token are all required before any publish.' });
+    });
+
+    router.get('/reposts', (_req, res) => res.json({ reposts: readReposts().slice(-100).reverse() }));
+
     // Delete an asset (and its file, if uploaded).
     router.delete('/:id', (req, res) => {
       const store = readStore();

← 961a1bb Image Bank: Instagram ingest — public-permalink og:image gra  ·  back to Marketing Command Center  ·  LinkedIn module (DTD verdict A): draft-first composer w/ DW 923cb9c →