From 87833fbc8851a945619401479ec7fc1a0c200461 Mon Sep 17 00:00:00 2001 From: serenakeyitan Date: Fri, 3 Jul 2026 08:03:53 -0700 Subject: [PATCH] feat: store and reuse the hosted-report writeToken on POST MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The website API now binds report ids to their first publisher: the first POST for an id returns a one-time writeToken, and overwriting that id (same-day re-scan, latest-wins) requires it as a Bearer header — strangers can no longer rewrite a hosted report they didn't publish. Scanner side: save returned tokens to .production-scan/write-tokens.json, attach them when re-POSTing a claimed id, and recover from 401 by publishing under -2 (same shape as the reserved-id 400 rule). Pairs with the website-side change in first-tree-website (src/pages/api/reports/index.ts — contract pair per Step 7). Co-Authored-By: Claude Fable 5 --- .claude/skills/production-scan/SKILL.md | 33 ++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/.claude/skills/production-scan/SKILL.md b/.claude/skills/production-scan/SKILL.md index 39915be..2203483 100644 --- a/.claude/skills/production-scan/SKILL.md +++ b/.claude/skills/production-scan/SKILL.md @@ -589,20 +589,42 @@ re-scans don't clobber): | `verdictHandle` / `quip` *(optional)* | `verdict_handle` / `verdict_quip` | stored verbatim by the API (no validator rule; keep ≤500 as skill-side discipline) — lets the hosted page adopt the scan's own voice (renderer support tracked website-side) | ```bash + # First upload of an id — no auth. The 201 body includes a ONE-TIME `writeToken`; + # save it (rule below) — overwriting this id later requires it. curl -X POST /api/reports -H "Content-Type: application/json" -d @.production-scan/api-payload.json + # Re-submitting an id we already claimed (same-day re-scan) — send the saved token: + curl -X POST /api/reports -H "Content-Type: application/json" \ + -H "Authorization: Bearer " -d @.production-scan/api-payload.json ``` + **Write token (authorship binding, API side 2026-07-03):** the first POST for an id + *claims* it — the 201 body carries a one-time `writeToken`, and every later POST of + the **same id** must send it as `Authorization: Bearer ` or the API + answers 401 (so strangers can't rewrite a hosted report they didn't publish). + Handling is mandatory: + - **Before POSTing**, look the id up in `.production-scan/write-tokens.json`; if a + token exists, attach the `Authorization` header. + - **On 201**, if the body contains `writeToken`, merge `{"": ""}` into + `.production-scan/write-tokens.json` (create the file if missing). The token is never + shown again; losing it means this id can no longer be overwritten (recovery: `-2`). + **Submission semantics — one payload, at most 3 delivery attempts:** - Retry (backoff 1s/2s/4s, 10s timeout each) ONLY on network error / timeout / 5xx. - **429** → stop immediately (Retry-After can be up to an hour); fall back per Step 9.5. - **400** → the payload violates the table above; fix the payload — the corrected body is a NEW submission, never re-send an identical body. Reserved-id 400 → append `-2` to the id and resubmit once. + - **401** → the id is claimed and we don't hold its token (write-tokens.json lost, or + someone else published under this id first): append `-2` to the id — a fresh claim, + same recovery as the reserved-id 400 — and resubmit once. Never re-send an + identical 401 body. - **413** → drop `markdown` from the payload and resubmit once. On 201, use the returned URL; anything else → the Step 9.5 fallback. The per-IP budget is 20 POSTs/hour — one scan = one logical submission. **`.production-scan/` must be gitignored by default** (add it to `.gitignore`); it -carries bounded evidence only — **never raw secret values or full file contents**. +carries bounded evidence only — **never raw secret values or full file contents** — +plus `write-tokens.json`, the per-id overwrite credentials for the hosted reports +(one more reason the directory stays out of git). The user opts in to committing a report. Keeping timestamped JSON lets a later scan diff against the previous one ("fixed 2, 1 new since "). The fix flow and re-scan read **only the JSON** — Markdown is a human-only leaf. @@ -1272,7 +1294,10 @@ shareable without manual deployment. composed id — the date suffix must survive); if the result starts with a non-alphanumeric, prefix `r`; if it equals a reserved sample id, append `-2` - The date suffix separates scans from different days; **a same-day re-scan of the - same repo reuses the id and OVERWRITES the hosted report (latest wins — deliberate)** + same repo reuses the id and OVERWRITES the hosted report (latest wins — deliberate)**, + authenticated by the id's `writeToken` from `.production-scan/write-tokens.json` + (Step 7 write-token rule); without it the API answers 401 and the re-scan + publishes under `-2` - Reports are stored **permanently** (product decision 2026-07-02) — share links never expire - **How the URL is produced (MANDATORY production flow with SAFETY checks):** 1. After building `scan-report.json`, determine the base URL using the detection logic above @@ -1332,7 +1357,9 @@ shareable without manual deployment. - This is THE conversion point - never skip ✅ **HTML Report URL (Step 9.5)** -- POST once (≤3 delivery attempts); 201 → hosted URL is the last line +- POST once (≤3 delivery attempts); 201 → hosted URL is the last line; persist any + returned `writeToken` to `.production-scan/write-tokens.json` and send it as a + Bearer header when re-POSTing the same id (Step 7 write-token rule) - Any failure → local-fallback line + cause (honest URL rule; no dead links) - Auto-detect base (First Tree → production, FIRST_TREE_BASE_URL, else localhost) - Production: `https://first-tree.ai/production-scan/r/--`