[object Object]

← back to Marketing Command Center

MCC bluesky: fail loud if image can't embed — never silently post text-only

39fcdefaafa8b97bbc82d349ef3296816f78b0d7 · 2026-07-27 15:56:08 -0700 · Steve Abrams

postBluesky() previously swallowed any image fetch/uploadBlob failure and posted
text-only anyway (embed left undefined), which slips past the route-level text-only
guard (content.mediaUrl was truthy upstream) and violates Steve's 2026-07-17
'DW never posts text-only' rule — silently, on ok:true. Now aborts the post with a
loud error on any fetch/upload failure. Found by /contrarian gating TK-8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 39fcdefaafa8b97bbc82d349ef3296816f78b0d7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 27 15:56:08 2026 -0700

    MCC bluesky: fail loud if image can't embed — never silently post text-only
    
    postBluesky() previously swallowed any image fetch/uploadBlob failure and posted
    text-only anyway (embed left undefined), which slips past the route-level text-only
    guard (content.mediaUrl was truthy upstream) and violates Steve's 2026-07-17
    'DW never posts text-only' rule — silently, on ok:true. Now aborts the post with a
    loud error on any fetch/upload failure. Found by /contrarian gating TK-8.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 modules/channels/index.js | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/modules/channels/index.js b/modules/channels/index.js
index ae46f7c..6c202ad 100644
--- a/modules/channels/index.js
+++ b/modules/channels/index.js
@@ -596,16 +596,25 @@ async function postBluesky(content) {
   const jwt = sj.accessJwt, did = sj.did;
   let embed;
   if (content.mediaUrl) {
+    // HARD RULE (Steve 2026-07-17): DW never posts text-only. If a mediaUrl was
+    // supplied, the image MUST embed — if the fetch or uploadBlob fails, ABORT the
+    // post instead of silently falling back to text-only. The route-level text-only
+    // guard can't catch that fallback because content.mediaUrl was truthy upstream,
+    // so this is the only place it can be enforced. Fail LOUD, never post naked.
     try {
       const img = await fetch(content.mediaUrl);
+      if (!img.ok) return [{ ok: false, error: `Bluesky image fetch failed (${img.status}) — post aborted; DW never posts text-only` }];
       const buf = Buffer.from(await img.arrayBuffer());
       const ct = img.headers.get('content-type') || 'image/jpeg';
       const up = await fetch(`${PDS}/com.atproto.repo.uploadBlob`, {
         method: 'POST', headers: { 'content-type': ct, authorization: `Bearer ${jwt}` }, body: buf,
       });
       const uj = await up.json().catch(() => ({}));
-      if (up.ok && uj.blob) embed = { $type: 'app.bsky.embed.images', images: [{ alt: (content.caption || '').slice(0, 280), image: uj.blob }] };
-    } catch { /* post text-only if image fetch/upload fails */ }
+      if (!up.ok || !uj.blob) return [{ ok: false, error: `Bluesky image upload failed (${up.status}) — post aborted; DW never posts text-only` }];
+      embed = { $type: 'app.bsky.embed.images', images: [{ alt: (content.caption || '').slice(0, 280), image: uj.blob }] };
+    } catch (e) {
+      return [{ ok: false, error: `Bluesky image upload error: ${e.message} — post aborted; DW never posts text-only` }];
+    }
   }
   const text = (content.caption || '').slice(0, 300);
   const record = { $type: 'app.bsky.feed.post', text, createdAt: new Date().toISOString() };

← f4429c8 auto-save: 2026-07-27T15:21:37 (1 files) — data/cc-warmup-se  ·  back to Marketing Command Center  ·  (newest)