← 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
M mailers/cc-api/cc-client.js
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 →