← 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
A scripts/ig-poster/cncp-alert.jsM scripts/ig-poster/post-next.jsA scripts/ig-poster/test-cncp-alert.js
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 →