[object Object]

← back to Marketing Command Center

Add META token health canary (Council #4): GET /me liveness + expiry warn + heartbeat stamp; alerts CNCP+George on expiry (the silent failure that darkened the Vendor IG panel)

ad0fc2aba17b6f5ca7f3215f48a6a5e6a82fd443 · 2026-06-16 07:12:18 -0700 · Steve Abrams

Files touched

Diff

commit ad0fc2aba17b6f5ca7f3215f48a6a5e6a82fd443
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 16 07:12:18 2026 -0700

    Add META token health canary (Council #4): GET /me liveness + expiry warn + heartbeat stamp; alerts CNCP+George on expiry (the silent failure that darkened the Vendor IG panel)
---
 .gitignore                   |  1 +
 scripts/meta-token-health.js | 68 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 69 insertions(+)

diff --git a/.gitignore b/.gitignore
index 87d44e2..aca5090 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,4 @@ data/reposts.json
 # Meta go-live creds — real Page token, never commit
 scripts/meta-creds.env
 data/meta-pages.json
+data/meta-token-health*
diff --git a/scripts/meta-token-health.js b/scripts/meta-token-health.js
new file mode 100644
index 0000000..64f744d
--- /dev/null
+++ b/scripts/meta-token-health.js
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+/*
+ * meta-token-health.js — canary for the MCC META_ACCESS_TOKEN that powers the
+ * Vendor IG panel's Business-Discovery post fetch. Born 2026-06-16 (Officer
+ * Council #4): the panel went dark on 2026-06-15 when the token silently
+ * expired with zero alert. This GETs /me with the token; on auth failure
+ * (code 190 / expired) it alerts (CNCP card + George email). Best-effort reads
+ * token expiry for an early <7d warning. Writes a heartbeat stamp on every run
+ * (Council #2 lesson) so the meta-watchdog can confirm it actually fired.
+ * READ-ONLY: never writes Shopify/DB, never rotates the token (Steve-gated).
+ */
+const fs = require('fs');
+const path = require('path');
+const GRAPH = 'https://graph.facebook.com/v23.0';
+const ROOT = path.join(__dirname, '..');
+const env = (f, k) => { try { return (fs.readFileSync(f, 'utf8').match(new RegExp('^' + k + '=(.+)$', 'm')) || [])[1]; } catch { return null; } };
+const TOKEN = env(path.join(ROOT, '.env'), 'META_ACCESS_TOKEN');
+const GEORGE_AUTH = env('/Users/stevestudio2/Projects/secrets-manager/.env', 'GEORGE_AUTH');
+const STAMP = path.join(ROOT, 'data', 'meta-token-health-latest.json');
+
+async function alert(subject, body) {
+  // CNCP parking-lot (local, reliable)
+  try {
+    await fetch('http://127.0.0.1:3333/api/parking-lot', { method: 'POST', headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ url: 'http://127.0.0.1:9661/#vendors', note: `⚠️ META token: ${subject} — ${body}` }) });
+  } catch {}
+  // George email (best-effort)
+  if (GEORGE_AUTH) {
+    try {
+      await fetch('http://100.107.67.67:9850/api/send', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + GEORGE_AUTH },
+        body: JSON.stringify({ to: 'steve@designerwallcoverings.com', subject: 'MCC META token — ' + subject, text: body }) });
+    } catch {}
+  }
+}
+
+(async () => {
+  const stamp = (status, detail) => { try { fs.writeFileSync(STAMP, JSON.stringify({ ok: status === 'ok', status, detail, checkedAt: new Date().toISOString() }, null, 2)); } catch {} };
+  if (!TOKEN) { console.log('DEAD: no META_ACCESS_TOKEN in .env'); stamp('missing', 'no token in .env'); await alert('NOT SET', 'No META_ACCESS_TOKEN in MCC .env — Vendor IG panel cannot fetch posts.'); process.exit(0); }
+
+  let r, j;
+  try { r = await fetch(`${GRAPH}/me?fields=id,name&access_token=${encodeURIComponent(TOKEN)}`); j = await r.json(); }
+  catch (e) { console.log('NET ERR', e.message); stamp('neterr', e.message); process.exit(0); }
+
+  if (j.error) {
+    const m = j.error.message || '';
+    const expired = j.error.code === 190 || /expired|session|validating access token/i.test(m);
+    console.log('DEAD:', m);
+    stamp('expired', m);
+    await alert(expired ? 'EXPIRED / INVALID' : 'ERROR',
+      `${m}\n\nThe Vendor IG panel (last-10 posts) is dark until you paste a fresh long-lived META_ACCESS_TOKEN (scopes: instagram_basic, pages_read_engagement, business_management) → route via the secrets skill into MCC .env, then click Refresh on the panel.`);
+    process.exit(0);
+  }
+
+  // token live — best-effort expiry read (debug_token self-inspect; may be unsupported without app token)
+  let expiryNote = 'expiry unknown (no app token)';
+  try {
+    const dr = await fetch(`${GRAPH}/debug_token?input_token=${encodeURIComponent(TOKEN)}&access_token=${encodeURIComponent(TOKEN)}`);
+    const dj = await dr.json();
+    const exp = dj?.data?.expires_at;
+    if (exp && exp > 0) {
+      const days = Math.round((exp * 1000 - Date.now()) / 86400000);
+      expiryNote = `expires in ~${days}d`;
+      if (days <= 7) { stamp('expiring', expiryNote); await alert('EXPIRING SOON', `META_ACCESS_TOKEN ${expiryNote} — refresh it before the Vendor IG panel goes dark.`); console.log('LIVE but', expiryNote); return; }
+    }
+  } catch {}
+  console.log(`LIVE: @${j.name || j.id} OK (${expiryNote})`);
+  stamp('ok', `live as ${j.name || j.id}; ${expiryNote}`);
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });

← 938df84 Code-health: gitignore *.bak + runtime social-queue.json (un  ·  back to Marketing Command Center  ·  Security: remove browser-shipped basic-auth password from cl de6eb5f →