← back to Marketing Command Center
follow-counts: read Norma auth from secrets-manager IG_AGENT_AUTH first, fall through on 401/403 to instagram-agent .env then NORMA_IG_PASS (TK-12327)
f88c44a154a8db510b2ca93a1096d7572b3bbafe · 2026-09-26 09:16:40 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZxSBvMaQ2irhbKdpmgRPj
Files touched
M modules/follow-counts/index.js
Diff
commit f88c44a154a8db510b2ca93a1096d7572b3bbafe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 26 09:16:40 2026 -0700
follow-counts: read Norma auth from secrets-manager IG_AGENT_AUTH first, fall through on 401/403 to instagram-agent .env then NORMA_IG_PASS (TK-12327)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZxSBvMaQ2irhbKdpmgRPj
---
modules/follow-counts/index.js | 60 +++++++++++++++++++++++++++++++-----------
1 file changed, 45 insertions(+), 15 deletions(-)
diff --git a/modules/follow-counts/index.js b/modules/follow-counts/index.js
index 3d73f95..8df6e88 100644
--- a/modules/follow-counts/index.js
+++ b/modules/follow-counts/index.js
@@ -24,27 +24,40 @@ 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(/\/$/, '');
-// 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() {
+// Credential sources, tried in order (TK-12327):
+// 1. secrets-manager master .env IG_AGENT_AUTH (canonical "Basic ..." header)
+// 2. the instagram-agent's OWN .env AUTH_USERNAME/AUTH_PASSWORD
+// 3. NORMA_IG_USER/NORMA_IG_PASS from this repo's env (explicit override, last resort)
+// A copied NORMA_IG_PASS went stale when Norma rotated its password after the 2026-07-29
+// empty-password incident -> every snapshot 403'd "Invalid credentials" (TK-12008). So a
+// 401/403 on one source falls through to the next rather than failing the run: a rotation
+// that updates only one of secrets-manager / the agent .env can't silently re-break this.
+function readEnvFile(f) {
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_* */ }
+ } catch { /* unreadable -> next source */ }
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');
+function normaAuthCandidates() {
+ const home = require('os').homedir();
+ const basic = (u, p) => 'Basic ' + Buffer.from(`${u}:${p}`).toString('base64');
+ const out = [];
+ const add = (source, header) => {
+ if (header && /^Basic \S+$/.test(header) && !out.some(c => c.header === header)) out.push({ source, header });
+ };
+ const sm = readEnvFile(process.env.SECRETS_MANAGER_ENV || path.join(home, 'Projects', 'secrets-manager', '.env'));
+ add('secrets-manager IG_AGENT_AUTH', (sm.IG_AGENT_AUTH || '').trim());
+ const agent = readEnvFile(process.env.NORMA_IG_AGENT_ENV ||
+ path.join(home, 'Projects', 'Norma', 'agents', 'instagram-agent', '.env'));
+ if (agent.AUTH_PASSWORD) add('instagram-agent .env', basic(agent.AUTH_USERNAME || 'admin', agent.AUTH_PASSWORD));
+ if (process.env.NORMA_IG_PASS) add('NORMA_IG_PASS env', basic(process.env.NORMA_IG_USER || 'admin', process.env.NORMA_IG_PASS));
+ return out;
+}
+const NORMA_AUTH = normaAuthCandidates();
// ── account roster ────────────────────────────────────────────────────────────
// The DW-owned IG accounts this tab tracks. Each: { handle, label, igUserId? }.
@@ -90,16 +103,33 @@ async function fetchCountsFromNorma(attempts = 4) {
return last;
}
async function fetchCountsFromNormaOnce() {
+ if (!NORMA_AUTH.length) {
+ return { ok: false, error: 'Norma auth not configured (no IG_AGENT_AUTH in secrets-manager, no instagram-agent .env, no NORMA_IG_PASS)' };
+ }
+ let denied;
+ for (const cand of NORMA_AUTH) {
+ const r = await fetchCountsWithAuth(cand.header);
+ if (r.status === 401 || r.status === 403) {
+ console.error(`[follow-counts] ${new Date().toISOString()} auth via ${cand.source} rejected (HTTP ${r.status}); trying next source`);
+ denied = { ok: false, error: `${r.error} (auth rejected by every source: ${NORMA_AUTH.map(c => c.source).join(', ')})` };
+ continue;
+ }
+ delete r.status;
+ return r;
+ }
+ return denied;
+}
+async function fetchCountsWithAuth(authHeader) {
const url = `${NORMA_BASE}/api/skill/monitor`;
try {
const r = await fetchWithTimeout(url, {
method: 'POST',
- headers: { 'Content-Type': 'application/json', Authorization: normaAuth },
+ headers: { 'Content-Type': 'application/json', Authorization: authHeader },
body: JSON.stringify({ period: 'day', push_to_pulse: false }),
});
const j = await r.json().catch(() => ({}));
if (!r.ok || j.success === false) {
- return { ok: false, error: j.error || `Norma monitor HTTP ${r.status}` };
+ return { ok: false, status: r.status, error: j.error || `Norma monitor HTTP ${r.status}` };
}
const acct = (j.result && j.result.account) || {};
return {
← c82763c auto-data-snapshot: 2026-09-26T08:50:16 (3 data files) — dat
·
back to Marketing Command Center
·
TK-11856 rc-propagation (RSYNC): mcc-clients-refresh propaga af0204b →