← back to Goldleafwallpaper
scripts/ig-poster/cncp-alert.js
95 lines
'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 };