← back to Marketing Command Center
follow-counts: read Norma instagram-agent .env for Basic-auth (stale NORMA_IG_PASS caused 403 'Invalid credentials'); retry boot-race connection errors (TK-12008)
bd903cd731dc3e78f63c46e06efb7b75db30846d · 2026-09-25 14:03:55 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7LB6rCwpA7ubJTqYzqXEX
Files touched
M modules/follow-counts/index.js
Diff
commit bd903cd731dc3e78f63c46e06efb7b75db30846d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 25 14:03:55 2026 -0700
follow-counts: read Norma instagram-agent .env for Basic-auth (stale NORMA_IG_PASS caused 403 'Invalid credentials'); retry boot-race connection errors (TK-12008)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7LB6rCwpA7ubJTqYzqXEX
---
modules/follow-counts/index.js | 35 ++++++++++++++++++++++++++++++++---
1 file changed, 32 insertions(+), 3 deletions(-)
diff --git a/modules/follow-counts/index.js b/modules/follow-counts/index.js
index 90574dd..701fd3e 100644
--- a/modules/follow-counts/index.js
+++ b/modules/follow-counts/index.js
@@ -24,8 +24,26 @@ const HISTORY = path.join(DATA, 'follow-counts-history.json');
// ── Norma instagram-agent (reuse its token/IG-user-id/simulation) ─────────────
const NORMA_BASE = (process.env.NORMA_IG_BASE || 'http://127.0.0.1:9810').replace(/\/$/, '');
-const NORMA_USER = process.env.NORMA_IG_USER || 'admin';
-const NORMA_PASS = process.env.NORMA_IG_PASS || ''; // empty by default (admin:)
+// Credential source of truth = the instagram-agent's OWN .env (AUTH_USERNAME/AUTH_PASSWORD).
+// A copied NORMA_IG_PASS in this repo's .env went stale when Norma rotated its password after
+// the 2026-07-29 empty-password incident -> every snapshot 403'd "Invalid credentials"
+// (TK-12008). Read the agent's .env first so a future rotation can't silently re-break this;
+// NORMA_IG_PASS remains an explicit override only if the agent .env is unreadable.
+function readNormaAgentEnv() {
+ const out = {};
+ const f = process.env.NORMA_IG_AGENT_ENV ||
+ path.join(require('os').homedir(), 'Projects', 'Norma', 'agents', 'instagram-agent', '.env');
+ try {
+ for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
+ const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
+ if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, '');
+ }
+ } catch { /* unreadable -> fall back to NORMA_IG_* */ }
+ return out;
+}
+const _normaEnv = readNormaAgentEnv();
+const NORMA_USER = _normaEnv.AUTH_USERNAME || process.env.NORMA_IG_USER || 'admin';
+const NORMA_PASS = _normaEnv.AUTH_PASSWORD || process.env.NORMA_IG_PASS || '';
const normaAuth = 'Basic ' + Buffer.from(`${NORMA_USER}:${NORMA_PASS}`).toString('base64');
// ── account roster ────────────────────────────────────────────────────────────
@@ -58,7 +76,18 @@ const todayKey = (d = new Date()) => d.toISOString().slice(0, 10); // YYYY-MM-DD
// Ask Norma for the current account counts. Returns
// { ok, followers, following, media, simulated, checked_at } (or { ok:false, error })
-async function fetchCountsFromNorma() {
+// RunAtLoad fires at login before pm2 has norma-instagram listening -> "fetch failed".
+// Retry connection errors (NOT auth/HTTP errors) a few times before giving up.
+async function fetchCountsFromNorma(attempts = 4) {
+ let last;
+ for (let i = 0; i < attempts; i++) {
+ last = await fetchCountsFromNormaOnce();
+ if (last.ok || !/^Norma unreachable/.test(last.error || '')) return last;
+ if (i < attempts - 1) await new Promise(r => setTimeout(r, 15000));
+ }
+ return last;
+}
+async function fetchCountsFromNormaOnce() {
const url = `${NORMA_BASE}/api/skill/monitor`;
try {
const r = await fetchWithTimeout(url, {
← f9ac112 auto-data-snapshot: 2026-09-25T13:56:04 (1 data files) — pub
·
back to Marketing Command Center
·
auto-data-snapshot: 2026-09-25T14:28:14 (1 data files) — pub f232ea2 →