# Google OAuth Alignment Runbook (April 7, 2026)

> **Retirement notice:** This is point-in-time evidence. Shared-development
> callbacks, project commands, and client mappings are retained only to support
> approval-gated external cleanup.

Reference procedure:

- `public/docs/OAUTH_PROVIDER_UPDATE_RUNBOOK_2026-03-19.md`.

This runbook applies the March 19 procedure specifically to Google OAuth and locks each environment to its dedicated Google OAuth 2.0 client.

## Audit Snapshot

Verified in the local workspace on April 7, 2026:

- `.env.local` already points at the dedicated `stratofusion-local` Google client and the correct local redirect URI.
- Untracked local snapshot files `.env.development`, `.env.production`, `.env.vercel.development`, and `.env.vercel.production` were corrected so they no longer reference the deleted shared Google client.
- Active Vercel team slug: `rikster1s-projects`.
- Vercel dev project: `stratofusion-dev` (`prj_IyHLvDsGAbyCe90tlIyPsykjUBBy`).
- Vercel prod project: `stratofusion-prod` (`prj_Bl0x4WPdmoiKALs8AD54wc5ZOLWg`).

## Git Bash Session Setup

Run from the repo root in Git Bash on Windows 11.

```bash
export VERCEL_SCOPE="rikster1s-projects"

export GOOGLE_CLIENT_ID_LOCAL="856037110351-7cjkts1e8ut7pp03a5nbqaobhot8jnuf.apps.googleusercontent.com"
export GOOGLE_CLIENT_SECRET_LOCAL="<secret-for-stratofusion-local-client>"
export GOOGLE_REDIRECT_URI_LOCAL="http://localhost:3000/api/google"

export GOOGLE_CLIENT_ID_DEV="856037110351-ho0oim9es4d9652kdt95ij4r19umdq32.apps.googleusercontent.com"
export GOOGLE_CLIENT_SECRET_DEV="<secret-for-stratofusion-dev-client>"
export GOOGLE_REDIRECT_URI_DEV="https://dev.stratofusion.io/api/google"

export GOOGLE_CLIENT_ID_PROD="856037110351-3k9mtp3tg364lf8qlesdl37t6t4lqf0e.apps.googleusercontent.com"
export GOOGLE_CLIENT_SECRET_PROD="<secret-for-stratofusion-prod-client>"
export GOOGLE_REDIRECT_URI_PROD="https://stratofusion.io/api/google"
```

## Google Cloud Console Checklist

Use the Google Cloud Console OAuth 2.0 Client IDs screen and verify each client separately.

Shared checks for all three clients:

1. Open the correct OAuth 2.0 Client ID by name and client ID.
2. Confirm application type is `Web application`.
3. Confirm the client belongs to the intended StratoFusion environment only.
4. Confirm `Google Drive API` is enabled in the project that owns the client.
5. Confirm the deleted shared `StratoFusion` client is not referenced anywhere in Vercel, local env files, or operator notes.
6. Copy the client secret for the matching client into the Git Bash exports above.

Client-specific checks:

- `stratofusion-prod`.
  - Client ID must be `856037110351-3k9mtp3tg364lf8qlesdl37t6t4lqf0e.apps.googleusercontent.com`.
  - Authorized redirect URIs must include `https://stratofusion.io/api/google`.
  - Remove any dev or localhost redirect URI from this client.

- `stratofusion-dev`.
  - Client ID must be `856037110351-ho0oim9es4d9652kdt95ij4r19umdq32.apps.googleusercontent.com`.
  - Authorized redirect URIs must include `https://dev.stratofusion.io/api/google`.
  - Remove any prod or localhost redirect URI from this client.

- `stratofusion-local`.
  - Client ID must be `856037110351-7cjkts1e8ut7pp03a5nbqaobhot8jnuf.apps.googleusercontent.com`.
  - Authorized redirect URIs must include `http://localhost:3000/api/google`.
  - Remove any prod or dev redirect URI from this client.

Do not edit or reuse:

- `StratoFusion`.
  - This shared client was deleted during the April 7, 2026 cleanup.

## Unused-client warnings and lifecycle policy

Google may warn project owners before deleting an OAuth client that it considers
unused. Treat the client ID in the warning as the source of truth; the display
name alone is not enough to identify an environment.

There is no StratoFusion code or Google Cloud Console setting that permanently
marks an idle client as "keep forever." Resolve every warning by choosing one of
these outcomes:

1. **The client is obsolete:** verify that its full client ID is absent from
   Vercel, local environment files, Clerk custom Google credentials, operator
   secrets, and the runtime OAuth URL. Delete the client in Google Cloud Console.
2. **The client serves production or dev:** verify the environment mapping below,
   then complete a real end-to-end authorization in that environment before the
   deadline in Google's notice. A successful token refresh alone must not be
   assumed to count as client use. Re-run the runtime URL and callback checks in
   this runbook.
3. **The client is local-only:** use it for a real local authorization when local
   development is active. If local Google OAuth is intentionally dormant, allow
   the client to be retired and create a new localhost-only client when it is
   next needed; do not manufacture background consent traffic solely to preserve
   an unused credential.

Never keep a client alive by putting production, dev, and localhost callbacks on
one client. Environment isolation remains more important than preserving an
inactive client ID.

### Quarterly ownership review

Run this review every quarter and whenever Google sends an unused-client notice:

1. Export the OAuth client list from the owning Google Cloud project and record
   each client ID suffix, display name, owner, environment, and expected callback
   in the private operator inventory. Do not put client secrets in the inventory
   or this repository.
2. Compare each active client with the three approved IDs in this runbook and
   with the corresponding Vercel and Clerk configuration.
3. Run the deployed runtime OAuth URL checks for dev and production. For each
   environment still in active use, perform a real browser authorization using a
   designated test account and record the date and result in the private
   inventory.
4. Delete clients that have no owner or runtime consumer after the absence checks
   above. Rotate credentials immediately if a warning names an unknown client or
   if its secret may have been exposed.

This policy makes the durable state explicit: production and active dev clients
are exercised by real release smoke tests, local clients are disposable, and
orphaned clients are deleted instead of being kept alive indefinitely.

## Vercel Update Commands

Important:

- Use `production` as the target in both Vercel projects. In this repo, each Vercel project represents its own deployed environment, so `stratofusion-dev` still needs its Google vars set in that project's `production` target.
- Secrets must be added with `--sensitive`.

### Update `stratofusion-dev`

```bash
vercel link --scope "$VERCEL_SCOPE" --project stratofusion-dev --yes

printf '%s' "$GOOGLE_CLIENT_ID_DEV" | vercel env add GOOGLE_CLIENT_ID production --force --scope "$VERCEL_SCOPE"
printf '%s' "$GOOGLE_CLIENT_SECRET_DEV" | vercel env add GOOGLE_CLIENT_SECRET production --force --sensitive --scope "$VERCEL_SCOPE"
printf '%s' "$GOOGLE_REDIRECT_URI_DEV" | vercel env add GOOGLE_REDIRECT_URI production --force --scope "$VERCEL_SCOPE"

vercel deploy --prod --yes --scope "$VERCEL_SCOPE"
```

### Update `stratofusion-prod`

```bash
vercel link --scope "$VERCEL_SCOPE" --project stratofusion-prod --yes

printf '%s' "$GOOGLE_CLIENT_ID_PROD" | vercel env add GOOGLE_CLIENT_ID production --force --scope "$VERCEL_SCOPE"
printf '%s' "$GOOGLE_CLIENT_SECRET_PROD" | vercel env add GOOGLE_CLIENT_SECRET production --force --sensitive --scope "$VERCEL_SCOPE"
printf '%s' "$GOOGLE_REDIRECT_URI_PROD" | vercel env add GOOGLE_REDIRECT_URI production --force --scope "$VERCEL_SCOPE"

vercel deploy --prod --yes --scope "$VERCEL_SCOPE"
```

## Local `.env.local` Update

This updates only the Google keys in `.env.local` and leaves other values untouched.

```bash
node - <<'EOF'
const fs = require('fs');
const path = '.env.local';
const updates = {
  GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID_LOCAL,
  GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET_LOCAL,
  GOOGLE_REDIRECT_URI: process.env.GOOGLE_REDIRECT_URI_LOCAL,
};

let text = fs.existsSync(path) ? fs.readFileSync(path, 'utf8') : '';

for (const [key, value] of Object.entries(updates)) {
  if (!value) {
    throw new Error(`Missing required value for ${key}`);
  }

  const line = `${key}=${value}`;
  const pattern = new RegExp(`^${key}=.*$`, 'm');

  if (pattern.test(text)) {
    text = text.replace(pattern, line);
  } else {
    if (text.length > 0 && !text.endsWith('\n')) {
      text += '\n';
    }
    text += `${line}\n`;
  }
}

fs.writeFileSync(path, text);
EOF
```

## Refresh Stale Local Snapshot Files

These files are untracked local artifacts. Regenerate them after Vercel updates so they stay aligned with the dedicated environment clients.

```bash
rm -f .env.vercel.development .env.vercel.production

vercel link --scope "$VERCEL_SCOPE" --project stratofusion-dev --yes
vercel env pull .env.vercel.development --environment=production --yes --scope "$VERCEL_SCOPE"

vercel link --scope "$VERCEL_SCOPE" --project stratofusion-prod --yes
vercel env pull .env.vercel.production --environment=production --yes --scope "$VERCEL_SCOPE"
```

If `.env.development` and `.env.production` are still carrying copied Google credentials from the deleted client, update or remove those local snapshots as well.

## Verification Commands

### 1. Verify Vercel Environment Values

Pull each project's `production` envs to a temporary file, confirm the non-secret values, confirm the secret exists, then delete the temp file.

```bash
vercel link --scope "$VERCEL_SCOPE" --project stratofusion-dev --yes
vercel env pull .env.verify.stratofusion-dev --environment=production --yes --scope "$VERCEL_SCOPE"
grep '^GOOGLE_CLIENT_ID=' .env.verify.stratofusion-dev
grep '^GOOGLE_REDIRECT_URI=' .env.verify.stratofusion-dev
grep '^GOOGLE_CLIENT_SECRET=' .env.verify.stratofusion-dev >/dev/null && echo 'GOOGLE_CLIENT_SECRET is present for stratofusion-dev'
rm -f .env.verify.stratofusion-dev

vercel link --scope "$VERCEL_SCOPE" --project stratofusion-prod --yes
vercel env pull .env.verify.stratofusion-prod --environment=production --yes --scope "$VERCEL_SCOPE"
grep '^GOOGLE_CLIENT_ID=' .env.verify.stratofusion-prod
grep '^GOOGLE_REDIRECT_URI=' .env.verify.stratofusion-prod
grep '^GOOGLE_CLIENT_SECRET=' .env.verify.stratofusion-prod >/dev/null && echo 'GOOGLE_CLIENT_SECRET is present for stratofusion-prod'
rm -f .env.verify.stratofusion-prod
```

Expected values:

- `stratofusion-dev`.
  - `GOOGLE_CLIENT_ID=856037110351-ho0oim9es4d9652kdt95ij4r19umdq32.apps.googleusercontent.com`.
  - `GOOGLE_REDIRECT_URI=https://dev.stratofusion.io/api/google`.

- `stratofusion-prod`.
  - `GOOGLE_CLIENT_ID=856037110351-3k9mtp3tg364lf8qlesdl37t6t4lqf0e.apps.googleusercontent.com`.
  - `GOOGLE_REDIRECT_URI=https://stratofusion.io/api/google`.

### 2. Verify Runtime OAuth URL Generation

The app returns a JSON payload from `/api/google` containing the Google auth URL. This checks the deployed route is emitting the correct `client_id`, `redirect_uri`, and `prompt`.

```bash
verify_google_oauth_route() {
  local endpoint="$1"
  local expected_client_id="$2"
  local expected_redirect_uri="$3"

  local response
  response="$(curl -fsS "$endpoint")"

  RESPONSE="$response" \
  EXPECTED_CLIENT_ID="$expected_client_id" \
  EXPECTED_REDIRECT_URI="$expected_redirect_uri" \
  node - <<'EOF'
const payload = JSON.parse(process.env.RESPONSE);
const authUrl = new URL(payload.url);
const actualClientId = authUrl.searchParams.get('client_id');
const actualRedirectUri = authUrl.searchParams.get('redirect_uri');
const actualPrompt = authUrl.searchParams.get('prompt');

if (actualClientId !== process.env.EXPECTED_CLIENT_ID) {
  console.error(`client_id mismatch: expected ${process.env.EXPECTED_CLIENT_ID}, got ${actualClientId}`);
  process.exit(1);
}

if (actualRedirectUri !== process.env.EXPECTED_REDIRECT_URI) {
  console.error(`redirect_uri mismatch: expected ${process.env.EXPECTED_REDIRECT_URI}, got ${actualRedirectUri}`);
  process.exit(1);
}

if (actualPrompt !== 'consent select_account') {
  console.error(`prompt mismatch: expected consent select_account, got ${actualPrompt}`);
  process.exit(1);
}

console.log(`OK ${actualClientId} -> ${actualRedirectUri}`);
EOF
}

verify_google_oauth_route \
  "https://dev.stratofusion.io/api/google?popup=true&intent=business" \
  "$GOOGLE_CLIENT_ID_DEV" \
  "$GOOGLE_REDIRECT_URI_DEV"

verify_google_oauth_route \
  "https://stratofusion.io/api/google?popup=true&intent=business" \
  "$GOOGLE_CLIENT_ID_PROD" \
  "$GOOGLE_REDIRECT_URI_PROD"
```

For local, start the app first, then run:

```bash
verify_google_oauth_route \
  "http://localhost:3000/api/google?popup=true&intent=business" \
  "$GOOGLE_CLIENT_ID_LOCAL" \
  "$GOOGLE_REDIRECT_URI_LOCAL"
```

### 3. Manual Browser Verification

Confirm each environment completes the full Google OAuth callback and lands back on the same-origin callback route:

- local returns through `http://localhost:3000/api/google`.
- dev returns through `https://dev.stratofusion.io/api/google`.
- prod returns through `https://stratofusion.io/api/google`.

## Success Criteria

The cutover is complete when all of the following are true:

1. Google Cloud Console shows exactly one matching redirect URI on each dedicated client.
2. `stratofusion-dev` uses only `856037110351-ho0oim9es4d9652kdt95ij4r19umdq32.apps.googleusercontent.com`.
3. `stratofusion-prod` uses only `856037110351-3k9mtp3tg364lf8qlesdl37t6t4lqf0e.apps.googleusercontent.com`.
4. Local uses only `856037110351-7cjkts1e8ut7pp03a5nbqaobhot8jnuf.apps.googleusercontent.com`.
5. No file or Vercel env output still references the deleted shared `StratoFusion` client.
