← back to Marketing Command Center
channels: Meta connect now upgrades short→long-lived token + re-caches all 80 Pages so the whole IG fleet comes back permanent (not 1-hour)
81c03cf86b16b03255943a396c32935de91cb1cf · 2026-07-15 16:00:47 -0700 · Steve Abrams
Files touched
M modules/channels/index.js
Diff
commit 81c03cf86b16b03255943a396c32935de91cb1cf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 15 16:00:47 2026 -0700
channels: Meta connect now upgrades short→long-lived token + re-caches all 80 Pages so the whole IG fleet comes back permanent (not 1-hour)
---
modules/channels/index.js | 35 ++++++++++++++++++++++++++++++++++-
1 file changed, 34 insertions(+), 1 deletion(-)
diff --git a/modules/channels/index.js b/modules/channels/index.js
index b3c265c..e2b1106 100644
--- a/modules/channels/index.js
+++ b/modules/channels/index.js
@@ -203,6 +203,22 @@ async function exchangeCode(p, code) {
return j; // {access_token, refresh_token?, expires_in?}
}
+// Upgrade a short-lived Meta user token to a long-lived (~60-day) one. Page tokens
+// derived from a long-lived user token are PERMANENT (never expire) — this is the
+// single step that stops the whole fleet dying in an hour. Returns the long-lived
+// access_token, or throws (caller decides whether to fall back to the short one).
+async function exchangeLongLivedMeta(shortToken) {
+ const appId = env('META_APP_ID'), secret = env('META_APP_SECRET');
+ if (!appId || !secret) throw new Error('META_APP_ID / META_APP_SECRET not set');
+ const qs = new URLSearchParams({
+ grant_type: 'fb_exchange_token', client_id: appId, client_secret: secret, fb_exchange_token: shortToken,
+ });
+ const r = await fetch(`${GRAPH}/oauth/access_token?${qs}`);
+ const j = await r.json().catch(() => ({}));
+ if (!r.ok || j.error || !j.access_token) throw new Error(j.error?.message || 'long-lived exchange returned no token');
+ return j.access_token;
+}
+
// ── Per-platform connection status (what's wired vs what Steve must authorize) ──
function platformStatus() {
const tk = readTokens();
@@ -540,7 +556,24 @@ module.exports = {
all[p] = { ...tok, connectedAt: new Date().toISOString() };
if (p === 'facebook' || p === 'instagram') { all.facebook = all[p]; all.instagram = all[p]; }
writeTokens(all);
- res.send(`<body style="font-family:system-ui;padding:50px;text-align:center"><h2>✓ ${p} connected</h2><p>You can close this tab and return to the Marketing Command Center.</p><a href="/#channels">← back to Channels</a></body>`);
+ // Meta short-lived → long-lived, then re-cache ALL Pages. A short-lived
+ // user token (and any Page tokens derived from it) dies in ~1-2h; Page
+ // tokens derived from a LONG-LIVED user token never expire. So we upgrade
+ // once here, persist it as META_ACCESS_TOKEN, and fetch /me/accounts so
+ // the whole 80-Page / IG fleet comes back permanent — not just for an hour.
+ let fleetNote = '';
+ if (p === 'facebook' || p === 'instagram') {
+ try {
+ const longLived = await exchangeLongLivedMeta(tok.access_token);
+ setEnvKeys({ META_ACCESS_TOKEN: longLived }); // process.env updated live
+ const cache = await fetchMetaPages(); // re-cache all Pages + IG links
+ const igCount = cache.pages.filter(x => x.igId).length;
+ fleetNote = `<p>Restored <b>${cache.pages.length} Pages</b> (${igCount} with Instagram) — long-lived, won’t expire.</p>`;
+ } catch (e) {
+ fleetNote = `<p style="color:#b00">Connected, but fleet refresh failed: ${e.message}. Click “↻ refresh Pages” on the Pages picker.</p>`;
+ }
+ }
+ res.send(`<body style="font-family:system-ui;padding:50px;text-align:center"><h2>✓ ${p} connected</h2>${fleetNote}<p>You can close this tab and return to the Marketing Command Center.</p><a href="/#channels">← back to Channels</a></body>`);
} catch (e) { res.status(500).send(`Token exchange failed for ${p}: ${e.message}`); }
});
← 9ef59f0 Add deploy/apply-cutover.sh — paste-safe one-command nginx c
·
back to Marketing Command Center
·
All Calendars module: backend (activation projection + shopi fea8d92 →