← back to Oauth Proxy Design
initial scaffold (gitify-all 2026-05-06)
fe72b78ead07b13009374706341606762ed16ef1 · 2026-05-06 10:25:44 -0700 · Steve Abrams
Files touched
Diff
commit fe72b78ead07b13009374706341606762ed16ef1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 10:25:44 2026 -0700
initial scaffold (gitify-all 2026-05-06)
---
.gitignore | 12 ++
DESIGN.md | 610 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 622 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e6a9c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+.env
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+*.bak
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..8a77fde
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,610 @@
+# OAuth Proxy Design — Mac2 pm2 Fleet
+**Owner:** theagentabrams@gmail.com
+**Date:** 2026-05-04
+**Status:** Design / Not yet implemented
+
+---
+
+## TL;DR
+
+Replace shared Basic Auth (`admin:DWSecure2024!`) across 18+ Mac2 pm2 services with a single Google SSO gate.
+**Recommended tool: oauth2-proxy** running as a pm2 process on Mac2, fronted by **Caddy** (single binary, no config hell).
+Traffic path: browser → Caddy (Mac2 tailnet IP, port 443) → oauth2-proxy (port 4180) → backend service (localhost:XXXX).
+One Google login covers all services. Session stored in a Redis-backed cookie. Audit log written to NDJSON file, queryable by `jq`.
+Total estimated effort: **12–16 hours** (including Caddy setup, Google Cloud credential registration, rollout of 18 services, and audit wiring).
+
+---
+
+## Problem
+
+| What | Detail |
+|------|--------|
+| Shared secret | `admin:DWSecure2024!` — same credential in every service, every browser, every script |
+| No audit trail | No record of who hit which service when |
+| No revocation granularity | Compromising one service compromises all |
+| No SSO | Each service prompts separately; clearing cookies locks you out of everything |
+| Exposure surface | Tailnet only mitigates network, not credential theft or phishing |
+
+---
+
+## Service Inventory (18 admin/viewer services)
+
+These are the services currently protected by Basic Auth or that expose admin UIs without any auth:
+
+| Service | Port | Notes |
+|---------|------|-------|
+| morning-review | 9762 | Daily findings viewer |
+| video-gallery | 9763 | MP4 session viewer |
+| sf-orchestrator | 9880 | Site Factory control |
+| sf-viewer | 9881 | Site Factory status |
+| sf-admin | (sf-admin) | Site Factory admin gate |
+| ai-factory-orchestrator | 9890 | AI Factory control |
+| ai-factory-viewer | 9891 | AI Factory status |
+| visual-factory-orchestrator | 9892 | Visual Factory control |
+| visual-factory-viewer | 9893 | Visual Factory status |
+| animals-viewer | 9720 | Animals directory viewer |
+| animals-audit | 9721 | Animals audit panel |
+| pd-preview | 9875 | Doctor directory preview |
+| pd-web | 9876 | Doctor directory public |
+| george-gmail | 9850 | Gmail send API |
+| launch-supervisor | 9843 | Launch supervisor |
+| vendor-command-center | 9660 | Vendor admin |
+| debate-ring | (debate-ring) | Debate viewer |
+| yolo-agent | 9670 | YOLO agent UI |
+
+> Note: Services like `norma-*`, `product-agent`, and backend workers (ingest, enrich, dedupe) that are loopback-only and have no browser UI are excluded — they don't need SSO.
+
+---
+
+## Tool Comparison
+
+| | **oauth2-proxy** | **Authelia** | **Pomerium** |
+|---|---|---|---|
+| **Complexity** | Low — single Go binary, ~30 lines of config | Medium — requires its own config YAML + Redis + optional LDAP | Medium-High — Go binary but needs upstream policy DSL; more suited to multi-user orgs |
+| **Google OAuth support** | Native, first-class | Yes, via OIDC provider | Yes, via OIDC |
+| **Session backend** | Cookie (HMAC-signed) or Redis | File / Redis / DB | Redis or in-process |
+| **Audit log** | Request log via stdout/file; `--request-logging` flag | Built-in access.log to file/stdout | Access log to stdout; paid tiers add audit dashboards |
+| **Per-service revocation** | Clear cookie for that upstream only (with path-based routing each service gets its own cookie path) | Session manager UI; revoke individual sessions | Policy-level; session invalidation via API |
+| **Proxy layer needed** | Yes — needs Caddy/nginx in front for TLS + routing | Yes | Yes, or self-terminates TLS |
+| **License** | MIT | Apache 2.0 | BSL (free for self-hosted <3 instances) |
+| **Maturity / stars (2026)** | ~10k GitHub stars, widely deployed | ~22k stars | ~13k stars |
+| **Best for** | Single-owner, all-Google, path/subdomain routing | Multi-user, MFA requirements, enterprise features | Zero-trust, fine-grained policy per route |
+| **Verdict** | **Recommended — minimal ops overhead for solo-owner setup** | Overkill: LDAP/MFA features not needed here | Overkill: policy engine not needed; BSL license risk |
+
+---
+
+## Recommended Architecture
+
+### Components
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Your Browser (tailnet-only access) │
+│ https://stevestudio2s-mac-studio.tail79cb8e.ts.net/... │
+└───────────────────────┬─────────────────────────────────────┘
+ │ HTTPS (Caddy TLS — tailnet cert)
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ Caddy (Mac2 :443) │
+│ - Terminates TLS via Tailscale's auto-HTTPS (ts.net cert) │
+│ - Path-based routing: /morning-review/* → :4180 │
+│ - Adds X-Forwarded-For, X-Real-IP headers │
+│ - Optionally direct-proxies non-auth services (health/API) │
+└───────────────────────┬─────────────────────────────────────┘
+ │ HTTP localhost:4180
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ oauth2-proxy (Mac2 :4180, pm2 process "oauth-proxy") │
+│ - Google OIDC: theagentabrams@gmail.com allowed │
+│ - Signs cookie: _oauth2_proxy (HMAC-SHA256, 24h TTL) │
+│ - Writes access log: ~/Logs/oauth-proxy/access.ndjson │
+│ - /oauth2/callback, /oauth2/sign_out, /oauth2/userinfo │
+│ - Upstream: header X-Forwarded-Uri → route to backend │
+└───────────────────────┬─────────────────────────────────────┘
+ │ HTTP localhost:XXXX (per-service)
+ ▼
+┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
+│morning-review│ │ sf-viewer │ │ ai-factory │ │ george-gmail│
+│ :9762 │ │ :9881 │ │ :9891 │ │ :9850 │
+└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
+ ...18 services total, all on localhost, never exposed to tailnet
+```
+
+### Key design decisions
+
+1. **Tailscale auto-HTTPS** — Mac2's tailnet hostname (`stevestudio2s-mac-studio.tail79cb8e.ts.net`) gets a free TLS cert from Tailscale's MagicDNS/Let's Encrypt integration. Caddy uses the `tailscale` TLS provider. Zero cert management.
+
+2. **Caddy, not nginx** — nginx is not installed on Mac2 (confirmed). Caddy is a single `brew install caddy` binary, runs as a pm2 process. No system daemon required. Caddyfile is 40 lines total.
+
+3. **Path-based routing, not subdomain-per-service** — eliminates need for 18 DNS records. All services live under `https://mac2.tail.../morning-review/`, `https://mac2.tail.../sf-viewer/`, etc. Cookie scope is `/` so one login covers all paths.
+
+4. **oauth2-proxy as pm2 process** — consistent with how everything else on Mac2 is managed. `pm2 start oauth2-proxy -- --config /Users/stevestudio2/Projects/oauth-proxy/config.cfg`.
+
+5. **Allowed email whitelist** — `--email-domain=` is too broad. Use `--authenticated-emails-file` pointing to a file containing `theagentabrams@gmail.com`. Adding a second person = one line added.
+
+6. **Per-service cookie path** — to enable per-service revocation, oauth2-proxy is run in **upstream routing mode**: each service gets a distinct `--upstream` entry. For revocation of a single service, set a server-side blocklist entry (see audit log section) or use `--cookie-path=/morning-review/` per instance. For simplicity, Phase 1 uses a single shared session; Phase 2 can fork into per-service oauth2-proxy instances.
+
+---
+
+## Audit Log
+
+oauth2-proxy emits a structured access log per request when `--request-logging=true` and `--request-logging-format` is set to JSON. Pipe stdout to a file via pm2's log path.
+
+Log entries look like:
+```json
+{"level":"info","ts":"2026-05-04T09:15:33Z","logger":"oauthproxy","msg":"GET /morning-review/ 200","remote_addr":"100.65.187.120","user":"theagentabrams@gmail.com","upstream":"morning-review","latency_ms":3}
+```
+
+Query examples:
+```bash
+# All requests from last hour
+jq 'select(.ts > "2026-05-04T08:")' ~/Logs/oauth-proxy/access.ndjson
+
+# Which services accessed today
+jq -r .upstream ~/Logs/oauth-proxy/access.ndjson | sort | uniq -c | sort -rn
+
+# 401/403 rejections (non-owner attempts)
+jq 'select(.msg | contains("401") or contains("403"))' ~/Logs/oauth-proxy/access.ndjson
+```
+
+Log rotation: `logrotate` config or a nightly launchd job (`find ~/Logs/oauth-proxy -mtime +30 -delete`).
+
+---
+
+## Per-Service Revocation
+
+**Phase 1 (single proxy instance):**
+oauth2-proxy stores session state in a signed cookie. Signing key is `--cookie-secret`. Rotating the secret invalidates ALL sessions (all services). This is the "nuke everything" approach — fine for a solo operator. Takes 30 seconds.
+
+**Phase 2 (per-service instances, optional):**
+Run one oauth2-proxy per service, each on its own port (e.g., 4181=morning-review, 4182=sf-viewer). Caddy routes to the matching proxy. Each proxy has an independent cookie secret. Revoking service X = `pm2 restart oauth-proxy-morning-review` with a new `COOKIE_SECRET` env var. The other 17 sessions are unaffected.
+
+This is a configuration multiplier (18 plist entries), not a code change. The Phase 2 migration script is a `for` loop over the service map.
+
+---
+
+## Network Perimeter
+
+**Recommendation: Tailnet-only.**
+Bind Caddy to the Tailscale interface IP only (`100.65.187.120:443`), not `0.0.0.0:443`. This means:
+- LAN access (192.168.x.x) does NOT reach the proxy — must be on Tailscale
+- Airplane mode / no network = no access (see drawbacks)
+- Kamatera and other tailnet nodes reach Mac2 services only if their Tailscale ACL permits it
+
+To allow LAN as a fallback (e.g., office desk access without Tailscale):
+Bind Caddy additionally to `192.168.1.X:443` with a self-signed cert. Keep the tailnet binding as primary. This is optional and noted in the Caddyfile below.
+
+---
+
+## Configuration Artifacts
+
+### Google Cloud OAuth setup (one-time)
+
+1. Go to console.cloud.google.com → existing project (or create `dw-agents-sso`)
+2. APIs & Services → Credentials → Create OAuth Client ID
+3. Application type: **Web application**
+4. Authorized redirect URIs:
+ - `https://stevestudio2s-mac-studio.tail79cb8e.ts.net/oauth2/callback`
+5. Save CLIENT_ID and CLIENT_SECRET to secrets manager:
+ ```bash
+ node ~/Projects/secrets-manager/cli.js add OAUTH_PROXY_CLIENT_ID "xxxx.apps.googleusercontent.com"
+ node ~/Projects/secrets-manager/cli.js add OAUTH_PROXY_CLIENT_SECRET "GOCSPX-xxxx"
+ ```
+
+### oauth2-proxy config file
+
+Path: `/Users/stevestudio2/Projects/oauth-proxy/config.cfg`
+
+```ini
+# oauth2-proxy config — Mac2 DW-Agents fleet
+# https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview
+
+provider = "google"
+client-id = "${OAUTH_PROXY_CLIENT_ID}"
+client-secret = "${OAUTH_PROXY_CLIENT_SECRET}"
+redirect-url = "https://stevestudio2s-mac-studio.tail79cb8e.ts.net/oauth2/callback"
+
+# Allowlist — single file, one email per line
+authenticated-emails-file = "/Users/stevestudio2/Projects/oauth-proxy/allowed-emails.txt"
+
+# Cookie settings
+cookie-secret = "${OAUTH_PROXY_COOKIE_SECRET}" # generate: openssl rand -base64 32
+cookie-secure = true
+cookie-httponly = true
+cookie-samesite = "lax"
+cookie-expire = "24h"
+cookie-refresh = "1h"
+cookie-name = "_dw_sso"
+
+# Upstream: path-based, Caddy handles the actual routing
+# oauth2-proxy receives the full URL from Caddy via X-Forwarded-Uri
+# and validates auth; on success passes to upstream via Caddy's proxy
+# (run oauth2-proxy in "reverse proxy" mode, NOT standalone upstream mode)
+# Caddy → oauth2-proxy auth check → Caddy → backend
+http-address = "127.0.0.1:4180"
+
+# Logging
+request-logging = true
+request-logging-format = '{"ts":"{{.Timestamp}}","user":"{{.Username}}","method":"{{.Method}}","path":"{{.Path}}","status":{{.StatusCode}},"upstream":"{{.Upstream}}","latency_ms":{{.RequestDuration}}}'
+standard-logging = true
+
+# Session
+session-store-type = "cookie"
+
+# Reverse proxy mode — do not rewrite upstream URLs
+skip-provider-button = true
+```
+
+`/Users/stevestudio2/Projects/oauth-proxy/allowed-emails.txt`:
+```
+theagentabrams@gmail.com
+```
+
+### Caddyfile
+
+Path: `/Users/stevestudio2/Projects/oauth-proxy/Caddyfile`
+
+```caddyfile
+# Bind to Tailscale IP only for tailnet-only access
+{
+ # Global options
+ admin off
+ log {
+ output file /Users/stevestudio2/Logs/caddy/access.log
+ format json
+ }
+}
+
+# Tailnet hostname — Caddy uses Tailscale TLS automatically
+stevestudio2s-mac-studio.tail79cb8e.ts.net {
+ tls {
+ # Tailscale provisions cert automatically via its ACME endpoint
+ # If using tailscale serve instead, omit this block
+ }
+
+ # oauth2-proxy auth subrequest for all admin routes
+ # Pattern: all paths except /oauth2/* are auth-gated
+ @needs_auth not path /oauth2/*
+ forward_auth @needs_auth localhost:4180 {
+ uri /oauth2/auth
+ copy_headers X-Auth-Request-User X-Auth-Request-Email X-Auth-Request-Access-Token
+ }
+
+ # OAuth2-proxy callback and sign-out (unprotected)
+ handle /oauth2/* {
+ reverse_proxy localhost:4180
+ }
+
+ # ── Service routes ─────────────────────────────────────────────
+ handle /morning-review/* {
+ uri strip_prefix /morning-review
+ reverse_proxy localhost:9762
+ }
+
+ handle /video-gallery/* {
+ uri strip_prefix /video-gallery
+ reverse_proxy localhost:9763
+ }
+
+ handle /sf-orchestrator/* {
+ uri strip_prefix /sf-orchestrator
+ reverse_proxy localhost:9880
+ }
+
+ handle /sf-viewer/* {
+ uri strip_prefix /sf-viewer
+ reverse_proxy localhost:9881
+ }
+
+ handle /ai-factory-viewer/* {
+ uri strip_prefix /ai-factory-viewer
+ reverse_proxy localhost:9891
+ }
+
+ handle /ai-factory/* {
+ uri strip_prefix /ai-factory
+ reverse_proxy localhost:9890
+ }
+
+ handle /visual-factory-viewer/* {
+ uri strip_prefix /visual-factory-viewer
+ reverse_proxy localhost:9893
+ }
+
+ handle /visual-factory/* {
+ uri strip_prefix /visual-factory
+ reverse_proxy localhost:9892
+ }
+
+ handle /animals-viewer/* {
+ uri strip_prefix /animals-viewer
+ reverse_proxy localhost:9720
+ }
+
+ handle /animals-audit/* {
+ uri strip_prefix /animals-audit
+ reverse_proxy localhost:9721
+ }
+
+ handle /pd-preview/* {
+ uri strip_prefix /pd-preview
+ reverse_proxy localhost:9875
+ }
+
+ handle /pd-web/* {
+ uri strip_prefix /pd-web
+ reverse_proxy localhost:9876
+ }
+
+ handle /george-gmail/* {
+ uri strip_prefix /george-gmail
+ reverse_proxy localhost:9850
+ }
+
+ handle /launch-supervisor/* {
+ uri strip_prefix /launch-supervisor
+ reverse_proxy localhost:9843
+ }
+
+ handle /vendor-command-center/* {
+ uri strip_prefix /vendor-command-center
+ reverse_proxy localhost:9660
+ }
+
+ handle /debate-ring/* {
+ uri strip_prefix /debate-ring
+ reverse_proxy localhost:9075
+ }
+
+ handle /yolo-agent/* {
+ uri strip_prefix /yolo-agent
+ reverse_proxy localhost:9670
+ }
+
+ # Dashboard — shows all service links
+ handle / {
+ respond `
+<html><head><title>DW-Agents Hub</title></head><body>
+<h2>DW-Agents — Mac2 Services</h2>
+<ul>
+ <li><a href="/morning-review/">Morning Review</a></li>
+ <li><a href="/video-gallery/">Video Gallery</a></li>
+ <li><a href="/sf-viewer/">Site Factory Viewer</a></li>
+ <li><a href="/sf-orchestrator/">Site Factory Orchestrator</a></li>
+ <li><a href="/ai-factory-viewer/">AI Factory Viewer</a></li>
+ <li><a href="/visual-factory-viewer/">Visual Factory Viewer</a></li>
+ <li><a href="/animals-viewer/">Animals Viewer</a></li>
+ <li><a href="/pd-preview/">Professional Directory Preview</a></li>
+ <li><a href="/george-gmail/">George Gmail</a></li>
+ <li><a href="/yolo-agent/">YOLO Agent</a></li>
+</ul>
+<p><a href="/oauth2/sign_out">Sign out</a></p>
+</body></html>` 200
+ }
+}
+```
+
+### pm2 ecosystem config additions
+
+Append to `/Users/stevestudio2/Projects/oauth-proxy/ecosystem.config.js` (then `pm2 start ecosystem.config.js`):
+
+```javascript
+module.exports = {
+ apps: [
+ {
+ name: 'caddy-proxy',
+ script: 'caddy',
+ args: 'run --config /Users/stevestudio2/Projects/oauth-proxy/Caddyfile --adapter caddyfile',
+ // caddy is not a Node script — use interpreter: none trick
+ interpreter: 'none',
+ env: {},
+ out_file: '/Users/stevestudio2/Logs/caddy/out.log',
+ error_file: '/Users/stevestudio2/Logs/caddy/err.log',
+ autorestart: true,
+ watch: false,
+ },
+ {
+ name: 'oauth-proxy',
+ script: '/opt/homebrew/bin/oauth2-proxy',
+ args: '--config /Users/stevestudio2/Projects/oauth-proxy/config.cfg',
+ interpreter: 'none',
+ env_file: '/Users/stevestudio2/Projects/oauth-proxy/.env',
+ out_file: '/Users/stevestudio2/Logs/oauth-proxy/out.log',
+ error_file: '/Users/stevestudio2/Logs/oauth-proxy/err.log',
+ autorestart: true,
+ watch: false,
+ }
+ ]
+};
+```
+
+`/Users/stevestudio2/Projects/oauth-proxy/.env` (do NOT commit; route via secrets-manager):
+```
+OAUTH_PROXY_CLIENT_ID=xxxx.apps.googleusercontent.com
+OAUTH_PROXY_CLIENT_SECRET=GOCSPX-xxxx
+OAUTH_PROXY_COOKIE_SECRET=<openssl rand -base64 32>
+```
+
+### launchd keepalive plist (alternative to pm2 for the proxy pair)
+
+If you prefer launchd over pm2 for the proxy (since pm2 is already managing 60+ processes), here is the Caddy plist. Mirror for oauth2-proxy by substituting the program args.
+
+`~/Library/LaunchAgents/com.steve.caddy-proxy.plist`:
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key> <string>com.steve.caddy-proxy</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>/opt/homebrew/bin/caddy</string>
+ <string>run</string>
+ <string>--config</string>
+ <string>/Users/stevestudio2/Projects/oauth-proxy/Caddyfile</string>
+ <string>--adapter</string>
+ <string>caddyfile</string>
+ </array>
+ <key>EnvironmentVariables</key>
+ <dict>
+ <key>HOME</key> <string>/Users/stevestudio2</string>
+ </dict>
+ <key>RunAtLoad</key> <true/>
+ <key>KeepAlive</key> <true/>
+ <key>StandardOutPath</key> <string>/Users/stevestudio2/Logs/caddy/out.log</string>
+ <key>StandardErrorPath</key> <string>/Users/stevestudio2/Logs/caddy/err.log</string>
+</dict>
+</plist>
+```
+
+Load with: `launchctl load ~/Library/LaunchAgents/com.steve.caddy-proxy.plist`
+
+---
+
+## Integration with Existing google-oauth-integration Skill
+
+The existing skill at `~/.claude/skills/google-oauth-integration/` uses **in-process auth middleware** (`requireGlobalAuth` in `shared-global-auth.ts`) and a custom Express auth server on port 9999. That is a different pattern from the external reverse-proxy approach designed here.
+
+**These can coexist** without conflict:
+- The existing skill handles agents that import `requireGlobalAuth` (typically Kamatera/DW-Agents on the remote server)
+- The new oauth2-proxy design handles Mac2 local services that do not import the middleware
+
+**Migration path if you want a single auth system long-term:**
+Point the existing `AUTH_SERVER` constant to the oauth2-proxy forward-auth endpoint instead of the custom Express server. That is a Phase 3 item and not required for initial rollout.
+
+**Re-use the same Google Cloud OAuth client ID** — simply add the Mac2 tailnet callback URL as an additional Authorized Redirect URI in the existing Google Cloud project.
+
+---
+
+## Phased Rollout Plan
+
+### Week 1 — Infrastructure (4 hours)
+
+- [ ] `brew install caddy oauth2-proxy` on Mac2
+- [ ] Create Google OAuth credentials (add redirect URI to existing project or new one)
+- [ ] Generate `COOKIE_SECRET`: `openssl rand -base64 32`
+- [ ] Route all three secrets via `secrets-manager cli.js add`
+- [ ] Create `~/Projects/oauth-proxy/` directory structure
+- [ ] Write `config.cfg`, `Caddyfile`, `allowed-emails.txt`
+- [ ] Start both processes via pm2 (not yet fronting any production service)
+- [ ] Test: `curl http://localhost:4180/oauth2/auth` should return 401
+- [ ] Test: browser to `https://stevestudio2s-mac-studio.tail79cb8e.ts.net/` should show Google login
+
+### Week 2 — Pilot: 2 services (3 hours)
+
+Pilot with `morning-review` (9762) and `video-gallery` (9763) — both new, no existing Basic Auth dependency.
+
+- [ ] Add Caddyfile routes for `/morning-review/` and `/video-gallery/`
+- [ ] Verify SSO login, session persistence across browser restart
+- [ ] Verify audit log entries appear in `~/Logs/oauth-proxy/access.ndjson`
+- [ ] Verify old direct-port access `localhost:9762` still works from localhost (curl only, not exposed on tailnet)
+- [ ] Remove Basic Auth middleware from `morning-review/server.js` and `video-gallery/server.js`
+
+### Week 3 — Rolling migration: remaining 16 services (4 hours)
+
+Group by project family to batch test:
+
+1. Site Factory: sf-orchestrator, sf-viewer, sf-admin
+2. AI/Visual Factory: ai-factory-*, visual-factory-*
+3. Directory services: animals-viewer, animals-audit, pd-preview, pd-web
+4. Tooling: george-gmail, launch-supervisor, vendor-command-center, debate-ring, yolo-agent
+
+For each group:
+- [ ] Add Caddyfile handle block
+- [ ] `caddy reload --config /Users/stevestudio2/Projects/oauth-proxy/Caddyfile`
+- [ ] Test new URL
+- [ ] Comment out / remove `basicAuth()` middleware in the service's `server.js`
+- [ ] `pm2 restart <service>`
+
+### Week 4 — Hardening (2 hours)
+
+- [ ] Add log rotation launchd job (`com.steve.oauth-proxy-logrotate`)
+- [ ] Add Caddy + oauth-proxy to `pm2 save` / `pm2 startup` output
+- [ ] Write a 5-line audit query alias to `~/.zshrc`:
+ ```bash
+ alias dw-audit="jq -r '[.ts,.user,.path,.status] | @tsv' ~/Logs/oauth-proxy/access.ndjson | tail -50"
+ ```
+- [ ] Update MEMORY.md with new service URLs
+- [ ] Optionally: Phase 2 per-service cookie isolation (fork to 18 oauth2-proxy instances)
+
+---
+
+## Drawbacks and Mitigations
+
+| Drawback | Severity | Mitigation |
+|----------|----------|-----------|
+| Extra hop latency (~2ms) | Low | Caddy+oauth2-proxy add <5ms on localhost; imperceptible |
+| Google session expiry (24h) | Low | `--cookie-refresh=1h` silently refreshes while browser is active; only trips on >24h away |
+| Airplane mode / no internet | Medium | Google token validation requires internet contact. Mitigation: run services directly via `localhost:PORT` when offline (they are still accessible locally; just not via the SSO proxy URL) |
+| Proxy is single point of failure | Medium | If Caddy or oauth2-proxy crashes, ALL services lose their SSO URL. Mitigation: launchd `KeepAlive` + `pm2 autorestart`; monitor with `com.steve.pm2-resurrect` |
+| macOS Gatekeeper on oauth2-proxy binary | Low | `brew install oauth2-proxy` bypasses Gatekeeper; do not download the binary directly |
+| Google Cloud project dependency | Low | If the OAuth client is deleted or suspended, all auth breaks. Keep client ID in secrets manager with a backup note |
+| Cookie secret rotation nukes all sessions | Low | Document the rotation procedure; only matters if Steve has multiple active browser sessions |
+
+---
+
+## When NOT to Use This
+
+**Tailscale ACLs alone are sufficient when:**
+- The service is a background worker with no browser UI (norma-*, ingest, enrich, dedupe pipelines)
+- The service is called only machine-to-machine (george-gmail called from scripts, not browser)
+- The service already blocks all non-localhost requests at the Express level
+- You are prototyping and will tear it down within a week
+
+**Add the OAuth proxy when:**
+- The service has a human-facing browser UI
+- You want an audit trail of who clicked what
+- The service will eventually be shared with a second person
+- The service exposes any write action (POST /api/send, POST /api/launch, etc.)
+
+**Pure Tailscale ACL approach (no oauth2-proxy, no Caddy):**
+Enable Tailscale's built-in `tailscale serve` and lock the ACL to `src == "steve@"` only. This works for read-only dashboards where even a compromised Tailscale device grants full access. Zero additional infrastructure. Appropriate for viewer-only services (video-gallery, sf-viewer) if you decide the audit log is not a requirement.
+
+---
+
+## Estimated Effort
+
+| Phase | Task | Hours |
+|-------|------|-------|
+| Week 1 | Infrastructure: brew install, Google creds, pm2 start | 4h |
+| Week 2 | Pilot: 2 services, test, fix path-stripping edge cases | 3h |
+| Week 3 | Rolling migration: 16 services (15 min each) | 4h |
+| Week 4 | Log rotation, pm2 persistence, audit alias, docs | 2h |
+| **Total** | | **13h** |
+
+Stretch goal — Phase 2 per-service instances: +4h (scripted, not manual).
+
+---
+
+## File Structure
+
+```
+~/Projects/oauth-proxy/
+├── config.cfg # oauth2-proxy config (env var references for secrets)
+├── Caddyfile # Caddy reverse proxy config
+├── allowed-emails.txt # Allowlist: one email per line
+├── ecosystem.config.js # pm2 process definitions for caddy + oauth2-proxy
+└── .env # Secrets (NOT committed; managed by secrets-manager)
+
+~/Logs/
+├── caddy/
+│ ├── access.log # Caddy access log (JSON)
+│ ├── out.log
+│ └── err.log
+└── oauth-proxy/
+ ├── access.ndjson # oauth2-proxy audit log
+ ├── out.log
+ └── err.log
+```
+
+---
+
+## References
+
+- oauth2-proxy docs: https://oauth2-proxy.github.io/oauth2-proxy/
+- Caddy reverse_proxy docs: https://caddyserver.com/docs/caddyfile/directives/reverse_proxy
+- Caddy forward_auth docs: https://caddyserver.com/docs/caddyfile/directives/forward_auth
+- Tailscale HTTPS certs for Caddy: https://tailscale.com/kb/1181/caddy
+- Existing DW-Agents auth skill: `~/.claude/skills/google-oauth-integration/SKILL.md`
+- Existing auth server: `/Users/stevestudio2/kamatera-mirror/DW-Agents/global-auth-system/auth-server.ts`
(oldest)
·
back to Oauth Proxy Design
·
(newest)