[object Object]

← back to Goldleafwallpaper

IG poster now verifies its token-expiry alert actually landed instead of claiming it did

7e809694739066c0452e0fd9d651010cc1200250 · 2026-09-13 16:23:49 -0700 · Steve

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GeVrknY4tKJETG3By4CAc5

Files touched

Diff

commit 7e809694739066c0452e0fd9d651010cc1200250
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Sep 13 16:23:49 2026 -0700

    IG poster now verifies its token-expiry alert actually landed instead of claiming it did
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01GeVrknY4tKJETG3By4CAc5
---
 scripts/ig-poster/cncp-alert.js      |  94 +++++++++++++++++++++++++++++
 scripts/ig-poster/post-next.js       |  48 +++++++++++----
 scripts/ig-poster/test-cncp-alert.js | 111 +++++++++++++++++++++++++++++++++++
 3 files changed, 243 insertions(+), 10 deletions(-)

diff --git a/scripts/ig-poster/cncp-alert.js b/scripts/ig-poster/cncp-alert.js
new file mode 100644
index 0000000..2028381
--- /dev/null
+++ b/scripts/ig-poster/cncp-alert.js
@@ -0,0 +1,94 @@
+'use strict';
+/*
+ * cncp-alert.js — post a CNCP parking-lot card and REPORT whether it landed.
+ *
+ * WHY THIS EXISTS (CLAUDE.md TK-11431, amendment 2: "alert delivery is a
+ * FINDING, not a log line" / "never hand-roll a curl alert arm")
+ * ---------------------------------------------------------------------------
+ * post-next.js used to alert on an expired IG token like this:
+ *
+ *     try {
+ *       await fetch('http://127.0.0.1:3333/api/parking-lot', {...}).catch(() => {});
+ *     } catch (_) {}
+ *     console.error('ALERT: ... posted CNCP card. Re-auth needed.');
+ *
+ * Three independent ways that printed a FALSE success claim:
+ *   (a) `.catch(() => {})` swallowed any network failure;
+ *   (b) the outer `catch (_) {}` swallowed everything else;
+ *   (c) fetch() RESOLVES on HTTP 404/500 — it does not throw on a non-2xx — so a
+ *       moved/renamed endpoint means NOTHING throws and the success line prints
+ *       anyway. That is the exact class that left two sibling canaries POSTing
+ *       into a 404 for months while logging success.
+ *
+ * This matters more here than on a canary: this is the ONLY alarm on a LIVE
+ * Instagram publisher. launchd-job-canary does NOT cover it — scan.sh:66 flags
+ * only exit 127 or a hard-failure stderr signature, and its BROKEN_RE has no
+ * alternative matching Meta's wording ("Error validating access token" /
+ * "OAuthException" / code 190). So post-next.js's exit(2) is NOT flagged: if
+ * this card never lands, the poster is silently dead until someone looks.
+ *
+ * The fix is to route through the SHARED sender (~/.claude/skills/_shared/
+ * cncp_post.sh) rather than hand-rolling curl/fetch again. That sender:
+ *   - remaps the payload to the two fields the handler actually reads
+ *     (url, note) — {title,...} alone is a permanent 400;
+ *   - ASSERTS a 2xx via `curl -w '%{http_code}'` and returns non-zero otherwise;
+ *   - emits a delivery RECEIPT at the chokepoint, which the producer cannot
+ *     suppress by forgetting.
+ *
+ * RECEIPT LOCATION CAVEAT (honest, measured): alert_receipt.sh resolves the
+ * receipt dir from $SKILL, else by walking BASH_SOURCE up to a "skills" parent.
+ * goldleafwallpaper is a PROJECT, not a skill, so there is no honest $SKILL to
+ * set and the receipt lands in the shared bucket
+ * ~/.claude/skills/_shared/data/alert-delivery.jsonl. fleet-health-rollup reads
+ * receipts per SKILL row, and _shared has no data/latest.json, so that receipt
+ * is durable but NOT surfaced on the morning panel. The project-local durable
+ * record is therefore the caller's own post-log.jsonl line plus loud stderr.
+ *
+ * TEST SEAM: CNCP_URL (honoured by cncp_post.sh itself) points the POST at a
+ * controlled local stub. Nothing here reads a --test flag, and no plist sets
+ * CNCP_URL, so a scheduled run always targets the real board.
+ *
+ * Cost: $0 (local POST to 127.0.0.1).
+ */
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+const SENDER = path.join(
+  process.env.HOME || '', '.claude', 'skills', '_shared', 'cncp_post.sh');
+
+/**
+ * Post one parking-lot card.
+ * @returns {{delivered: boolean, error: string|null}} — delivered:true ONLY when
+ *   the shared sender verified a 2xx. Every other outcome (transport failure,
+ *   non-2xx, missing sender, missing jq, timeout) returns delivered:false with a
+ *   reason. NEVER throws: an alert arm must not be able to mask the underlying
+ *   failure it is reporting.
+ */
+function postCncpCard(url, note, opts) {
+  const o = opts || {};
+  const sender = o.sender || SENDER;
+  try {
+    execFileSync(
+      '/bin/bash',
+      ['-c', '. "$CNCP_SENDER" && cncp_post "$CNCP_CARD_URL" "$CNCP_CARD_NOTE"'],
+      {
+        env: Object.assign({}, process.env, {
+          CNCP_SENDER: sender,
+          CNCP_CARD_URL: String(url || ''),
+          CNCP_CARD_NOTE: String(note || ''),
+        }),
+        stdio: 'pipe',
+        timeout: o.timeoutMs || 15000,
+      },
+    );
+    return { delivered: true, error: null };
+  } catch (e) {
+    // cncp_post prints its reason (HTTP code / transport / jq) on stderr and
+    // returns non-zero, so bash exits non-zero and execFileSync throws here.
+    const se = e && e.stderr ? String(e.stderr).trim() : '';
+    const reason = se || String((e && e.message) || e);
+    return { delivered: false, error: reason.replace(/\s+/g, ' ').slice(0, 300) };
+  }
+}
+
+module.exports = { postCncpCard, SENDER };
diff --git a/scripts/ig-poster/post-next.js b/scripts/ig-poster/post-next.js
index 1c427e5..39ab490 100644
--- a/scripts/ig-poster/post-next.js
+++ b/scripts/ig-poster/post-next.js
@@ -22,6 +22,7 @@
 const fs = require('fs');
 const path = require('path');
 const { buildCaption } = require('./caption');
+const { postCncpCard } = require('./cncp-alert');
 
 const ROOT = path.resolve(__dirname, '../..');
 const PRODUCTS = path.join(ROOT, 'data/products.json');
@@ -147,17 +148,44 @@ async function publish(imageUrl, caption) {
       // If this is a token/auth failure, alert LOUDLY so the poster can never
       // silently stop after the token expires — post a CNCP parking-lot card.
       if (/OAuth|expired|code":?\s*190|access token|not valid|session/i.test(msg)) {
+        // CLAUDE.md TK-11431 amendment 2 — ALERT DELIVERY IS A FINDING, NOT A
+        // LOG LINE. The previous arm was a hand-rolled fetch wrapped in
+        // `.catch(() => {})` inside `try {} catch (_) {}`, and then printed
+        // "posted CNCP card" UNCONDITIONALLY. It could not tell a landed card
+        // from a swallowed network error, from a swallowed throw, or — the
+        // case a positive-only test misses entirely — from an HTTP 404/500,
+        // because fetch() RESOLVES on a non-2xx and so reaches neither catch.
+        // This is the ONLY alarm on a LIVE Instagram publisher, and
+        // launchd-job-canary does not cover this exit(2) (scan.sh:66 flags only
+        // exit 127 or a hard-failure stderr signature, and its BROKEN_RE has no
+        // alternative matching Meta's wording), so a false success here means
+        // the poster dies silently. Route through the shared sender, which
+        // asserts a 2xx and emits a delivery receipt at the chokepoint — then
+        // report what it ACTUALLY said.
+        const note = `[@goldleafwallpaper IG poster] token expired — needs re-auth. `
+          + `The 4h RML poster stopped: ${msg.slice(0, 200)}. `
+          + `Refresh GOLDLEAF_IG_TOKEN in scripts/ig-poster/.env (see CONNECT.md).`;
+        const alert = postCncpCard('alert://goldleafwallpaper/ig-poster', note);
+        // Report FIRST, persist second. A throwing appendLog (full disk, bad
+        // perms) between the two would otherwise swallow the loud stderr line
+        // this whole change exists to guarantee — the same silencing shape,
+        // one level up. The write is wrapped for the same reason: bookkeeping
+        // must never be able to mask the failure it is bookkeeping.
+        if (alert.delivered) {
+          console.error('ALERT: token appears expired/invalid — CNCP card DELIVERED (verified 2xx). Re-auth needed.');
+        } else {
+          console.error('ALERT NOT DELIVERED: token appears expired/invalid AND the CNCP card FAILED to post'
+            + ` (${alert.error}). NOBODY HAS BEEN TOLD — re-auth GOLDLEAF_IG_TOKEN by hand (see CONNECT.md).`);
+        }
+        // Durable, project-local record of the ALARM's own fate. The shared
+        // receipt lands in _shared/data/alert-delivery.jsonl, which
+        // fleet-health-rollup does not surface for a non-skill project.
         try {
-          await fetch('http://127.0.0.1:3333/api/parking-lot', {
-            method: 'POST', headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({
-              title: '@goldleafwallpaper IG poster: token expired — needs re-auth',
-              note: `The 4h RML poster stopped: ${msg.slice(0, 200)}. Refresh GOLDLEAF_IG_TOKEN in scripts/ig-poster/.env (see CONNECT.md).`,
-              project: 'goldleafwallpaper',
-            }),
-          }).catch(() => {});
-        } catch (_) {}
-        console.error('ALERT: token appears expired/invalid — posted CNCP card. Re-auth needed.');
+          appendLog({ ts: nowISO(), dw_sku: sku, result: 'alert', channel: 'cncp',
+                      alert_delivered: alert.delivered, alert_error: alert.error });
+        } catch (logErr) {
+          console.error('WARN: could not append the alert receipt to post-log.jsonl:', String(logErr.message || logErr));
+        }
       }
       // Do NOT advance the cursor on failure — retry the same SKU next tick.
       console.log('cost: $0 (local; Graph publish is free)');
diff --git a/scripts/ig-poster/test-cncp-alert.js b/scripts/ig-poster/test-cncp-alert.js
new file mode 100644
index 0000000..d5f7f49
--- /dev/null
+++ b/scripts/ig-poster/test-cncp-alert.js
@@ -0,0 +1,111 @@
+'use strict';
+/*
+ * test-cncp-alert.js — NEGATIVE-TEST-FIRST proof for the IG-poster alert arm.
+ *
+ * CLAUDE.md TK-11431 amendment 3: "a check ships with a negative test proving it
+ * goes RED on an injected fault, or it does not ship. A positive-only test on a
+ * detector proves nothing." The defect this replaces was invisible to exactly a
+ * positive-only test, because fetch() RESOLVES on HTTP 404/500 — so the ONLY
+ * case that mattered (non-2xx) looked identical to success.
+ *
+ * Runs four cases against LOCAL STUB servers — it never touches the real CNCP
+ * board, never posts a real card, and never loads post-next.js (so it cannot
+ * post to Instagram):
+ *   1. NEGATIVE  non-2xx  (stub returns 404)          -> MUST report delivered:false
+ *   2. NEGATIVE  non-2xx  (stub returns 500)          -> MUST report delivered:false
+ *   3. NEGATIVE  transport (nothing listening)        -> MUST report delivered:false
+ *   4. POSITIVE  2xx      (stub returns 200)          -> MUST report delivered:true
+ * Plus: the shared sender's delivery RECEIPT is asserted for both a failed and a
+ * successful send, since that receipt is the durable, producer-unsuppressable
+ * signal the whole design rests on.
+ *
+ * The seam is CNCP_URL (honoured by _shared/cncp_post.sh). No plist sets it, so
+ * a scheduled run always targets the real board.
+ *
+ * Run:  node scripts/ig-poster/test-cncp-alert.js        ($0 local)
+ */
+const http = require('http');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { spawn } = require('child_process');
+const { postCncpCard } = require('./cncp-alert');
+
+// The stub MUST live in its own process: postCncpCard is synchronous
+// (execFileSync), so an in-process http server would never get a chance to
+// answer and every case — including the 200 control — would fail as a timeout.
+// That is itself a measuring-the-wrong-thing trap, so it is designed out.
+function stub(status) {
+  return new Promise((resolve, reject) => {
+    const code = `const http=require('http');const s=http.createServer((q,r)=>{r.writeHead(${status});r.end('{}')});`
+      + `s.listen(0,'127.0.0.1',()=>console.log(s.address().port));`;
+    const p = spawn(process.execPath, ['-e', code], { stdio: ['ignore', 'pipe', 'inherit'] });
+    let buf = '';
+    p.stdout.on('data', (d) => {
+      buf += d;
+      if (buf.includes('\n')) resolve({ srv: { close: () => p.kill() }, port: Number(buf.trim()) });
+    });
+    p.on('error', reject);
+  });
+}
+function freePort() {
+  return new Promise((resolve) => {
+    const srv = http.createServer();
+    srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => resolve(p)); });
+  });
+}
+
+// Receipts go to a THROWAWAY skills-shaped dir so the real shared receipt log is
+// not polluted with test rows (alert_receipt.sh prefers $SKILL when it is a dir
+// whose parent is named "skills").
+const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'goldleaf-alert-test-'));
+const SKILL = path.join(TMP, 'skills', 'goldleaf-ig-poster-test');
+fs.mkdirSync(SKILL, { recursive: true });
+process.env.SKILL = SKILL;
+const RECEIPTS = path.join(SKILL, 'data', 'alert-delivery.jsonl');
+const receipts = () => (fs.existsSync(RECEIPTS)
+  ? fs.readFileSync(RECEIPTS, 'utf8').split('\n').filter(Boolean).map(JSON.parse) : []);
+
+let failures = 0;
+function check(name, cond, detail) {
+  console.log(`${cond ? 'PASS' : 'FAIL'}  ${name}${detail ? '  ::  ' + detail : ''}`);
+  if (!cond) failures++;
+}
+
+(async () => {
+  console.log(`receipts -> ${RECEIPTS}\n`);
+
+  // --- 1 & 2: NEGATIVE, non-2xx. The case the old code got wrong. ---
+  for (const code of [404, 500]) {
+    const { srv, port } = await stub(code);
+    process.env.CNCP_URL = `http://127.0.0.1:${port}`;
+    const r = postCncpCard('alert://test', 'injected fault: stub returns ' + code);
+    srv.close();
+    check(`NEGATIVE non-2xx ${code} reports NOT delivered`, r.delivered === false, JSON.stringify(r));
+    check(`NEGATIVE non-2xx ${code} names the HTTP code in its reason`,
+      String(r.error).includes(String(code)), r.error);
+  }
+  const afterFail = receipts().slice(-1)[0];
+  check('NEGATIVE non-2xx writes a receipt with ok:false',
+    !!afterFail && afterFail.ok === false && afterFail.channel === 'cncp', JSON.stringify(afterFail));
+
+  // --- 3: NEGATIVE, transport failure (nothing listening). ---
+  const dead = await freePort();
+  process.env.CNCP_URL = `http://127.0.0.1:${dead}`;
+  const r3 = postCncpCard('alert://test', 'injected fault: nothing listening');
+  check('NEGATIVE transport failure reports NOT delivered', r3.delivered === false, JSON.stringify(r3));
+  check('NEGATIVE transport failure explains itself', /transport|reaching|CNCP/i.test(String(r3.error)), r3.error);
+
+  // --- 4: POSITIVE, 2xx. The success path must still report success. ---
+  const { srv: okSrv, port: okPort } = await stub(200);
+  process.env.CNCP_URL = `http://127.0.0.1:${okPort}`;
+  const r4 = postCncpCard('alert://test', 'control: stub returns 200');
+  okSrv.close();
+  check('POSITIVE 2xx reports delivered', r4.delivered === true, JSON.stringify(r4));
+  const afterOk = receipts().slice(-1)[0];
+  check('POSITIVE 2xx writes a receipt with ok:true',
+    !!afterOk && afterOk.ok === true && afterOk.channel === 'cncp', JSON.stringify(afterOk));
+
+  console.log(`\n${failures === 0 ? 'ALL PASS' : failures + ' FAILURE(S)'} · receipts written: ${receipts().length} · cost: $0 (local)`);
+  process.exit(failures === 0 ? 0 : 1);
+})();

← 094acc2 Add real privacy page + /privacy route (was catch-all servin  ·  back to Goldleafwallpaper  ·  IG poster: stop misclassifying code-9004 media errors as a d 8e34555 →