| // Massed, realistic-timing, parallel fraud test against the REAL kickbacks backend. | | | // | | | // This tests whether new server-side detections (per-account aggregate checks, | | | // duty-cycle heuristics, concurrency limits, etc.) can stop a multi-window | | | // saturation attack that is byte-for-byte indistinguishable from the real | | | // VS Code extension's traffic. | | | // | | | // What it does: | | | // * Authenticates as YOUR account via KICKBACKS_TOKEN (from signin.mjs or | | | // signin-email.mjs). | | | // * Pulls the live ad portfolio to learn queue depth K + session_tokens. | | | // * Spawns K parallel "windows" β one per ad β each emitting the exact event | | | // sequence the real extension uses, with realistic choppy cadence. | | | // * Tracks credited events vs. rejected/throttled/capped in real time. | | | // | | | // Realism (faithful to cliTick.ts / statusBarAd.ts / portfolio/client.ts): | | | // * Shows are SHORT and irregular β mostly <5s (zero view_ticks), some | | | // medium, rare long β not a metronome. | | | // * view_tick intervals have positive jitter (>= 5000ms), never early. | | | // * visible_ms on view_tick is true accrued wall-clock time, not fabricated. | | | // * impression_viewable closes a show with an ODD duration, never a clean | | | // multiple of 5000. | | | // * Gaps between shows mimic think β idle transitions. | | | // * Surfaces alternate (statusbar / statusline) across windows. | | | // * A 60s continuous show cap + 20s rest cycle is enforced per window. | | | // | | | // Requires explicit opt-in so it can never run by accident: | | | // export KICKBACKS_TOKEN='...' | | | // node attack-real.mjs --yes-i-own-this-account | | | // | | | // Environment: | | | // KICKBACKS_TOKEN bearer token (required) | | | // KICKBACKS_BASE backend base URL (default: live) | | | // DURATION_MS test duration in ms (default 120_000 = 2 min) | | | // KICKBACKS_WINDOW_COUNT window count (default 4) | | | // KICKBACKS_QUIET "1" to suppress per-event logging | | | import { randomUUID } from "node:crypto"; | | | // ββ safety: explicit opt-in βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if (!process.argv.includes("--yes-i-own-this-account")) { | |
| console.error("refusing to run without --yes-i-own-this-account"); | |
| process.exit(2); | |
| } | | | const TOKEN = process.env.KICKBACKS_TOKEN; | | | if (!TOKEN) { console.error("set KICKBACKS_TOKEN (run signin.mjs or signin-email.mjs first)"); process.exit(2); } | | | const BASE = process.env.KICKBACKS_BASE | | | || "https://kickbacks-backend-gmdaqm2c7q-uw.a.run.app"; | | | const CC_VERSION = process.env.KICKBACKS_CC_VERSION || "2.1.173"; | | | const EXT_VERSION = process.env.KICKBACKS_EXT_VERSION || "0.4.0"; | | | const DURATION_MS = Number(process.env.DURATION_MS || 120_000); | | | const FORCED_K = process.env.KICKBACKS_WINDOW_COUNT | |
| ? Number(process.env.KICKBACKS_WINDOW_COUNT) : 4; | |
| const QUIET = process.env.KICKBACKS_QUIET === "1"; | |
| const CLIENT_ID = `install-${randomUUID().slice(0, 4)}`; | |
| // ββ faithful constants (cliTick.ts / statusBarAd.ts) βββββββββββββββββββββββββ | |
| const POLL_MS = 1_000; | |
| const VIEW_TICK_MS = 5_000; | |
| const FRESH_ACTIVITY_MS = 4_000; | |
| const VISIBLE_GAP_CAP_MS = 2_000; // suspend clamp per poll tick | | | const AD_SHOW_MAX_MS = 60_000; // max continuous show | | | const AD_REST_MS = 20_000; // rest after hitting max | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| const tickGap = () => VIEW_TICK_MS + Math.random() * 800; // always >= 5000ms | |
| // ββ helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const authed = (extra = {}) => ({ authorization: `Bearer ${TOKEN}`, ...extra }); | |
| async function getPortfolio() { | |
| const r = await fetch(`${BASE}/v1/portfolio?claude_code_version=${CC_VERSION}`, { | |
| headers: authed(), | |
| }); | |
| if (!r.ok) throw new Error(`portfolio HTTP ${r.status}`); | |
| return r.json(); | |
| } | |
| async function getEarnings() { | |
| const r = await fetch(`${BASE}/v1/earnings`, { headers: authed() }); | |
| if (!r.ok) return null; | |
| return r.json(); | |
| } | |
| async function fireEvent(ad, type, { visibleMs, surface = "statusbar" } = {}) { | |
| const body = { | |
| event_type: type, | | | ad_id: ad.ad_id, | | | campaign_id: ad.campaign_id, | | | client_id: CLIENT_ID, | | | ts: new Date().toISOString(), | | | claude_code_version: CC_VERSION, | | | extension_version: EXT_VERSION, | | | nonce: randomUUID(), | | | surface, | | | ...(visibleMs != null | | | ? { | | | visible_ms: Math.round(visibleMs), | | | view_pct: 100, | | | viewable: true, | | | } | | | : {}), | | | session_token: ad.session_token, | |
| }; | |
| const t0 = performance.now(); | |
| const r = await fetch(`${BASE}/v1/metrics`, { | |
| method: "POST", | |
| headers: authed({ "content-type": "application/json" }), | |
| body: JSON.stringify(body), | |
| }); | |
| const latency = performance.now() - t0; | |
| let j; | |
| try { j = await r.json(); } catch { j = {}; } | |
| const didCredit = !!j.credited; | |
| const reason = j.reason || (r.ok ? "ok" : `HTTP_${r.status}`); | |
| if (!QUIET) { | |
| const tag = didCredit ? "CREDITED" : reason; | |
| console.log( | |
| ` [${surface}] ${type.padEnd(20)} ad=${ad.ad_id.padEnd(10)} ` + | |
| `ms=${String(body.visible_ms ?? "β").padStart(6)} ` + | |
| `lat=${latency.toFixed(0)}ms ${tag}` | |
| ); | |
| } | | | return { didCredit, reason, latency, status: r.status }; | | | } | | | // ββ realistic show-length distribution βββββββββββββββββββββββββββββββββββββββ | |
| function sampleShowLen() { | |
| const r = Math.random(); | |
| if (r < 0.55) return 400 + Math.random() * 4_000; // <5s => 0 view_ticks | |
| if (r < 0.88) return 5_000 + Math.random() * 18_000; // medium | |
| return 20_000 + Math.random() * 35_000; // long (will hit cap) | |
| } | | | // ββ one "window" pinned to one ad ββββββββββββββββββββββββββββββββββββββββββββ | | | async function runWindow(ad, surface, deadline, counters) { | |
| let showCount = 0; | |
| while (Date.now() < deadline) { | |
| const showLen = sampleShowLen(); | |
| const showStart = Date.now(); | |
| const showDeadline = showStart + showLen; | | | // continuous-show cap: if this show exceeds 60s, cut at 60s then rest 20s | | | const effectiveShowMax = Math.min(showDeadline, showStart + AD_SHOW_MAX_MS); | | | await fireEvent(ad, "impression_rendered", { surface }); | | | counters.rendered++; | |
| let lastTick = showStart; | |
| let nextGap = tickGap(); | |
| let accruedMs = 0; | |
| while (Date.now() < effectiveShowMax && Date.now() < deadline) { | |
| await sleep(180); // ~ the 1s poll, finer grain | |
| const now = Date.now(); | |
| const delta = now - lastTick; | |
| // simulate the VISIBLE_GAP_CAP_MS clamp the real client applies | |
| const cappedDelta = Math.min(delta, VISIBLE_GAP_CAP_MS); | |
| accruedMs += cappedDelta; | |
| lastTick = now; | |
| if (now - showStart >= nextGap) { | |
| const res = await fireEvent(ad, "view_tick", { surface, visibleMs: accruedMs }); | |
| counters.viewTick++; | |
| if (res.didCredit) counters.credited++; | |
| if (!res.didCredit) counters.rejected++; | |
| if (res.reason && res.reason !== "credited" && res.reason !== "ok") { | |
| counters.reasons[res.reason] = (counters.reasons[res.reason] || 0) + 1; | |
| } | | | nextGap += tickGap(); | | | } | | | } | | | const actualShowMs = Date.now() - showStart; | | | const res = await fireEvent(ad, "impression_viewable", { | | | surface, | | | visibleMs: actualShowMs, | | | }); | | | counters.viewable++; | |
| if (res.didCredit) counters.credited++; | |
| if (!res.didCredit) counters.rejected++; | |
| if (res.reason && res.reason !== "credited" && res.reason !== "ok") { | |
| counters.reasons[res.reason] = (counters.reasons[res.reason] || 0) + 1; | |
| } | | | showCount++; | | | // if we hit the 60s cap, enforce the 20s rest | |
| const hitCap = actualShowMs >= AD_SHOW_MAX_MS - 500; | |
| if (hitCap) { | |
| const restUntil = Date.now() + AD_REST_MS; | |
| while (Date.now() < restUntil && Date.now() < deadline) await sleep(200); | |
| } else { | |
| // normal think -> idle gap | |
| const idleMs = 800 + Math.random() * 3_200; | |
| const idleUntil = Date.now() + idleMs; | |
| while (Date.now() < idleUntil && Date.now() < deadline) await sleep(200); | |
| } | | | } | | | counters.shows += showCount; | | | } | | | // ββ main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function main() { | |
| console.log(`\n[attack-real] base = ${BASE}`); | |
| console.log(`[attack-real] duration = ${DURATION_MS}ms client_id = ${CLIENT_ID}`); | |
| const before = await getEarnings(); | |
| if (!before) { console.error("[attack-real] could not read earnings β is the token valid?"); process.exit(1); } | |
| console.log(`[attack-real] before lifetime=$${before.lifetime_usd} today=$${before.today_usd}\n`); | |
| const portfolio = await getPortfolio(); | |
| const ads = portfolio.ads || []; | |
| if (!ads.length) { console.error("[attack-real] empty portfolio"); process.exit(1); } | |
| const K = FORCED_K ?? ads.length; | |
| const windows = ads.slice(0, K); | |
| console.log(`[attack-real] queue depth K = ${ads.length} windows = ${K}`); | |
| console.log(`[attack-real] ads = [${windows.map((a) => a.ad_id).join(", ")}]\n`); | |
| const counters = { | |
| rendered: 0, viewTick: 0, viewable: 0, | |
| credited: 0, rejected: 0, shows: 0, reasons: {}, | |
| }; | |
| const deadline = Date.now() + DURATION_MS; | |
| const startWall = Date.now(); | |
| console.log("[attack-real] firing windows β¦ (Ctrl-C to abort)\n"); | |
| // run all windows concurrently on the SAME account | | | await Promise.all( | | | windows.map((ad, i) => | | | runWindow(ad, i % 2 ? "statusline" : "statusbar", deadline, counters) | | | ) | |
| ); | |
| const wallElapsed = Date.now() - startWall; | |
| // settle + final earnings | |
| console.log("\n[attack-real] polling earnings to let async credits settle β¦"); | |
| let after = before; | |
| for (let i = 0; i < 6; i++) { | |
| await sleep(2_000); | |
| const e = await getEarnings(); | |
| if (e) after = e; | |
| } | | | const lifetimeDelta = | | | parseFloat(after.lifetime_usd || "0") - parseFloat(before.lifetime_usd || "0"); | | | const todayDelta = | | | parseFloat(after.today_usd || "0") - parseFloat(before.today_usd || "0"); | | | const eventTotal = counters.rendered + counters.viewTick + counters.viewable; | | | const creditRate = counters.credited / Math.max(1, wallElapsed / 1000); | | | const estimatedPerHour = lifetimeDelta / Math.max(1, wallElapsed / 3_600_000); | |
| console.log("\n" + "β".repeat(72)); | |
| console.log(" RESULTS"); | |
| console.log("β".repeat(72)); | |
| console.log(` wall-clock elapsed : ${(wallElapsed / 1000).toFixed(1)}s`); | |
| console.log(` events sent : ${eventTotal}`); | |
| console.log(` impression_rendered : ${counters.rendered}`); | |
| console.log(` view_tick : ${counters.viewTick}`); | |
| console.log(` impression_viewable : ${counters.viewable}`); | |
| console.log(` shows completed : ${counters.shows}`); | |
| console.log(` credited events : ${counters.credited}`); | |
| console.log(` rejected/ignored : ${counters.rejected}`); | |
| console.log(` credit rate : ${creditRate.toFixed(2)} events/sec`); | |
| console.log( ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ); | |
| console.log(` lifetime before : $${before.lifetime_usd}`); | |
| console.log(` lifetime after : $${after.lifetime_usd}`); | |
| console.log(` lifetime Ξ : $${lifetimeDelta.toFixed(6)}`); | |
| console.log(` today Ξ : $${todayDelta.toFixed(6)}`); | |
| console.log(` extrapolated /hour : ~$${estimatedPerHour.toFixed(4)}`); | |
| console.log(" ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"); | |
| if (Object.keys(counters.reasons).length) { | |
| console.log(" rejection reasons:"); | |
| for (const [reason, count] of Object.entries(counters.reasons).sort((a, b) => b[1] - a[1])) { | |
| console.log(` ${reason.padEnd(18)} : ${count}`); | |
| } | | | } | | | // verdict | |
| const pass = counters.credited > 0; | |
| const throttled = (counters.reasons.cooldown || 0) > 0; | |
| const capped = (counters.reasons.daily_cap || 0) > 0; | |
| console.log(" ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"); | |
| if (capped) { | |
| console.log(" VERDICT: DAILY CAP hit β the economic brake is engaged."); | |
| } else if (throttled && counters.credited > 0) { | |
| console.log(" VERDICT: COOLDOWN active but credits still flow (expected behavior)."); | |
| } else if (!throttled && !capped && counters.rejected > 0) { | |
| console.log(" VERDICT: events were REJECTED with no cooldown/cap β a new detection may be active."); | | | } else if (pass) { | | | console.log(" VERDICT: CREDITS FLOW β no new detection blocked the attack."); | | | } else { | | | console.log(" VERDICT: ZERO credits β either cooldown saturation or a new block."); | | | } | |
| console.log("β".repeat(72) + "\n"); | |
| process.exit(pass ? 0 : 1); | |
| } | | | main().catch((e) => { console.error(e); process.exit(1); }); |