Capgo Builder — Support-Logs Upload (R2)

docs/superpowers/specs/2026-06-05-builder-support-logs-upload-design.md · 2026-06-05
✅ APPROVED follow-up — kills the manual attach. Builds on the shipped local-file + mailto flow.

1 · Goal

On confirm, upload the gzipped logs bundle to R2 and put a logs ID + download link in the email body. The user just hits Send; support clicks the link from Discord. The local file stays as the offline/failure fallback — no attach, no Finder/Mail window dance.

2 · Data flow

CLI (existing confirm gate; "view logs first" preview unchanged)
  gzip bundle → sha256(gz) = ID
  POST ${apiHost}/build/support_logs  { appId, jobId?, gzB64 }   (capgkey)
        ▼
capgo backend  public/build/support_logs.ts   (mirrors ai_analyze.ts)
  auth capgkey → resolve user → rate limit → forward { gzB64, appId, jobId?, userId }
        ▼
capgo_builder worker  POST /support-logs
  1. gz ≤ 10 MB (413)      2. text-only validation (gunzip 64KB → UTF-8/printable; 415)
  3. R2 put  capgo-builder-support-logs / <sha256>.log.gz   (idempotent by key)
  4. → { id, url }
        ▼
email body:  "Support logs (kept 30 days): <url>"  → SEND-READY (URL-only; the id is inside it)
support:     GET /support-logs/<sha256> → streams the .log.gz (404 after expiry)

FALLBACK (upload fails/offline): exactly today's shipped behavior —
  clipboard .log.gz path + Finder reveal + attach instructions.

3 · Storage — NEW dedicated bucket

npx wrangler r2 bucket create capgo-builder-support-logs
npx wrangler r2 bucket lifecycle add capgo-builder-support-logs --expire-days 30
// worker wrangler.jsonc (prod + preprod)
"r2_buckets": [ …, { "binding": "SUPPORT_LOGS_BUCKET", "bucket_name": "capgo-builder-support-logs" } ]

4 · API

5 · Abuse posture (the honest version)

Defenses, strongest first:
  1. Economics + TTL: worst case (colo-hopping VPN diluting the limiter ~100×: 1,000/day × 10 MB × 30-day TTL) ≈ 300 GB ≈ $4.50/month, self-purging. R2 egress is free → the GET side can't run up a bill. No payoff.
  2. 10 MB gz cap (hard server check).
  3. Text-only validation — kills the only attractive misuse (file hosting behind a capgo URL).
  4. sha256 keying — duplicate floods cost nothing.
  5. Account-keyed limit 1/min + 10/day via the existing CacheHelper limiter. Documented caveat: Cache-API-backed → per-colo, fails open — a speed bump, not a guarantee (acceptable given 1–4).
  6. Account bans as backstop (capgkey required; no anonymous path).

No captcha (authenticated CLI API — wrong tool). Optional later hardening: globally-consistent per-account Postgres counter — deferred unless real abuse appears.

6 · CLI changes

7 · Components & tests

WhereWhat
Ops (one-time)bucket + lifecycle rule; SUPPORT_LOGS_BUCKET binding (prod + preprod)
WorkerPOST/GET routes; size cap; gunzip/text validation; sha256; mock-R2 tests (put/get/oversize/non-text/idempotent re-put)
Backendsupport_logs.ts proxy + registration; tests 401/413/429/forward
CLIsupport-upload.ts + upload branch + confirm copy; tests: ok-path (id+url in body, no attach text), fail-path (identical to today), timeout degrade

8 · Open items

  1. Exact public GET host/path (reuse the artifact-download pattern — confirm in code).
  2. Backend→worker internal auth detail (mirror ai_analyze — confirm in code).
  3. Preprod: same bucket or -preprod twin (suggest twin).
  4. Deferred: global Postgres rate counter (only if abuse ever appears).

Builds on: 2026-06-03-builder-contact-support-simple-design.md (shipped) · Rejected alternative: backend/bridge email sending (2026-06-03-builder-contact-support-design.md)