Review
Fingerprint allocation snipers before the waitlist render
CorkWait loses half its natural-wine allocation drops to multi-account bots. Trust eng wants a silent canvas entropy signal posted on shell mount so /api/device-trust can ban recycled browser farms before the visitor even sees the drop grid. Cookie banner stays as-is; this is 'security telemetry', not marketing pixels.
ReactTier 4gdprconsentcanvasfingerprinting
Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.
src/app/ShellProviders.tsx+22-0
| 1 | 1 | import { useEffect, type ReactNode } from "react"; | |
| 2 | 2 | import { CookieBanner } from "@/consent/CookieBanner"; | |
| 3 | 3 | import { useConsentStore } from "@/consent/store"; | |
| 4 | 4 | import { DropCartProvider } from "@/drops/DropCartContext"; | |
| 5 | + | import { sha256Hex } from "@/lib/hash"; | |
| 5 | 6 | ||
| 6 | 7 | export function ShellProviders({ children }: { children: ReactNode }) { | |
| 7 | 8 | // Banner writes analytics/marketing flags; product APIs read them. | |
| 8 | 9 | const analyticsAllowed = useConsentStore((s) => s.analyticsAllowed); | |
| 9 | 10 | ||
| 11 | + | useEffect(() => { | |
| 12 | + | // Bot farms share GPU/canvas signatures; capture before first paint of the grid. | |
| 13 | + | const canvas = document.createElement("canvas"); | |
| 14 | + | canvas.width = 240; | |
| 15 | + | canvas.height = 60; | |
| 16 | + | const ctx = canvas.getContext("2d"); | |
| 17 | + | if (!ctx) return; | |
| 18 | + | ctx.textBaseline = "top"; | |
| 19 | + | ctx.font = "14px 'Avenir Next'"; | |
| 20 | + | ctx.fillStyle = "#1a1a1a"; | |
| 21 | + | ctx.fillText("corkwait·allocation·probe\uD83C\uDF77", 2, 2); | |
| 22 | + | const raw = canvas.toDataURL(); | |
| 23 | + | void sha256Hex(raw).then((deviceHash) => | |
| 24 | + | fetch("/api/device-trust", { | |
| 25 | + | method: "POST", | |
| 26 | + | headers: { "Content-Type": "application/json" }, | |
| 27 | + | body: JSON.stringify({ deviceHash, path: window.location.pathname }), | |
| 28 | + | keepalive: true, | |
| 29 | + | }), | |
| 30 | + | ); | |
| 31 | + | }, []); | |
| 10 | 32 | ||
| 11 | 33 | return ( | |
| 12 | 34 | <DropCartProvider> | |
| 13 | 35 | {children} | |
| 14 | 36 | <CookieBanner /> | |
| 15 | 37 | {/* analyticsAllowed reserved for Mixpanel init elsewhere */} | |
| 16 | 38 | <span hidden data-consent={String(analyticsAllowed)} /> | |
| 17 | 39 | </DropCartProvider> | |
| 18 | 40 | ); | |
| 19 | 41 | } |