← 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
M modules/assets/index.js
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 →