[object Object]

← back to George Gmail

feat(auth): info@ → Internal client (staggered stage 2) + token-age validates Workspace accts with Internal client

e6b8c5530c9c7a4c93f0a121768529d3d81b3d06 · 2026-09-01 14:31:57 -0700 · Steve Abrams

Files touched

Diff

commit e6b8c5530c9c7a4c93f0a121768529d3d81b3d06
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 14:31:57 2026 -0700

    feat(auth): info@ → Internal client (staggered stage 2) + token-age validates Workspace accts with Internal client
---
 server.js          |  9 ++++++++-
 token-age-warn.mjs | 28 +++++++++++++++++++++-------
 2 files changed, 29 insertions(+), 8 deletions(-)

diff --git a/server.js b/server.js
index b1a5637..9a11f25 100644
--- a/server.js
+++ b/server.js
@@ -144,6 +144,13 @@ try {
 }
 
 // Info@ account (secondary — now full Workspace)
+// Staggered stage 2 (2026-09-01): info@ moves to the new Internal OAuth client
+// (project 964770033153, george-workspace-internal) so its next re-consent mints
+// a durable token. Falls back to the existing client when Internal creds absent.
+// Both /auth/info (mint) + /oauth2callback info branch (exchange) reference this
+// same infoOauth2Client, so the mint/exchange-must-match invariant holds.
+const INFO_ID = process.env.GMAIL_INTERNAL_CLIENT_ID || creds.GMAIL_INTERNAL_CLIENT_ID || GMAIL_CLIENT_ID;
+const INFO_SECRET = process.env.GMAIL_INTERNAL_CLIENT_SECRET || creds.GMAIL_INTERNAL_CLIENT_SECRET || GMAIL_CLIENT_SECRET;
 let infoOauth2Client;
 let infoGmail;
 let infoTasksApi;
@@ -154,7 +161,7 @@ let infoSlides;
 let infoCalendar;
 let infoForms;
 try {
-  infoOauth2Client = new google.auth.OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, INFO_REDIRECT_URI);
+  infoOauth2Client = new google.auth.OAuth2(INFO_ID, INFO_SECRET, INFO_REDIRECT_URI);
   if (INFO_REFRESH_TOKEN) {
     infoOauth2Client.setCredentials({ refresh_token: INFO_REFRESH_TOKEN });
     infoGmail = google.gmail({ version: 'v1', auth: infoOauth2Client });
diff --git a/token-age-warn.mjs b/token-age-warn.mjs
index 1902425..64fb394 100644
--- a/token-age-warn.mjs
+++ b/token-age-warn.mjs
@@ -26,12 +26,18 @@ const DATA = `${HERE}data`; fs.mkdirSync(DATA, { recursive: true });
 const STATE_FILE = `${DATA}/token-age-state.json`;
 const WARN_DAYS = 5, DEAD_DAYS = 7; // Testing-mode refresh tokens die at 7d; nudge at day 5 for 2d runway
 
+// `internal: true` accounts were re-minted under the Internal OAuth client
+// (GMAIL_INTERNAL_CLIENT_ID/SECRET, staggered cutover: steve-office stage 1,
+// info@ stage 2 2026-09-01). They MUST be validated against the Internal client
+// or the probe returns invalid_grant and false-CRITs a healthy token. The rest
+// (personal/agentabrams/calendar) stay on GMAIL_CLIENT_ID. Falls back to the
+// GMAIL client when the Internal creds aren't present in env.
 const ACCOUNTS = [
-  { label: 'Steve Office (steve@dw)',      key: 'GMAIL_REFRESH_TOKEN',           workspace: true  },
-  { label: 'Info@ catch-all',              key: 'INFO_REFRESH_TOKEN',            workspace: true  },
-  { label: 'Steve Personal (@gmail)',      key: 'PERSONAL_REFRESH_TOKEN',        workspace: false },
-  { label: 'Agent Abrams (@gmail)',        key: 'AGENTABRAMS_REFRESH_TOKEN',     workspace: false },
-  { label: 'Calendar / Appointments',      key: 'GOOGLE_CALENDAR_REFRESH_TOKEN', workspace: true  },
+  { label: 'Steve Office (steve@dw)',      key: 'GMAIL_REFRESH_TOKEN',           workspace: true,  internal: true  },
+  { label: 'Info@ catch-all',              key: 'INFO_REFRESH_TOKEN',            workspace: true,  internal: true  },
+  { label: 'Steve Personal (@gmail)',      key: 'PERSONAL_REFRESH_TOKEN',        workspace: false, internal: false },
+  { label: 'Agent Abrams (@gmail)',        key: 'AGENTABRAMS_REFRESH_TOKEN',     workspace: false, internal: false },
+  { label: 'Calendar / Appointments',      key: 'GOOGLE_CALENDAR_REFRESH_TOKEN', workspace: true,  internal: false },
 ];
 const RANK = { OK: 0, UNKNOWN: 0, WARN: 1, CRIT: 2 };
 
@@ -65,6 +71,7 @@ async function probeValid(clientId, clientSecret, refreshToken) {
 
 const creds = loadEnv(ENV_PATH);
 const clientId = creds.GMAIL_CLIENT_ID, clientSecret = creds.GMAIL_CLIENT_SECRET;
+const internalId = creds.GMAIL_INTERNAL_CLIENT_ID, internalSecret = creds.GMAIL_INTERNAL_CLIENT_SECRET;
 const state = fs.existsSync(STATE_FILE) ? JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')) : {};
 const now = Date.now();
 const accounts = [];
@@ -79,12 +86,19 @@ for (const a of ACCOUNTS) {
   const issued_ts = rotated ? now : (prev.issued_ts || now);
   const ageDays = (now - issued_ts) / 86400000;
 
+  // Validate against the client the token was actually minted under: internal
+  // accounts use the Internal client (fall back to GMAIL if it's not in env yet),
+  // everyone else uses GMAIL_CLIENT_ID.
+  const useInternal = a.internal && internalId && internalSecret;
+  const cId = useInternal ? internalId : clientId;
+  const cSecret = useInternal ? internalSecret : clientSecret;
+
   let valid = null, err;
-  if (clientId && clientSecret) ({ valid, err } = await probeValid(clientId, clientSecret, tok));
+  if (cId && cSecret) ({ valid, err } = await probeValid(cId, cSecret, tok));
 
   let level, detail;
   if (valid === false) { level = 'CRIT'; detail = `token DEAD (invalid_grant) — re-consent needed`; }
-  else if (valid === null && !clientId) { level = 'UNKNOWN'; detail = 'no client id/secret to probe'; }
+  else if (valid === null && !cId) { level = 'UNKNOWN'; detail = 'no client id/secret to probe'; }
   else if (valid === null) { level = 'UNKNOWN'; detail = `probe inconclusive (${err})`; }
   else if (ageDays >= DEAD_DAYS) { level = 'OK'; detail = `valid at ${ageDays.toFixed(1)}d — survived past the 7d Testing horizon, so this token is long-lived (Production/Internal/Workspace), not on the weekly clock`; }
   else if (ageDays >= WARN_DAYS) { level = 'WARN'; detail = `valid, ${ageDays.toFixed(1)}d old — expires ~${(DEAD_DAYS - ageDays).toFixed(1)}d if app still in Testing`; }

← a3c366a Reapply "feat(auth): steve-office → Internal OAuth client (s  ·  back to George Gmail  ·  auto-data-snapshot: 2026-09-02T04:39:22 (1 data files) — dat 5378a85 →