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.
capgo-builder-support-logs — separate from the artifact bucket (isolated lifecycle/access/blast radius — decided).<sha256(gz)>.log.gz — the hash is the ID and the capability; "upload once" by construction (retries/dupes hit the same key, no new storage).app_id, job_id?, user_id (account id — no email/PII).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" } ]
public/build/support_logs.ts — capgkey auth → user → rate limit → forward (same internal backend→worker auth as ai_analyze). Errors: 401 / 413 too big / 415 not-text / 429.POST /support-logs (validate → sha256 → put → {id,url}) and GET /support-logs/:id (stream gz, Content-Disposition: attachment; friendly 404 "logs expired — ask the user to re-send"). Public route on the artifact-download host pattern.CacheHelper limiter. Documented caveat: Cache-API-backed → per-colo, fails open — a speed bump, not a guarantee (acceptable given 1–4).No captcha (authenticated CLI API — wrong tool). Optional later hardening: globally-consistent per-account Postgres counter — deferred unless real abuse appears.
contact-support.ts: new optional upload dep after the confirm + bundle write. Upload OK → email body gets the download URL + "kept 30 days" (URL-only; the id is embedded in it); skip clipboard-path/reveal/attach text (nothing to attach); local paths still printed. Upload fails (15 s timeout) → exactly today's behavior. Never blocks the flow.support-upload.ts (new): gz→base64 POST to ${apiHost}/build/support_logs (same client pattern as ai/analyze.ts); null on any error.contactSupport.| Where | What |
|---|---|
| Ops (one-time) | bucket + lifecycle rule; SUPPORT_LOGS_BUCKET binding (prod + preprod) |
| Worker | POST/GET routes; size cap; gunzip/text validation; sha256; mock-R2 tests (put/get/oversize/non-text/idempotent re-put) |
| Backend | support_logs.ts proxy + registration; tests 401/413/429/forward |
| CLI | support-upload.ts + upload branch + confirm copy; tests: ok-path (id+url in body, no attach text), fail-path (identical to today), timeout degrade |
ai_analyze — confirm in code).-preprod twin (suggest twin).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)