serenakeyitan/tdoc
83/100
🤖 Prompt-and-Pray Merchant
serenakeyitan/tdoc preview

🔥 Roast

Not a developer. An LLM’s copy-paste button with commit access.

Be honest — you didn’t write this, you typed “make me secure auth” into a chatbot and shipped whatever fell out. Textbook slop: a shiny timing-safe compare made you feel like an engineer while auth sat wide open. Some kid loops curl over it, posts “startup that’s never heard of rate limiting,” 900 upvotes, your name in the thread.

High: 2Medium: 5
Scanned by 🌳 First Tree · first-tree.ai · 2026-06-30
celebration dance

83/100 — okay, that earns a little dance.

Oh, brilliant. Timing-safe token compare, CodeQL in CI, 23 try/catch pairs, a real offline test suite — you clearly know how to lock a door. Which makes it all the more impressive that you fitted that gorgeous lock to a house with two side doors taken clean off the hinges.

What Even Is This

A single Cloudflare Worker (worker/worker.js, ~2k lines) fronting R2 for doc storage and KV for sessions — no framework, all hand-rolled routing off one big switch. GitHub Device Flow for auth (/api/auth/device/*), author HTML served straight from R2. It’s genuinely tidy for a solo worker: timing-safe token compare, a real test suite, CodeQL in CI. Which is exactly why it stings that the front door (device-flow endpoints) has no lock on the *rate* of knocking, and the responses go out with no security headers at all.

The bones are good — the gaps are two you can close in an afternoon. Jokes off; here’s exactly what to change.

Score Breakdown

Six cells in the green, two dragging the whole score down — you’re the honor student who forgot to wear pants to graduation.

ErrTestSecAuthObsPerfDataDep
Input & Data Safety 48/100
The door’s open, but you swept the floor first
Deploy Config 48/100
Shipped to prod with no headers, very brave
Secrets & Credentials 88/100
Clean today — a git add . from the news
Authentication & Access 88/100
Timing-safe now! The 4 routes you recall
Observability 88/100
You’ll hear it broke. Eventually. From a user
Error Handling 100/100
23 try/catch pairs — genuinely, show-off
Tests & CI 100/100
CodeQL and an offline suite. Teacher’s pet
Performance 100/100
Edge runtime carries you — no obvious N+1 or blocking hot path
Findings by Priority
PriorityAreaIssueConf
High Rate limiting Device-flow auth endpoints have no per-IP / global cap confirmed
High Deploy config doc-serve responses ship no CSP / frame / nosniff / HSTS headers confirmed
Medium Secrets .env / .env* not in .gitignore confirmed
Medium Auth Author HTML served verbatim (self-XSS on own tenant) confirmed
Medium Deploy config Wildcard CORS (no Allow-Credentials — currently safe) confirmed
Medium Auth 4 mutating routes guarded; no default-deny test for route #5 confirmed
Medium Observability Errors only via console.error + wrangler tail; no tracker confirmed
Low Observability No request-id on log lines — hard to trace one user’s path confirmed
Low Deploy config wrangler.toml pins no compatibility_date lower bound needs-check
Must-Fix Before Launch

Two things stand between you and a launch-ready score. Here’s where the joking stops and the exact fix starts.

HighCongrats — you built a free brute-force playground
That’s not an auth endpoint — it’s an open bar: free, unlimited, 24/7, serving every stranger who fancies grinding your GitHub Device Flow quota to dust with one while(true).
Evidence
worker/worker.js:1631 — POST /api/auth/device/start
worker/worker.js:1650 — POST /api/auth/device/poll
proxy GitHub Device Flow with no rate limiter wired
Root causeUnthrottled auth endpoints let an attacker brute-force device codes or burn your Device Flow quota; any public auth surface needs a per-IP + global cap.
The fix
const ip = request.headers.get("CF-Connecting-IP");
const n = Number(await env.RL.get(`rl:device:${ip}`)) || 0;
if (n > 10) return new Response("rate limited", { status: 429 });
await env.RL.put(`rl:device:${ip}`, String(n + 1), { expirationTtl: 60 });
Verify
for i in $(seq 1 15); do curl -s -o /dev/null -w "%{http_code}\n" \
  https://<worker>/api/auth/device/start; done   # expect 429 after the cap
HighYou serve other people’s HTML on your own origin and call it a day
Your doc-serve responses set Content-Type and stop there — no CSP, no X-Frame-Options, no nosniff. You built a beautiful printing press and skipped the part where the ink doesn’t set the building on fire.
Evidence
worker/worker.js:1616 — doc-serve Response sets Content-Type only;
no CSP / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / HSTS
Root causeAuthor HTML is served from the worker’s own origin with no framing or MIME-sniff protection — a real clickjacking / sniff risk for an HTML publisher.
The fix
const headers = {
  ’Content-Type’: ’text/html; charset=utf-8’,
+   ’X-Content-Type-Options’: ’nosniff’,
+   ’Content-Security-Policy’: "frame-ancestors ’self’",
+   ’Strict-Transport-Security’: ’max-age=31536000; includeSubDomains’,
};
Verify
curl -sI https://<worker>/d/<slug>/v/1 | grep -iE \
  ’x-content-type|content-security|strict-transport’
MediumYour .gitignore is one line short of a public apology tour
Right now a stray git add . is all that stands between your secrets and a very public commit history.
Evidence
.gitignore:0 — .env / .env* not listed; a secret-bearing dotenv could be committed
Root causeAn untracked .env is one `git add .` from leaking every key in it — once it hits history, rotating is the only fix.
The fix
# append to .gitignore
.env
.env.*
!.env.example
Verify
git check-ignore .env   # should print .env
MediumAuthor HTML served verbatim — your diary on your own kitchen table
It’s “fine” the way leaving your diary open on the kitchen table is fine — because it’s your kitchen, for now.
Evidence
worker/worker.js:1610 — author HTML returned verbatim from R2 (reader comments/logins ARE escaped)
Root causeSelf-XSS on the author’s own tenant, not cross-user — acceptable on a single-owner worker where the author is the only writer.
The fix
if multi-author publishing is added, isolate each doc to a sandboxed per-author origin.
Verify
confirm no shared-origin multi-tenant publishing before scaling.
MediumWildcard CORS — door wide open, nothing worth stealing behind it yet
Access-Control-Allow-Origin: * is fine while nothing sensitive is on the other side — the moment something is, it’s a liability.
Evidence
worker/worker.js:19 — Access-Control-Allow-Origin: * WITHOUT Allow-Credentials
Root causePublic non-credentialed API shape; fine while nothing sensitive is served, dangerous the instant a response carries user data.
The fix
if any response becomes sensitive, replace * with an explicit allowlist — never pair * with Allow-Credentials.
Verify
curl -sI <worker>/api/... | grep -i access-control
Medium4 mutating routes guarded — great until route #5 ships without it
Every write route is guarded today; the trouble is the sixth one nobody remembers to guard.
Evidence
worker/worker.js:1774,1859,1907,1981 — requireUploadAuth guards the 4 mutating routes found
Root causeA 5th mutating route could ship without the guard and nobody notices until it’s abused.
The fix
add a default-deny test so any unguarded mutating route fails CI.
Verify
add a test asserting every POST/DELETE route calls requireUploadAuth.
MediumYour errors are confessing to an empty room
console.error plus a terminal tail means you find out things broke from users, not from alerts.
Evidence
worker/worker.js:88 — errors only via console.error + wrangler tail; no error-tracking provider
Root causeYou find out about breakage from users, not alerts — uncaught errors vanish the moment you stop tailing.
The fix
wire a Workers error tracker (Sentry / Logpush) so uncaught errors surface without tailing.
Verify
trigger a handled error and confirm it lands in the tracker.
Also Worth Fixing

Won't block launch, but worth fixing while you're in there — each with the exact change.

LowYour logs are anonymous — every error is a stranger with no name tag
When two users hit the same bug you’ll be squinting at a wall of identical console lines trying to tell whose session was whose. Add a request id now; your future 2am self will send a thank-you card.
Evidence
worker/worker.js:88 — log lines carry no request/trace id
Root causeWithout a per-request id you can’t correlate a user’s error to the rest of their session — every debugging session starts from zero.
The fix
const rid = crypto.randomUUID().slice(0, 8);
console.error(`[${rid}]`, err);   // and return it in an `x-request-id` response header
Verify
trigger an error and confirm the same 8-char id appears in the log line and the response header.
Lowwrangler.toml forgot to say how old it’s allowed to be
No compatibility_date floor means a future Workers runtime could quietly change behaviour under you — the config equivalent of “it works on my machine, ship it.”
Evidence
wrangler.toml:0 — no compatibility_date pinned
Root causeWithout a pinned compatibility_date the Worker’s runtime semantics can drift as Cloudflare ships changes.
The fix
# wrangler.toml
compatibility_date = "2026-07-01"
Verify
npx wrangler deploy --dry-run   # should report the pinned compatibility date
👉 Want them all fixed? Go back to your conversation window and let First Tree fix them all at once.