← back to George Gmail

PLAN-OAUTH-DURABLE-TOKENS.md

80 lines

# George OAuth — ending the weekly-token treadmill

_Decision memo. Read-only inventory + migration paths. The migration itself is
Steve-gated (Google Cloud console + Workspace admin console + possible CASA spend)._

Authored 2026-07-01, Council #3 (vp-engineering, "kill the George OAuth weekly-token treadmill").

## The problem

The Cloud OAuth app is in **"Testing" publishing status → refresh tokens expire in 7 days.**
Every week the tokens die, calendar push + fleet mail silently stop, and the
`dw-appointments-oauth-canary` just *watches the symptom*. This is monitoring debt:
we built an alarm for a wound instead of removing the cause.

## Inventory (from `server.js`)

**ONE OAuth client** (`GMAIL_CLIENT_ID` / `GMAIL_CLIENT_SECRET`) is reused across **4 accounts**,
each holding its own `*_REFRESH_TOKEN` that the app rewrites into `.env` on re-consent:

| Account key | Address | Type | Notes |
|---|---|---|---|
| GMAIL       | steve@designerwallcoverings.com | **Workspace** | fleet send path (daily-overnight-skus, digests, reminders) |
| INFO        | info@designerwallcoverings.com  | **Workspace** | the 209-domain catch-all forwards here |
| PERSONAL    | steveabramsdesigns@gmail.com    | consumer @gmail | |
| AGENTABRAMS | theagentabrams@gmail.com        | consumer @gmail | |

**Flow:** `access_type: 'offline'` + `prompt: 'consent'`, Web-application client (redirect URIs
present). Standard installed/web OAuth — NOT a service account.

**Scopes requested:**
- **Restricted:** `gmail.modify`, `gmail.settings.basic`, `gmail.settings.sharing`, `drive`
- **Sensitive:** `calendar`, `documents`, `spreadsheets`, `presentations`, `forms.body`,
  `forms.responses.readonly`, `tasks`

## The decisive constraint = restricted scopes

An **External + Production** app that keeps restricted scopes (`gmail.modify`, `drive`)
triggers Google's **annual CASA third-party security assessment** (~$500–$4,500/yr) + brand
verification. So "just push the app to Production" is the expensive path. Avoid it if possible.

## Recommended migration — split by account type

### Workspace accounts (steve@ + info@) — the accounts that actually matter
These carry the entire fleet send path and the 209-domain catch-all. Two verification-free fixes:

1. **BEST — Service account + domain-wide delegation (DWD)** in the Workspace admin console.
   Tokens are minted per-request from the SA key: **no refresh token, no 7-day expiry, no
   consent screen, no verification, $0 ongoing.** Requires a small code change (swap the
   OAuth2Client for a JWT/SA client that impersonates the two Workspace users).
2. **CHEAPEST if the Cloud project lives in the designerwallcoverings.com org — flip consent
   screen User Type → "Internal".** One console toggle makes Workspace tokens durable, **no
   code change.** Caveat: Internal apps can only be authorized by users *in the org*, so the
   two consumer @gmail accounts break and need their own separate (External) client — which
   they need anyway once the client is split.

### Consumer @gmail accounts (personal + agentabrams)
Can't use SA/DWD or Internal. Options, cheapest first:
- **(a)** Fold their duties onto the Workspace (e.g. a send-as alias on steve@) and **retire
  consumer OAuth entirely** → everything becomes SA/DWD. Cleanest end state.
- **(b)** Give them a separate OAuth client requesting only **non-restricted** scopes to dodge
  CASA (leaves just free brand verification).
- **(c)** Keep weekly re-consent but add a **pre-emptive 6-day WARN** so it's a scheduled
  30-second task, not a silent failure. (Shipped now — see `token-age-warn.mjs`, this is the
  reversible stopgap while the migration decision is pending.)

## The gated fork — two questions decide the plan (Steve answers)

1. **Is the Cloud OAuth project inside the designerwallcoverings.com Workspace org?**
   - Yes → the "Internal" toggle is a ~30-second durable fix for steve@ + info@.
   - No / consumer-project → go straight to service-account + DWD for the two Workspace users.
2. **For the 2 consumer accounts — fold into Workspace (retire their OAuth), or keep them on
   weekly re-consent with the new WARN?**

## What shipped now (ungated, reversible, local)
- `token-age-warn.mjs` — read-only pre-emptive token-age clock. On each run it records the
  last-good timestamp per account and emits a CNCP WARN + George email when any token crosses
  ~6 days while still valid, so Steve can re-consent on schedule instead of mid-pipeline. This
  removes the *silent* part of the failure; it does NOT remove the treadmill (the migration
  above does).