← back to George Gmail
docs: service-account + domain-delegation runbook — permanent zero-expiry fix for steve@/info@ Workspace accounts
fbeffde45e694db4512ebc3df058beb91c0fb710 · 2026-07-15 11:18:57 -0700 · Steve
Files touched
A OAUTH-SERVICE-ACCOUNT-RUNBOOK.md
Diff
commit fbeffde45e694db4512ebc3df058beb91c0fb710
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 15 11:18:57 2026 -0700
docs: service-account + domain-delegation runbook — permanent zero-expiry fix for steve@/info@ Workspace accounts
---
OAUTH-SERVICE-ACCOUNT-RUNBOOK.md | 105 +++++++++++++++++++++++++++++++++++++++
1 file changed, 105 insertions(+)
diff --git a/OAUTH-SERVICE-ACCOUNT-RUNBOOK.md b/OAUTH-SERVICE-ACCOUNT-RUNBOOK.md
new file mode 100644
index 0000000..6bef636
--- /dev/null
+++ b/OAUTH-SERVICE-ACCOUNT-RUNBOOK.md
@@ -0,0 +1,105 @@
+# George OAuth — permanent fix for the Workspace accounts (service account + domain-wide delegation)
+
+**Goal:** kill the ~7-day token treadmill **forever** for the two Google **Workspace** accounts
+`steve@designerwallcoverings.com` and `info@designerwallcoverings.com`. No refresh token, no
+consent screen, no CASA verification, $0.
+
+**Does NOT apply to** `steveabramsdesigns@gmail.com` / `theagentabrams@gmail.com` — those are
+**consumer** Gmail, which a service account cannot impersonate. Their only options stay:
+periodic re-consent, or drop them from George.
+
+**Split of labor:** the Google Cloud Console + Workspace Admin steps are **yours** (identity /
+admin — I can't touch them). The George code changes are **mine** (additive, reversible, off
+until the key is present). Nothing breaks the current refresh-token path until you flip it.
+
+---
+
+## Why this works (30 seconds)
+A service account (SA) with **domain-wide delegation** is authorized, once, in your Workspace
+Admin console to impersonate users in `designerwallcoverings.com` for a fixed scope list. George
+then mints a short-lived access token per request by signing a JWT with the SA key + a `subject`
+(the user to act as). Google issues the token directly — there is no user refresh token to expire,
+no "Testing" 7-day cap, and no OAuth consent screen. Internal to your own Workspace, so **no app
+verification / no CASA**.
+
+---
+
+## PART 1 — Google Cloud Console (YOU · ~5 min)
+Use the **same Cloud project** that holds the current `GMAIL_CLIENT_ID`
+(`493480746821-…apps.googleusercontent.com`).
+
+1. **APIs & Services → Enable APIs** — confirm these are enabled (they already are for OAuth, but verify):
+ Gmail API, Google Calendar API, Google Drive API, Google Docs API, Google Sheets API,
+ Google Slides API, Google Tasks API, Google Forms API.
+2. **IAM & Admin → Service Accounts → Create service account**
+ - Name: `george-workspace-impersonation`
+ - Skip the optional roles (delegation is granted in Admin, not via IAM roles). Create.
+3. Open the new SA → **Keys → Add key → Create new key → JSON**. A `.json` downloads. Keep it safe.
+4. Open the SA's details and **copy its numeric Client ID** ("Unique ID", ~21 digits) — you need it in Part 2.
+ (Also confirm **Domain-wide delegation** is enabled on the SA; on newer consoles it's implicit once
+ you authorize the client in Admin in Part 2.)
+
+## PART 2 — Workspace Admin console (YOU · ~3 min)
+`admin.google.com` → **Security → Access and data control → API controls → Domain-wide delegation → Add new**
+
+- **Client ID:** the SA numeric Client ID from Part 1 step 4.
+- **OAuth scopes** (comma-separated — these are the exact 11 George uses):
+ ```
+ https://www.googleapis.com/auth/gmail.modify,
+ https://www.googleapis.com/auth/gmail.settings.sharing,
+ https://www.googleapis.com/auth/gmail.settings.basic,
+ https://www.googleapis.com/auth/tasks,
+ https://www.googleapis.com/auth/drive,
+ https://www.googleapis.com/auth/documents,
+ https://www.googleapis.com/auth/spreadsheets,
+ https://www.googleapis.com/auth/presentations,
+ https://www.googleapis.com/auth/calendar,
+ https://www.googleapis.com/auth/forms.body,
+ https://www.googleapis.com/auth/forms.responses.readonly
+ ```
+- **Authorize.** (Propagation is usually instant, occasionally up to ~30 min.)
+
+## PART 3 — hand me the key (YOU → ME)
+Put the SA JSON key somewhere George can read it and tell me the path, e.g.:
+```
+~/Projects/george-gmail/secrets/george-sa.json (chmod 600; add secrets/ to .gitignore)
+```
+Route it via the `secrets` skill if you prefer — do **not** paste the key contents in chat.
+
+## PART 4 — George code (ME · additive, reversible)
+Once the key exists I add an SA auth path that is used **only** for `steve-office` + `info` when
+`GEORGE_SA_KEY` is set, and otherwise falls back to today's refresh-token client (zero behavior
+change until you flip it). Sketch:
+
+```js
+// lib/sa-auth.js (new file — does not touch the existing OAuth2 clients)
+import { google } from 'googleapis';
+const SA_KEY = process.env.GEORGE_SA_KEY; // path to the JSON key
+const SCOPES = [/* the 11 scopes above */];
+export function saClientFor(subject) { // subject = user email to impersonate
+ const auth = new google.auth.GoogleAuth({ keyFile: SA_KEY, scopes: SCOPES, clientOptions: { subject } });
+ return auth; // google.gmail({version:'v1', auth}) etc.
+}
+```
+Wiring in `server.js`: for the `steve-office` and `info` account entries, if `GEORGE_SA_KEY` is set,
+build their gmail/calendar/drive/etc. clients from `saClientFor('steve@…')` / `saClientFor('info@…')`
+instead of the refresh-token `oauth2Client`. Consumer accounts are untouched.
+
+## PART 5 — roll out + verify (ME with your go)
+1. `GEORGE_SA_KEY=<path>` in `.env`; restart George (local) — then Kamatera (your deploy).
+2. Verify: `GET /api/profile?account=steve-office` and `?account=info` return OK with **no refresh token in play**.
+3. Let it sit past 7 days — the `token-age-warn` CRIT for those two never fires again.
+4. Retire the now-pointless watchers for the Workspace accounts:
+ `com.steve.dw-appointments-oauth-canary` (it watched calendar-push death caused by this exact expiry).
+
+---
+
+## After this
+- **steve@ + info@** → never expire again. This is the fleet-critical win (send path + info@ catch-all across 209 domains).
+- **steve-personal + agentabrams** → still consumer Gmail; still a periodic re-consent (or drop them). No service-account path exists for consumer accounts — that's Google's boundary, not a gap in this plan.
+
+## Provenance
+Extends `PLAN-OAUTH-DURABILITY.md` (2026-07-01, council idea #3) into an executable runbook. The
+"just publish to Production" path is a trap for restricted Gmail/Drive scopes (annual CASA
+assessment) — domain-wide delegation avoids verification entirely because it's internal to your
+own Workspace.
← 2c2c36d chore: version v1.1.1 (session close)
·
back to George Gmail
·
docs: lead the OAuth runbook with the Internal-flip fast pat 76f3426 →