// ───────────────────────────────────────────────────────────────────────────── // Knockoff report worker (Cloudflare Worker + D1) // // Accepts brand misclassification reports from the extension and stores them // for review. No accounts, no cookies, no PII. The reporter IP is only ever // stored as a salted hash, and only to rate-limit abuse. // // POST /report one report (JSON body, see below) // GET /brands known-brands list (text, one per line; // served from D1 and edge-cached 6h; the // extension's daily refresh hits this) // GET /flagged curated blocklist additions (text, one // per line; extensions fetch daily) // GET /review?token=... HTML triage dashboard: queue of uncurated // brands + one-click Trust/Block/Dismiss // POST /curate?token=... {brand, list: "flagged"|"known"|"dismissed"} // to decide, {brand, remove: true} to undo // GET /reports?token=...&days=7 recent reports (JSON, review only) // GET /tallies?token=... per-brand vote tallies (JSON, review only) // GET /queue?token=... triage queue, same rows the dashboard // shows (JSON, review only) // // Deploy (from this directory): // wrangler d1 create knockoff-reports # once; put the id in wrangler.toml // wrangler d1 execute knockoff-reports --file=schema.sql --remote // wrangler d1 execute knockoff-reports --file=migrate-triage.sql --remote # pre-existing DBs, once // wrangler d1 execute knockoff-reports --file=seed-brands.sql --remote # once // wrangler secret put REVIEW_TOKEN # any long random string // wrangler secret put IP_SALT # any long random string // wrangler deploy // Then set REPORT_ENDPOINT in the extension's src/content.js to the worker URL. // ───────────────────────────────────────────────────────────────────────────── const CORS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type" }; const MAX_REPORTS_PER_IP_PER_HOUR = 30; const BRANDS_CACHE_SECONDS = 6 * 60 * 60; function json(body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...CORS } }); } async function sha256(text) { const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join(""); } function normalize(s) { return (s || "").toLowerCase().replace(/[^a-z0-9]/g, ""); } async function handleReport(request, env) { let body; try { body = await request.json(); } catch { return json({ error: "invalid JSON" }, 400); } const brand = String(body.brand || "").trim().slice(0, 64); const brandKey = normalize(brand); const suggestion = body.suggestion; if (!brandKey) return json({ error: "brand required" }, 400); if (suggestion !== "is_junk" && suggestion !== "not_junk") { return json({ error: "suggestion must be is_junk or not_junk" }, 400); } const asin = /^[A-Z0-9]{10}$/.test(body.asin || "") ? body.asin : null; const verdict = String(body.verdict || "").slice(0, 20) || null; const marketplace = String(body.marketplace || "").slice(0, 40) || null; const extVersion = String(body.extVersion || "").slice(0, 20) || null; const title = String(body.title || "").slice(0, 150) || null; const reason = String(body.reason || "").slice(0, 200) || null; const ip = request.headers.get("CF-Connecting-IP") || "unknown"; const ipHash = await sha256(env.IP_SALT + ip); const { count } = await env.DB.prepare( "SELECT COUNT(*) AS count FROM reports WHERE ip_hash = ?1 AND created_at > datetime('now', '-1 hour')" ).bind(ipHash).first(); if (count >= MAX_REPORTS_PER_IP_PER_HOUR) { return json({ error: "rate limited" }, 429); } // One row per reporter per brand: reporting the same brand again updates // the earlier vote instead of stacking the tally. COALESCE keeps context // fields (ASIN, title...) from an earlier report when the new one lacks them. await env.DB.prepare( `INSERT INTO reports (brand, brand_key, suggestion, verdict, asin, marketplace, ext_version, ip_hash, title, reason) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10) ON CONFLICT (ip_hash, brand_key) DO UPDATE SET brand = excluded.brand, suggestion = excluded.suggestion, verdict = excluded.verdict, asin = COALESCE(excluded.asin, asin), marketplace = COALESCE(excluded.marketplace, marketplace), ext_version = excluded.ext_version, title = COALESCE(excluded.title, title), reason = COALESCE(excluded.reason, reason), created_at = datetime('now')` ).bind(brand, brandKey, suggestion, verdict, asin, marketplace, extVersion, ipHash, title, reason).run(); return json({ ok: true }); } // Serve the known-brands allowlist: the seeded base list plus known-brand // additions curated from the review dashboard, edge-cached. A failed D1 read // throws and 500s, which clients treat like any bad refresh — they keep // their last good copy. async function handleBrands(request, env, ctx) { // The Cache API only works on custom-domain zones; touching it with a // workers.dev URL dies at the edge with error 1042 (uncatchable), so on // the workers.dev alias we just serve uncached. const url = new URL(request.url); const useCache = !url.hostname.endsWith("workers.dev"); const cache = caches.default; const cacheKey = new Request(url.origin + "/brands-v3"); if (useCache) { const cached = await cache.match(cacheKey); if (cached) return cached; } const { results } = await env.DB.prepare( `SELECT brand FROM brands UNION SELECT brand FROM curated WHERE list = 'known' ORDER BY brand` ).all(); const res = new Response(results.map((r) => r.brand).join("\n"), { headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "public, max-age=" + BRANDS_CACHE_SECONDS, ...CORS } }); if (useCache) ctx.waitUntil(cache.put(cacheKey, res.clone())); return res; } // Curated blocklist additions. Uncached: traffic is one hit per install per // day and a new block should propagate immediately. async function handleFlagged(env) { const { results } = await env.DB.prepare( "SELECT brand FROM curated WHERE list = 'flagged' ORDER BY brand" ).all(); return new Response(results.map((r) => r.brand).join("\n"), { headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", ...CORS } }); } async function handleCurate(request, env, url) { if (url.searchParams.get("token") !== env.REVIEW_TOKEN) { return json({ error: "unauthorized" }, 401); } let body; try { body = await request.json(); } catch { return json({ error: "invalid JSON" }, 400); } const brand = String(body.brand || "").trim().slice(0, 64); const key = normalize(brand); if (!key) return json({ error: "brand required" }, 400); if (body.remove) { await env.DB.prepare("DELETE FROM curated WHERE brand_key = ?1").bind(key).run(); return json({ ok: true }); } if (body.list !== "flagged" && body.list !== "known" && body.list !== "dismissed") { return json({ error: "list must be flagged, known or dismissed" }, 400); } await env.DB.prepare( `INSERT INTO curated (brand, brand_key, list) VALUES (?1, ?2, ?3) ON CONFLICT(brand_key) DO UPDATE SET list = ?3, brand = ?1` ).bind(brand, key, body.list).run(); return json({ ok: true }); } // Human-readable review dashboard. Brand strings are reporter-submitted, so // everything is HTML-escaped on the way out. function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } // The queue is only brands with no curation decision yet — Trust, Block and // Dismiss all clear a brand from it. brand_tallies already excludes reports // that agree with the verdict shown at report time, so pure noise never // queues. The latest reported ASIN, product title and detector reason ride // along as decision context. const QUEUE_SQL = `SELECT t.*, (SELECT r.asin FROM reports r WHERE r.brand_key = t.brand_key AND r.asin IS NOT NULL ORDER BY r.id DESC LIMIT 1) AS asin, (SELECT r.marketplace FROM reports r WHERE r.brand_key = t.brand_key AND r.asin IS NOT NULL ORDER BY r.id DESC LIMIT 1) AS marketplace, (SELECT r.title FROM reports r WHERE r.brand_key = t.brand_key AND r.title IS NOT NULL ORDER BY r.id DESC LIMIT 1) AS title, (SELECT r.reason FROM reports r WHERE r.brand_key = t.brand_key AND r.reason IS NOT NULL ORDER BY r.id DESC LIMIT 1) AS reason, (SELECT GROUP_CONCAT(DISTINCT r.verdict) FROM reports r WHERE r.brand_key = t.brand_key) AS verdicts FROM brand_tallies t LEFT JOIN curated c ON c.brand_key = t.brand_key WHERE c.brand_key IS NULL ORDER BY t.total DESC, t.last_report DESC LIMIT 500`; async function handleDashboard(env, url) { if (url.searchParams.get("token") !== env.REVIEW_TOKEN) { return json({ error: "unauthorized" }, 401); } const { results: queue } = await env.DB.prepare(QUEUE_SQL).all(); const { results: recent } = await env.DB.prepare( `SELECT brand, suggestion, verdict, asin, marketplace, created_at FROM reports ORDER BY id DESC LIMIT 100` ).all(); const { results: curated } = await env.DB.prepare( "SELECT brand, brand_key, list, created_at FROM curated ORDER BY created_at DESC" ).all(); // False positives (real brands being filtered) erode trust the most, so // brands whose reporters lean "real" triage first. const isFp = (t) => t.real_votes > 0 && t.real_votes >= t.junk_votes; const fp = queue.filter(isFp) .sort((a, b) => b.real_votes - a.real_votes || b.total - a.total); const junk = queue.filter((t) => !isFp(t)) .sort((a, b) => b.junk_votes - a.junk_votes || b.total - a.total); const queueRow = (t) => { const product = t.asin ? `${esc(t.asin)}` : `search`; // Second line: the reported product title (the fastest way for a human to // judge a brand), falling back to the detector's reason for old reports. const context = t.title || t.reason ? `
${esc(t.title || t.reason)}
` : ""; // Single, uncorroborated reports start collapsed behind the section's // toggle; search still reaches them. const single = t.total === 1 ? ' class="single" hidden' : ""; return `` + `` + `${esc(t.brand)}${context}` + `${t.real_votes || ""}` + `${t.junk_votes || ""}` + `${esc((t.verdicts || "").replace(/,/g, ", "))}` + `${esc((t.last_report || "").slice(0, 10))}` + `${product}` + `` + `` + ``; }; const queueHead = `` + `BrandRealJunkReported asLastProduct`; const queueEmpty = `Queue clear.`; const queueTable = (items) => { const singles = items.filter((t) => t.total === 1).length; const more = singles ? `` : ""; return `${queueHead}${items.map(queueRow).join("") || queueEmpty}${more}
`; }; const curatedRows = curated.map((c) => `${esc(c.brand)}` + `${c.list}` + `${esc(c.created_at)}` + `` ).join(""); const recentRows = recent.map((r) => `${esc(r.brand)}` + `${r.suggestion === "is_junk" ? "junk" : "real brand"}` + `${esc(r.verdict)}` + `${r.asin ? `${esc(r.asin)}` : ""}` + `${esc(r.created_at)}` ).join(""); const empty = `No reports yet.`; const html = ` Knockoff report review

Knockoff report review

Trust/Block ship to every install within its next daily refresh — no extension release. Dismiss just clears the brand from the queue.

j/k move · x select (shift-click for a range) · t trust · b block · d dismiss · o open product · / search

Possible false positives ${fp.length}

${queueTable(fp)}

Reported junk ${junk.length}

${queueTable(junk)}
Curated ${curated.length} ${curatedRows || ``}
BrandListAdded
Nothing curated yet.
Recent reports ${recentRows || empty}
BrandSuggestionVerdict at reportASINWhen
`; return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" } }); } async function handleReview(request, env, url) { if (url.searchParams.get("token") !== env.REVIEW_TOKEN) { return json({ error: "unauthorized" }, 401); } if (url.pathname === "/tallies") { const { results } = await env.DB.prepare( "SELECT * FROM brand_tallies ORDER BY total DESC LIMIT 500" ).all(); return json(results); } if (url.pathname === "/queue") { const { results } = await env.DB.prepare(QUEUE_SQL).all(); return json(results); } const days = Math.min(parseInt(url.searchParams.get("days") || "7", 10) || 7, 90); const { results } = await env.DB.prepare( `SELECT id, brand, brand_key, suggestion, verdict, asin, marketplace, ext_version, created_at FROM reports WHERE created_at > datetime('now', ?1) ORDER BY id DESC LIMIT 1000` ).bind(`-${days} days`).all(); return json(results); } export default { async fetch(request, env, ctx) { const url = new URL(request.url); if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: CORS }); } if (request.method === "POST" && url.pathname === "/report") { return handleReport(request, env); } if (request.method === "GET" && url.pathname === "/brands") { return handleBrands(request, env, ctx); } if (request.method === "GET" && url.pathname === "/flagged") { return handleFlagged(env); } if (request.method === "GET" && url.pathname === "/review") { return handleDashboard(env, url); } if (request.method === "POST" && url.pathname === "/curate") { return handleCurate(request, env, url); } if (request.method === "GET" && (url.pathname === "/reports" || url.pathname === "/tallies" || url.pathname === "/queue")) { return handleReview(request, env, url); } return json({ error: "not found" }, 404); } };