[object Object]

← back to Designer Wallcoverings

cc-api: survive Constant Contact refresh-token rotation (cache-first + persist to .env); PR Naturals builder now loads v10 mailer

573fac58575c148e0cf066c48f4cb9250431a35a · 2026-07-22 12:20:27 -0700 · Steve

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit 573fac58575c148e0cf066c48f4cb9250431a35a
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 22 12:20:27 2026 -0700

    cc-api: survive Constant Contact refresh-token rotation (cache-first + persist to .env); PR Naturals builder now loads v10 mailer
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 mailers/cc-api/cc-client.js | 33 ++++++++++++++++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/mailers/cc-api/cc-client.js b/mailers/cc-api/cc-client.js
index 7b6c19ee..efad14cc 100644
--- a/mailers/cc-api/cc-client.js
+++ b/mailers/cc-api/cc-client.js
@@ -73,6 +73,33 @@ function readTokenCache() {
   return null;
 }
 
+// CC rotates the refresh token on every use — the freshest one lives in the
+// cache file, not .env. An expired cache still holds the ONLY valid refresh token.
+function readCachedRefreshToken() {
+  try {
+    const j = JSON.parse(fs.readFileSync(TOKEN_CACHE, 'utf8'));
+    return (j && j.refresh_token) || null;
+  } catch (_) { return null; }
+}
+
+// Persist the rotated refresh token back into .env so a wiped cache can't
+// strand us with a dead token again.
+function persistRefreshTokenToEnv(refreshToken) {
+  if (!refreshToken) return;
+  const envPath = path.join(__dirname, '.env');
+  try {
+    let env = fs.readFileSync(envPath, 'utf8');
+    if (/^CC_REFRESH_TOKEN=.*$/m.test(env)) {
+      env = env.replace(/^CC_REFRESH_TOKEN=.*$/m, `CC_REFRESH_TOKEN=${refreshToken}`);
+    } else {
+      env += `\nCC_REFRESH_TOKEN=${refreshToken}\n`;
+    }
+    fs.writeFileSync(envPath, env, { mode: 0o600 });
+  } catch (e) {
+    log('WARN: could not persist rotated refresh token to .env:', e.message);
+  }
+}
+
 function writeTokenCache(tok) {
   const expires_at = Date.now() + (Number(tok.expires_in || 0) * 1000);
   const payload = { access_token: tok.access_token, refresh_token: tok.refresh_token, expires_at };
@@ -110,8 +137,11 @@ async function getAccessToken() {
   }
 
   const basic = Buffer.from(`${CC_CLIENT_ID}:${CC_CLIENT_SECRET}`).toString('base64');
+  // Prefer the cache's rotated refresh token (CC single-use rotation) over the
+  // possibly-stale .env copy.
+  const refreshToken = readCachedRefreshToken() || CC_REFRESH_TOKEN;
   const body = new URLSearchParams({
-    refresh_token: CC_REFRESH_TOKEN,
+    refresh_token: refreshToken,
     grant_type: 'refresh_token',
   });
 
@@ -130,6 +160,7 @@ async function getAccessToken() {
   }
   const tok = await res.json();
   const saved = writeTokenCache(tok);
+  persistRefreshTokenToEnv(tok.refresh_token);
   log('refreshed access token (expires', new Date(saved.expires_at).toISOString() + ')');
   return saved.access_token;
 }

← c84a6f8f auto-save: 2026-07-22T12:13:58 (1 files) — mailers/cc-api/bu  ·  back to Designer Wallcoverings  ·  New Arrivals weekly campaign: 12-vendor mailer from live mir 1100dbaa →