← back to Marketing Command Center
channels: resolve IG per-page (bulk field dead) + add /meta-token paste-ingest route to bypass blocked OAuth redirect
bd25186145a28b90daa6d5d97428b856e9565467 · 2026-07-15 16:33:34 -0700 · Steve Abrams
Files touched
M modules/channels/index.js
Diff
commit bd25186145a28b90daa6d5d97428b856e9565467
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 15 16:33:34 2026 -0700
channels: resolve IG per-page (bulk field dead) + add /meta-token paste-ingest route to bypass blocked OAuth redirect
---
modules/channels/index.js | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/modules/channels/index.js b/modules/channels/index.js
index e2b1106..6c5af5f 100644
--- a/modules/channels/index.js
+++ b/modules/channels/index.js
@@ -41,6 +41,21 @@ async function fetchMetaPages() {
});
url = j.paging?.next || null;
}
+ // Meta stopped populating instagram_business_account in the bulk /me/accounts
+ // response (verified 2026-07-15 — it returns null there even with instagram_basic).
+ // It DOES resolve when you query the page node WITH ITS OWN PAGE TOKEN. So for any
+ // page still missing an IG link but holding a token, resolve it per-page (batched).
+ const needIg = pages.filter(p => !p.igId && p.token);
+ for (let i = 0; i < needIg.length; i += 10) {
+ await Promise.all(needIg.slice(i, i + 10).map(async (p) => {
+ try {
+ const r = await fetch(`${GRAPH}/${p.id}?fields=instagram_business_account{id,username}&access_token=${encodeURIComponent(p.token)}`);
+ const j = await r.json();
+ const ig = j.instagram_business_account;
+ if (ig) { p.igId = ig.id || null; p.igUsername = ig.username || null; }
+ } catch { /* leave this page IG-less; a single failure never aborts the fleet */ }
+ }));
+ }
pages.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
const cache = { fetchedAt: new Date().toISOString(), pages };
writeMetaPages(cache);
@@ -577,6 +592,24 @@ module.exports = {
} catch (e) { res.status(500).send(`Token exchange failed for ${p}: ${e.message}`); }
});
+ // Paste-a-token ingest — the fallback when the OAuth redirect is blocked (e.g.
+ // App Domains / redirect-URI not registered in the Meta app). Steve generates a
+ // user token in Graph API Explorer and pastes it; we upgrade it to long-lived,
+ // persist it as META_ACCESS_TOKEN, and re-cache the whole Page/IG fleet. Same end
+ // state as a successful OAuth callback, zero redirect config required.
+ router.post('/meta-token', async (req, res) => {
+ const raw = (req.body && (req.body.token || req.body.access_token) || '').trim();
+ if (!raw) return res.status(400).json({ error: 'no token provided' });
+ try {
+ let longLived = raw;
+ try { longLived = await exchangeLongLivedMeta(raw); } catch { /* already long-lived / exchange n/a → use as-is */ }
+ setEnvKeys({ META_ACCESS_TOKEN: longLived }); // process.env updated live
+ const cache = await fetchMetaPages(); // re-cache all Pages + IG (per-page resolve)
+ const igCount = cache.pages.filter(p => p.igId).length;
+ res.json({ ok: true, pages: cache.pages.length, ig: igCount, igAccounts: cache.pages.filter(p => p.igId).map(p => p.igUsername || p.igId) });
+ } catch (e) { res.status(500).json({ error: e.message }); }
+ });
+
// The publish pipeline. Stages by default; only fires live with confirm + !dryRun
// AND a connected platform. Unconnected platforms always stage.
router.post('/publish', async (req, res) => {
← fea8d92 All Calendars module: backend (activation projection + shopi
·
back to Marketing Command Center
·
All Calendars: front-end panel (facet rail + multi-layer mon c3e1ea5 →