feat: add public anonymous one-page check as the proof-to-repair entry path - #66
Conversation
…y path The public sample (/demo) is now backed by a live, indexable entry path: any visitor can paste one public URL and see browser-rendered proof without an account, an email, or a stored report. - /check: indexable public page with the URL form, proof-loop copy, the no-ranking boundary, and a measured handoff into private beta access. - POST /api/public-check: anonymous handler that runs the shared audit engine against one page (Browser Run), returns measured static-vs-rendered proof, guarded false positives, actionable findings when present, issue counts, engine version, and truthful next-step/boundary copy. Nothing is stored; only per-network and per-site rate-limit counters (check:* buckets in audit_usage) are written. - Local dev twin in server/index.js with in-memory rate limits. - Copy stays truthful everywhere it previously said anonymous audits were unavailable: /demo, /methodology, /packages, llms.txt, skill.md, README. - /check is in rootSitemap, public/sitemap.xml, llms.txt, and Worker run_worker_first, and linked from /, /demo, /methodology, and /packages. - Tests: real-engine fixture test pins the response to live engine output; unit tests pin validation, quota buckets, response mapping, and page boundary copy; promise-audit and live-spot-check suites extended.
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
📝 WalkthroughWalkthroughThis change adds an anonymous ChangesAnonymous public SEO check
Sequence Diagram(s)sequenceDiagram
participant Browser
participant PublicCheckAPI
participant AuditEngine
participant TargetPage
Browser->>PublicCheckAPI: Submit page URL
PublicCheckAPI->>PublicCheckAPI: Validate target and quota
PublicCheckAPI->>AuditEngine: Run one-page audit
AuditEngine->>TargetPage: Fetch rendered page
AuditEngine-->>PublicCheckAPI: Return measurements and findings
PublicCheckAPI-->>Browser: Return proof-backed JSON
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7005f2ce83
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| const ipHash = await requestIpHash(request); | ||
| const quota = await checkQuotaSet(env, publicCheckQuotaChecks(ipHash, hostname)); |
There was a problem hiding this comment.
Charge quotas to the final redirected host
When the submitted URL redirects to another hostname, this charges every target quota to the original hostname even though shared/audit-engine.js follows the redirect and renders the destination. Attackers can therefore use wildcard subdomains or multiple redirectors to repeatedly hit one victim site without reaching that site's 3/hour or 10/day limits, defeating the protection this anonymous endpoint promises; reject cross-host redirects or also reserve quota for the final host before rendering.
AGENTS.md reference: AGENTS.md:L7-L7
Useful? React with 👍 / 👎.
| error: "Daily one-page check limit reached from this network. Try again tomorrow." | ||
| }, | ||
| { | ||
| bucket: `check:target-hour:${hour.key}:${targetKey}`, |
There was a problem hiding this comment.
Hash target hosts before persisting quota buckets
For every successful validation, this writes the clear target hostname into audit_usage as part of both target bucket keys, while the new page, README, and agent documentation promise that nothing about the check is stored. Because these D1 rows persist beyond the request, checking a sensitive-but-public staging or customer domain leaves a durable record; hash the normalized target host as is already done for network identifiers, or narrow the public storage claim.
AGENTS.md reference: AGENTS.md:L7-L7
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
worker/routes/public-check.test.mjs (2)
77-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd handler-level coverage for
runPublicCheck.The suite covers
validatePublicCheckUrl,publicCheckQuotaChecks,buildPublicCheckResponse, andcheckHtml. It does not coverrunPublicCheckitself. The untested branches carry the security and abuse behavior of the route: the 400 private-address rejection, the 503 missing-WAITLIST_DBresponse, the 429 quota response withresetAt, and the 422 failure path. A test with a stubbedenvand a fakeRequestwould pin those status codes and keep the quota contract stable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@worker/routes/public-check.test.mjs` around lines 77 - 130, Add handler-level tests for runPublicCheck using a stubbed env and fake Request, covering private-address rejection (400), missing WAITLIST_DB (503), quota exhaustion with resetAt (429), and check failure (422). Assert each response status and relevant response payload fields, while keeping existing helper-level coverage unchanged.
187-196: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract the duplicated
visibleWordCounthelper.This helper is identical to
visibleWordCountinworker/routes/pages.test.mjslines 159-168. Move it into one shared test helper module and import it in both files.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@worker/routes/public-check.test.mjs` around lines 187 - 196, Extract the duplicated visibleWordCount helper from the test files into a shared test-helper module, then import and use that shared symbol in both worker/routes/public-check.test.mjs and worker/routes/pages.test.mjs. Preserve the helper’s current HTML-stripping and word-count behavior.worker/routes/public-check.js (2)
180-189: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueConsider a
Retry-Afterhint and quota-neutral capacity failures.The quota counters increment before the audit runs. If the browser pool returns
BROWSER_BUSY, the caller loses one of six hourly checks for a failure the caller did not cause. Consider decrementing or skipping the counter for the 503 path, and adding aretry-afterheader on the 429 and 503 responses so clients can back off correctly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@worker/routes/public-check.js` around lines 180 - 189, Update the public check error handling around the BROWSER_BUSY branch and the existing 429 response to include a Retry-After header with an appropriate backoff value. Ensure capacity failures are quota-neutral by reversing or bypassing the quota increment when BROWSER_BUSY occurs, while preserving the current 503 and 422 response behavior.
304-409: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winUse a relative API path in the inline script.
The submit handler posts to the absolute URL
${origin}/api/public-check. The page is served from the same origin in every route, so a relative/api/public-checkavoids a cross-origin request whenever the page is reached through an alternate hostname, for examplewww.seofixkit.comversusseofixkit.com. A relative path also removes one interpolation of untrusted host data into the script body on the Express path, whereoriginis built from theHostheader.♻️ Proposed change
- const response = await fetch("${origin}/api/public-check", { + const response = await fetch("/api/public-check", {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@worker/routes/public-check.js` around lines 304 - 409, Update the fetch call in the submit handler to use the relative `/api/public-check` path instead of interpolating `origin`. Leave the request method, headers, body, and response handling unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Line 44: Replace the broad “nothing stored” claim with explicit wording that
no report or page contents are stored, while retaining the existing rate-limit
behavior. Apply this wording consistently in the README public-check
description, public/.well-known/skill.md, and the /check response copy in the
public-check route.
In `@scripts/live-promise-spot-check.test.mjs`:
- Around line 39-41: Update the test around spotCheckPublicPages to assert that
every returned result has an empty failures list, rather than only checking
results.length. Keep the existing result-count assertion if needed, and ensure
failures from missing pages or expected strings cause the test to fail.
In `@worker/routes/public-check.js`:
- Around line 159-172: Reorder the route flow so the WAITLIST_DB guard and the
requestIpHash/checkQuotaSet quota enforcement using publicCheckQuotaChecks run
immediately after deriving hostname from validated.url, before
resolvesToPrivateAddress. Preserve the existing 429 response for exhausted
quotas, and only perform the private-address DNS check after the request has
passed quota validation.
---
Nitpick comments:
In `@worker/routes/public-check.js`:
- Around line 180-189: Update the public check error handling around the
BROWSER_BUSY branch and the existing 429 response to include a Retry-After
header with an appropriate backoff value. Ensure capacity failures are
quota-neutral by reversing or bypassing the quota increment when BROWSER_BUSY
occurs, while preserving the current 503 and 422 response behavior.
- Around line 304-409: Update the fetch call in the submit handler to use the
relative `/api/public-check` path instead of interpolating `origin`. Leave the
request method, headers, body, and response handling unchanged.
In `@worker/routes/public-check.test.mjs`:
- Around line 77-130: Add handler-level tests for runPublicCheck using a stubbed
env and fake Request, covering private-address rejection (400), missing
WAITLIST_DB (503), quota exhaustion with resetAt (429), and check failure (422).
Assert each response status and relevant response payload fields, while keeping
existing helper-level coverage unchanged.
- Around line 187-196: Extract the duplicated visibleWordCount helper from the
test files into a shared test-helper module, then import and use that shared
symbol in both worker/routes/public-check.test.mjs and
worker/routes/pages.test.mjs. Preserve the helper’s current HTML-stripping and
word-count behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f2b8dcf2-8886-4c6e-bb57-321e8eced00f
📒 Files selected for processing (15)
README.mdpackage.jsonpublic/.well-known/skill.mdpublic/sitemap.xmlscripts/live-promise-spot-check.mjsscripts/live-promise-spot-check.test.mjsserver/index.jsshared/audit-engine.jsshared/promise-audit.test.mjsworker/index.jsworker/routes/pages.jsworker/routes/pages.test.mjsworker/routes/public-check.jsworker/routes/public-check.test.mjswrangler.jsonc
| - Founder-friendly React interface. | ||
| - Cloudflare Worker target using Workers Static Assets and Browser Run. | ||
| - Locked private-beta homepage with `/api/waitlist` and `/api/access/request` backed by D1. | ||
| - Public anonymous one-page URL check at `/check` and `POST /api/public-check`: real browser rendering of one public page, static-vs-rendered proof, guarded false positives, actionable findings when present, per-network and per-site rate limits, nothing stored, and a handoff into private beta access with no ranking promise. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Narrow the storage claim.
The rate-limit design stores hashed network and target identifiers in audit_usage, and README.md, Line 57 documents quota-bucket cleanup. The phrase nothing stored can mislead users about retention. State that no report or page contents are stored. Apply the same wording to public/.well-known/skill.md, Line 10, and the /check copy in worker/routes/public-check.js, Lines 192-351.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` at line 44, Replace the broad “nothing stored” claim with explicit
wording that no report or page contents are stored, while retaining the existing
rate-limit behavior. Apply this wording consistently in the README public-check
description, public/.well-known/skill.md, and the /check response copy in the
public-check route.
| test("live spot-check passes against the shipped public page copy", async () => { | ||
| const results = await spotCheckPublicPages({ baseUrl: origin, fetcher: pageFetcher() }); | ||
| assert.equal(results.length, 3); | ||
| assert.equal(results.length, 4); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert that all spot-check expectations pass.
spotCheckPublicPages returns failures, but this test only checks the number of result objects. It remains green when /check returns 404 or misses every expected string. Assert that every failure list is empty.
Suggested assertion
assert.equal(results.length, 4);
+ assert.deepEqual(
+ results.flatMap(({ path, failures }) =>
+ failures.map((failure) => `${path}: ${failure}`)
+ ),
+ []
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test("live spot-check passes against the shipped public page copy", async () => { | |
| const results = await spotCheckPublicPages({ baseUrl: origin, fetcher: pageFetcher() }); | |
| assert.equal(results.length, 3); | |
| assert.equal(results.length, 4); | |
| test("live spot-check passes against the shipped public page copy", async () => { | |
| const results = await spotCheckPublicPages({ baseUrl: origin, fetcher: pageFetcher() }); | |
| assert.equal(results.length, 4); | |
| assert.deepEqual( | |
| results.flatMap(({ path, failures }) => | |
| failures.map((failure) => `${path}: ${failure}`) | |
| ), | |
| [] | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/live-promise-spot-check.test.mjs` around lines 39 - 41, Update the
test around spotCheckPublicPages to assert that every returned result has an
empty failures list, rather than only checking results.length. Keep the existing
result-count assertion if needed, and ensure failures from missing pages or
expected strings cause the test to fail.
| const hostname = new URL(validated.url).hostname; | ||
| if (await resolvesToPrivateAddress(hostname)) { | ||
| return jsonNoStore({ error: "This URL points at a private or internal address and cannot be checked." }, 400); | ||
| } | ||
|
|
||
| if (!env.WAITLIST_DB) { | ||
| return jsonNoStore({ error: "Check storage is not configured." }, 503); | ||
| } | ||
|
|
||
| const ipHash = await requestIpHash(request); | ||
| const quota = await checkQuotaSet(env, publicCheckQuotaChecks(ipHash, hostname)); | ||
| if (!quota.ok) { | ||
| return jsonNoStore({ error: quota.error, resetAt: quota.resetAt }, 429); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Enforce the quota before the DNS resolution check.
resolvesToPrivateAddress performs up to two outbound DNS-over-HTTPS requests to cloudflare-dns.com per call (see shared/url-safety.js lines 122-145). This route runs that check before any rate limit applies. An anonymous caller can therefore drive unlimited outbound DNS lookups with a cheap request loop, at zero quota cost and with no D1 write to record the attempt.
Move the WAITLIST_DB guard and the quota check above the private-address check. The quota key uses hostname, which is available from validated.url without resolution.
🛡️ Proposed reordering
const hostname = new URL(validated.url).hostname;
- if (await resolvesToPrivateAddress(hostname)) {
- return jsonNoStore({ error: "This URL points at a private or internal address and cannot be checked." }, 400);
- }
-
if (!env.WAITLIST_DB) {
return jsonNoStore({ error: "Check storage is not configured." }, 503);
}
const ipHash = await requestIpHash(request);
const quota = await checkQuotaSet(env, publicCheckQuotaChecks(ipHash, hostname));
if (!quota.ok) {
return jsonNoStore({ error: quota.error, resetAt: quota.resetAt }, 429);
}
+
+ if (await resolvesToPrivateAddress(hostname)) {
+ return jsonNoStore({ error: "This URL points at a private or internal address and cannot be checked." }, 400);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const hostname = new URL(validated.url).hostname; | |
| if (await resolvesToPrivateAddress(hostname)) { | |
| return jsonNoStore({ error: "This URL points at a private or internal address and cannot be checked." }, 400); | |
| } | |
| if (!env.WAITLIST_DB) { | |
| return jsonNoStore({ error: "Check storage is not configured." }, 503); | |
| } | |
| const ipHash = await requestIpHash(request); | |
| const quota = await checkQuotaSet(env, publicCheckQuotaChecks(ipHash, hostname)); | |
| if (!quota.ok) { | |
| return jsonNoStore({ error: quota.error, resetAt: quota.resetAt }, 429); | |
| } | |
| const hostname = new URL(validated.url).hostname; | |
| if (!env.WAITLIST_DB) { | |
| return jsonNoStore({ error: "Check storage is not configured." }, 503); | |
| } | |
| const ipHash = await requestIpHash(request); | |
| const quota = await checkQuotaSet(env, publicCheckQuotaChecks(ipHash, hostname)); | |
| if (!quota.ok) { | |
| return jsonNoStore({ error: quota.error, resetAt: quota.resetAt }, 429); | |
| } | |
| if (await resolvesToPrivateAddress(hostname)) { | |
| return jsonNoStore({ error: "This URL points at a private or internal address and cannot be checked." }, 400); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@worker/routes/public-check.js` around lines 159 - 172, Reorder the route flow
so the WAITLIST_DB guard and the requestIpHash/checkQuotaSet quota enforcement
using publicCheckQuotaChecks run immediately after deriving hostname from
validated.url, before resolvesToPrivateAddress. Preserve the existing 429
response for exhausted quotas, and only perform the private-address DNS check
after the request has passed quota validation.
) The lane-2 promise audit found the live /check page fails the spot-check, but the failure was misdiagnosed as copy drift. The deployed Worker predates the /check route (commit 4bd9868, #66): /check is served by the static-asset SPA fallback (id="root" shell) instead of the Worker route, and POST /api/public-check returns 405. The spot-check now detects the SPA fallback and reports it as a stale deployment (deploy main, then rerun) instead of pointing at the copy or the claim. Adds a test that worker-rendered pages are never flagged.
GitHub-hosted runners fail at job start (account billing outage, known class). The repo-scoped hardened runner on netcup-rs2000 is online; route every hosted job at it, mirroring TinyStudio.io #66.
What
Turns the public sample into a truthful, searchable proof-to-repair entry path (lane 1, research 2026-08-08, rank 1).
New live surface
GET /check— indexable public entry page: paste one public URL, no account, no email, nothing stored.POST /api/public-check— anonymous handler running the shared audit engine (Browser Run) against one page. Returns only live engine output: measured static-vs-rendered proof, guarded false positives, actionable findings when present, issue counts, engine version, scannedAt, and truthful next-step + no-ranking boundary copy.server/index.jswith in-memory rate limits.Rate limiting & safety
check:ip-hour(6/h),check:ip-day(15/d),check:target-hour(3/h),check:target-day(10/d) buckets in the existingaudit_usageD1 table; per-network counters are IP-hashed, no PII.normalizeUrl+publicAuditUrlStatus+ DNSresolvesToPrivateAddress(SSRF) before any render.Truthfulness sweep (was: "anonymous public audits stay disabled")
/demoboundary copy updated (it now links to the live check),/methodology"What is live today",/packageslive card,llms.txt,.well-known/skill.md, README intro + "What is live in this repo" + Cloudflare path section.rootSitemap,public/sitemap.xml,llms.txt, Workerrun_worker_first, and links from/,/demo,/methodology,/packagesmake the entry path searchable.Verification
npm run checkgreen locally (all suites + build), including newtest:public-check(unit + real-engine fixture test that fails if the public payload drifts from live engine output).audit:live-promise) extended with/check./checkon the local twin: 5/5 links 200, form check againsthttps://example.comrendered proof + guards + findings + handoff, zero console errors, no horizontal scroll.Rollback: remove the
/checkpage +/api/public-checkroute and restore the copy lines; all other claims revert via git.Summary by CodeRabbit
New Features
/checkpage for one-page SEO checks without an account or stored report.Documentation
Tests