Skip to content
This repository was archived by the owner on Sep 28, 2026. It is now read-only.
23 changes: 23 additions & 0 deletions scripts/check-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,29 @@ for (const text of requiredWorkerCopy) {
if (!worker.includes(text)) failures.push(`Missing worker agent behavior: ${text}`);
}

// Monthly intake cap (the "six a month" public promise). The site promises
// "when the sixth is taken, the intake closes until the next" on five
// surfaces; the signup handler must honor it: a calendar-month counter that
// rejects the seventh signup with a truthful closed-intake response (JSON
// error "intake_closed" for API clients, a self-contained 409 page for form
// posts). These are STATIC SOURCE GUARDS, not behavioral tests — the
// behavioral proof lives in test-agent-worker.mjs.
if (!worker.includes("MAX_APPRAISALS_PER_MONTH")) {
failures.push("Worker must define MAX_APPRAISALS_PER_MONTH (the six-a-month cap).");
}
if (!worker.includes("signup:")) {
failures.push("Worker must key the monthly signup cap on a signup:YYYY-MM bucket.");
}
if (!worker.includes("\"intake_closed\"")) {
failures.push("Worker must expose the intake_closed error for API signup clients.");
}
if (!worker.includes("closedIntakeResponse()")) {
failures.push("Worker must serve the closed-intake page to browser form posts.");
}
if (!worker.includes("The six appraisals for this month are taken")) {
failures.push("Closed-intake page must state the six-a-month truth.");
Comment on lines +184 to +197

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the static guards check executable behavior.

These checks only search for text. For example, closedIntakeResponse() matches its function declaration even if signupResponse no longer calls it. The other strings can also remain in unused code or comments while the signup cap flow is removed.

Match the relevant executable statements or inspect the parsed Worker source. Keep the behavioral tests as the primary contract 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 `@scripts/check-site.mjs` around lines 184 - 197, Update the static guards in
the check-site script to validate executable signup-flow behavior rather than
merely searching for strings. Tie the MAX_APPRAISALS_PER_MONTH, signup: bucket,
intake_closed response, closedIntakeResponse invocation, and closed-intake
message checks to the relevant parsed Worker statements or call relationships,
while preserving the existing behavioral tests as the primary contract.

}

for (const text of requiredPublicArtifacts) {
// llms.txt and offer.md are mirrors of the same offer contract. A fact must
// appear in BOTH (case-insensitively, since one file may head it while the
Expand Down
104 changes: 104 additions & 0 deletions scripts/test-agent-worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,3 +1065,107 @@ test("retired API host frames the current offer as The Website Appraisal, not th
assert.match(body.message, /free leak audit of high-ticket service homepages/, "retired API host message must state the current offer truth");
assert.doesNotMatch(body.message, /self-serve Agent Desk/, "retired API host message must not point at the retired Agent Desk as the current offer");
});

// Scripted counter for the monthly signup cap: every INSERT into
// agent_usage_limits returns the next count for its bucket key, mirroring
// the worker's incrementUsageCounter upsert (RETURNING count).
class CountingFakeDB extends FakeDB {
constructor() {
super();
this.counts = new Map();
}

prepare(sql) {
return new CountingFakeStatement(this, sql);
}
}

class CountingFakeStatement extends FakeStatement {
async first() {
this.db.calls.push({ method: "first", sql: this.sql, values: this.values });
if (this.sql.includes("agent_usage_limits") && this.values[0] && String(this.values[0]).startsWith("signup:")) {
const key = this.values[0];
const next = (this.db.counts.get(key) || 0) + 1;
this.db.counts.set(key, next);
return { count: next };
}
return { count: 1 };
}
}

function signupRequest(email, website, accept = "text/html") {
return new Request("https://tinystudio.io/api/signups", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://tinystudio.io",
"Accept": accept,
"User-Agent": "tinystudio-worker-test"
},
body: new URLSearchParams({ website, email }).toString()
});
}

test("signup handler accepts six signups in a calendar month, then closes the intake truthfully (JSON)", async () => {
const db = new CountingFakeDB();
const env = { DB: db, AI: new FakeAI("") };

for (let i = 1; i <= 6; i++) {
const res = await worker.fetch(signupRequest(`cap-json-${i}@example.com`, `example-${i}.com`, "application/json"), env);
assert.equal(res.status, 201, `signup ${i} of 6 must succeed`);
}

const seventh = await worker.fetch(signupRequest("cap-json-7@example.com", "example-7.com", "application/json"), env);
assert.equal(seventh.status, 409, "the seventh signup in the month must be rejected");
const body = await seventh.json();
assert.equal(body.ok, false);
assert.equal(body.error, "intake_closed");
assert.match(body.message, /six appraisals for this month are taken/, "the closed signal must state the six-a-month truth");

// The cap counter must be a single calendar-month bucket.
const buckets = [...db.counts.keys()];
assert.equal(buckets.length, 1, "all signups in one month share one bucket");
assert.match(buckets[0], /^signup:\d{4}-\d{2}$/, "the bucket key must be signup:YYYY-MM");
assert.equal(db.counts.get(buckets[0]), 7, "the counter records all seven attempts so the intake stays closed");

// No seventh row is persisted: exactly six INSERTs into email_signups.
const signupInserts = db.calls.filter((call) => call.sql.includes("INSERT INTO email_signups"));
assert.equal(signupInserts.length, 6, "the closed seventh signup must not be stored");
});

test("signup handler serves the truthful closed-intake page to a browser form post after six signups", async () => {
const db = new CountingFakeDB();
const env = { DB: db, AI: new FakeAI("") };

for (let i = 1; i <= 6; i++) {
const res = await worker.fetch(signupRequest(`cap-html-${i}@example.com`, `example-${i}.com`), env);
assert.equal(res.status, 303, `browser signup ${i} of 6 must redirect to the thank-you page`);
assert.equal(new URL(res.headers.get("Location")).pathname, "/brief-requested");
}

const seventh = await worker.fetch(signupRequest("cap-html-7@example.com", "example-7.com"), env);
assert.equal(seventh.status, 409, "the seventh browser form post must not land on the thank-you page");
const html = await seventh.text();
assert.match(html, /The six appraisals for this month are taken/, "the closed page must state the six-a-month truth");
assert.match(html, /intake closes until the next/, "the closed page must echo the public promise wording");
assert.doesNotMatch(html, /request received/i, "the closed page must not impersonate the success page");

const signupInserts = db.calls.filter((call) => call.sql.includes("INSERT INTO email_signups"));
assert.equal(signupInserts.length, 6, "the closed seventh browser signup must not be stored");
});

test("signup handler does not consume a monthly slot for an invalid email", async () => {
const db = new CountingFakeDB();
const env = { DB: db, AI: new FakeAI("") };

const invalid = await worker.fetch(signupRequest("not-an-email", "example.com"), env);
assert.equal(invalid.status, 303, "invalid email still takes the existing invalid-signal redirect");
assert.equal(new URL(invalid.headers.get("Location")).search, "?signal=invalid");

const valid = await worker.fetch(signupRequest("cap-valid-1@example.com", "example.com", "application/json"), env);
assert.equal(valid.status, 201, "a valid signup after an invalid attempt must still be accepted");

const buckets = [...db.counts.keys()].filter((key) => key.startsWith("signup:"));
assert.equal(buckets.length, 1, "only the valid signup created a monthly bucket");
assert.equal(db.counts.get(buckets[0]), 1, "the invalid attempt must not consume a slot");
});
64 changes: 64 additions & 0 deletions src/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ const MAX_FIELD_LENGTH = 1800;
const MAX_REQUEST_BYTES = 24000;
const SOFT_AGENT_RUNS_PER_EMAIL_PER_DAY = 5;
const MAX_AGENT_RUNS_PER_IP_PER_DAY = 20;
// Public promise: "Six a month. When the sixth is taken, the intake closes
// until the next." (homepage, /audit, /pricing, /agents, llms.txt). The
// signup endpoint must honor it: the sixth valid signup in a calendar month
// is accepted, and any further POST in the same month gets a truthful
// closed-intake response instead of a normal success.
const MAX_APPRAISALS_PER_MONTH = 6;
const CURRENCY_AMOUNT_PATTERN = String.raw`(?:(?:₹|\$|€|£|inr|usd|us\$|aud|cad|sgd|gbp|eur|rs\.?|rupees?)\s*\d[\d,.]*(?:\s*(?:k|lakh|lakhs|l|cr))?|\d[\d,.]*\s*(?:inr|usd|aud|cad|sgd|gbp|eur|rupees?))`;
const METRIC_VALUE_PATTERN = String.raw`(?:${CURRENCY_AMOUNT_PATTERN}|\b\d[\d,.]*\b)`;
const WEEKLY_METRIC_LABELS = [
Expand Down Expand Up @@ -283,6 +289,46 @@ function htmlRedirect(url, signal) {
return withSecurityHeaders(Response.redirect(nextUrl.toString(), 303));
}

// Truthful closed-intake page for the monthly "six a month" cap. The form
// posts with Accept: text/html, so a redirect would need homepage machinery
// to render; a self-contained response (the same pattern as the retired-host
// pages) tells the visitor the truth in place, with no JS and no new asset.
function closedIntakeResponse() {
return withSecurityHeaders(
new Response(
`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TinyStudio — The intake is closed</title>
<style>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move the closed-page styles behind the CSP

When the seventh browser form submission receives this page, withSecurityHeaders attaches the worker's style-src 'self' https://fonts.googleapis.com policy, which does not permit this inline <style> block. Browsers therefore discard all of these rules and render the intended self-contained closed-intake page unstyled; serve the CSS from an allowed asset or authorize this block with a nonce/hash.

Useful? React with 👍 / 👎.

body{margin:0;min-height:100vh;display:grid;place-items:center;background:#fffdf7;color:#171713;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
main{width:min(720px,calc(100% - 40px));padding:48px;border:1px solid rgba(23,23,19,.14);border-radius:8px;background:#fff}
h1{margin:0;font-size:clamp(34px,6vw,60px);line-height:1.05;letter-spacing:0}
p{color:#57534b;font-size:18px;line-height:1.55}
a{display:inline-flex;align-items:center;min-height:46px;padding:0 16px;border-radius:8px;background:#171713;color:#fffdf7;font-weight:800;text-decoration:none}
</style>
</head>
<body>
<main>
<h1>The six appraisals for this month are taken.</h1>
<p>Six a month, done by hand. When the sixth is taken, the intake closes until the next — and it is closed now. The form on the homepage will accept requests again on the first of next month.</p>
<a href="https://tinystudio.io/">Back to TinyStudio.io</a>
</main>
</body>
</html>`,
{
status: 409,
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store"
}
}
)
);
}

function signupPagePath(request, fallback) {
const referer = request.headers.get("Referer");

Expand Down Expand Up @@ -359,6 +405,24 @@ async function signupResponse(request, env, url) {
return jsonResponse({ ok: false, error: "invalid_email" }, { status: 400 });
}

// Monthly intake cap (the "six a month" promise). The bucket key uses the
// calendar month so the counter resets naturally on the first of the next
// month; the increment is the reservation, so the sixth request passes and
// every request after it in the same month is told the truth: the intake
// is closed until the next.
const monthBucket = `signup:${new Date().toISOString().slice(0, 7)}`;
const monthCount = await incrementUsageCounter(env, monthBucket);
Comment on lines +413 to +414

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Seed the counter with this month's accepted signups

When this change is deployed after the first of a month, the new signup:YYYY-MM bucket does not exist and nothing backfills it from the signups already stored that month. If n requests were accepted before deployment, this starts at zero and accepts six more, allowing n + 6 appraisals during the month and immediately breaking the cap this change is meant to enforce.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not spend slots on upserted duplicate submissions

Because the counter is incremented before saveEmailSignup, every repeat submission consumes another slot even though the subsequent ON CONFLICT(email) DO UPDATE leaves only one signup row. Reposting the same valid email six times therefore fills the global monthly quota while the database contains only one actionable request, causing the next genuine applicant to receive intake_closed; deduplicate the request before reserving capacity or count records that can actually be fulfilled.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve and persist each slot atomically

If saveEmailSignup fails after this independent counter statement succeeds—for example because of a transient D1 write failure—the request is not stored, but its monthly slot remains permanently consumed. Enough such failures make intake close before six appraisals have been accepted, so the reservation and signup write need a single transaction/batch or the reservation must be rolled back on failure.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Cloudflare D1 documentation: Can D1Database.batch execute a monthly counter upsert and an email_signups upsert atomically, with rollback when either statement fails?

💡 Result:

Yes, Cloudflare D1's batch method can execute multiple SQL statements, such as a monthly counter upsert and an email_signups upsert, as an atomic operation [1][2]. According to official Cloudflare documentation, batched statements are executed as a single transaction [1][3]. If any statement within the provided sequence fails, the entire batch is aborted or rolled back [1][4][2]. Conversely, if all statements in the batch succeed, they are committed together [2]. Key points regarding this behavior include: - Atomicity: The batch API guarantees that multiple mutations are processed together, providing an all-or-nothing outcome [5][2]. - No Interactive Transactions: D1 does not support traditional session-based transactions (e.g., explicit BEGIN, COMMIT, or ROLLBACK commands) [6][7]. The batch method is the recommended way to perform atomic multi-statement operations [8][2]. - Constraints: Because the batch executes as a single transaction, you cannot use the results of one statement within the same batch to inform the parameters of subsequent statements in that same batch [9][6]. You must know all statements and their parameters upfront [8].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- plan files ---'
find .. -maxdepth 3 -type f \( -iname '*plan*' -o -name 'PLAN.md' \) -print 2>/dev/null | head -50

printf '%s\n' '--- worker outline ---'
ast-grep outline src/worker.js 2>/dev/null | head -200 || true

printf '%s\n' '--- relevant symbols and call sites ---'
rg -n -C 12 'incrementUsageCounter|saveEmailSignup|monthBucket|monthCount|email_signups|usage' src test tests 2>/dev/null | head -500

printf '%s\n' '--- tracked test files ---'
git ls-files | rg '(^|/)(test|tests|spec|__tests__)(/|\\.)|worker|d1|signup' | head -300

Repository: nish3451/TinyStudio.io

Length of output: 14546


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- signup flow ---'
sed -n '320,465p' src/worker.js

printf '%s\n' '--- schema and migrations ---'
for file in $(git ls-files 'migrations/*' 'schema*' '*.sql' | head -80); do
  printf '\n--- %s ---\n' "$file"
  cat -n "$file"
done

printf '%s\n' '--- worker test script ---'
cat -n scripts/test-agent-worker.mjs | head -260

printf '%s\n' '--- package and configuration ---'
for file in $(git ls-files 'package.json' 'wrangler.toml' 'wrangler.json' 'wrangler.jsonc' '*.config.*' | head -80); do
  printf '\n--- %s ---\n' "$file"
  cat -n "$file"
done

Repository: nish3451/TinyStudio.io

Length of output: 20397


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- signup-related tests and routes ---'
rg -n -C 18 'signup|signal_saved|intake_closed|MAX_APPRAISALS_PER_MONTH|/api/signup|saveEmailSignup' scripts src README.md docs 2>/dev/null | head -700

printf '%s\n' '--- database batch usage and exports ---'
rg -n -C 8 '\\.batch\\(|export default|async fetch|signupResponse' src scripts 2>/dev/null | head -400

printf '%s\n' '--- D1 API references in repository ---'
rg -n -i -C 5 'D1Database|transaction|atomic|rollback|batch' . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' 2>/dev/null | head -500

Repository: nish3451/TinyStudio.io

Length of output: 48771


🏁 Script executed:

#!/bin/bash
set -eu

PLAN=$(find specs -type f -path '*/plan.md' -print -quit 2>/dev/null || true)
if [ -n "$PLAN" ]; then
  printf '%s\n' "--- $PLAN ---"
  cat -n "$PLAN"
else
  printf '%s\n' 'No specs plan found'
fi

printf '%s\n' '--- signup test block ---'
sed -n '990,1180p' scripts/test-agent-worker.mjs

Repository: nish3451/TinyStudio.io

Length of output: 11390


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- current product plan ---'
cat -n specs/004-website-appraisal/plan.md

printf '%s\n' '--- all plan paths ---'
find specs -type f -path '*/plan.md' -print

Repository: nish3451/TinyStudio.io

Length of output: 4253


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import sqlite3

schema = """
CREATE TABLE agent_usage_limits (
  bucket_key TEXT PRIMARY KEY,
  count INTEGER NOT NULL DEFAULT 0,
  first_seen_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);
CREATE TABLE email_signups (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL UNIQUE,
  source TEXT NOT NULL,
  page_path TEXT NOT NULL,
  referer TEXT,
  user_agent TEXT,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  website TEXT
);
"""

counter_sql = """
INSERT INTO agent_usage_limits (bucket_key, count, first_seen_at, updated_at)
VALUES (?, 1, ?, ?)
ON CONFLICT(bucket_key) DO UPDATE SET
  count = count + 1,
  updated_at = excluded.updated_at
"""

signup_sql = """
INSERT INTO email_signups
  (email, source, page_path, referer, user_agent, created_at, updated_at, website)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"""

def state(db):
    count = db.execute(
        "SELECT count FROM agent_usage_limits WHERE bucket_key = 'signup:2026-08'"
    ).fetchone()
    signup_count = db.execute("SELECT COUNT(*) FROM email_signups").fetchone()[0]
    return (count[0] if count else 0, signup_count)

# Model the current two-call flow. The second mutation fails.
db = sqlite3.connect(":memory:")
db.executescript(schema)
db.execute("BEGIN")
db.execute(counter_sql, ("signup:2026-08", "now", "now"))
db.commit()
try:
    db.execute(signup_sql, ("x@example.com", "agent-self-serve", "/", None, None, "now", "now", None))
    db.commit()
except sqlite3.IntegrityError:
    db.rollback()
print("current_flow_after_signup_failure:", state(db))

# Model both mutations in one transaction. The same failure rolls back the reservation.
db = sqlite3.connect(":memory:")
db.executescript(schema)
try:
    db.execute("BEGIN")
    db.execute(counter_sql, ("signup:2026-08", "now", "now"))
    db.execute(signup_sql, ("x@example.com", "agent-self-serve", "/", None, None, "now", "now", None))
    db.execute(signup_sql, ("x@example.com", "agent-self-serve", "/", None, None, "now", "now", None))
    db.commit()
except sqlite3.IntegrityError:
    db.rollback()
print("atomic_flow_after_signup_failure:", state(db))
PY

Repository: nish3451/TinyStudio.io

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import sqlite3

schema = """
CREATE TABLE agent_usage_limits (
  bucket_key TEXT PRIMARY KEY,
  count INTEGER NOT NULL DEFAULT 0,
  first_seen_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);
CREATE TABLE email_signups (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL UNIQUE,
  source TEXT NOT NULL,
  page_path TEXT NOT NULL,
  referer TEXT,
  user_agent TEXT,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  website TEXT
);
CREATE TRIGGER fail_signup
BEFORE INSERT ON email_signups
BEGIN
  SELECT RAISE(ABORT, 'injected signup failure');
END;
"""

counter_sql = """
INSERT INTO agent_usage_limits (bucket_key, count, first_seen_at, updated_at)
VALUES (?, 1, ?, ?)
ON CONFLICT(bucket_key) DO UPDATE SET
  count = count + 1,
  updated_at = excluded.updated_at
"""

signup_sql = """
INSERT INTO email_signups
  (email, source, page_path, referer, user_agent, created_at, updated_at, website)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(email) DO UPDATE SET updated_at = excluded.updated_at
"""

def state(db):
    count = db.execute(
        "SELECT count FROM agent_usage_limits WHERE bucket_key = 'signup:2026-08'"
    ).fetchone()
    signup_count = db.execute("SELECT COUNT(*) FROM email_signups").fetchone()[0]
    return (count[0] if count else 0, signup_count)

def mutate(db):
    db.execute(counter_sql, ("signup:2026-08", "now", "now"))
    db.execute(signup_sql, ("x@example.com", "agent-self-serve", "/", None, None, "now", "now", None))

# Current flow: the counter is committed before saveEmailSignup runs.
db = sqlite3.connect(":memory:")
db.executescript(schema)
db.execute("BEGIN")
db.execute(counter_sql, ("signup:2026-08", "now", "now"))
db.commit()
try:
    db.execute("BEGIN")
    db.execute(signup_sql, ("x@example.com", "agent-self-serve", "/", None, None, "now", "now", None))
    db.commit()
except sqlite3.DatabaseError:
    db.rollback()
print("current_flow_after_signup_failure:", state(db))

# Atomic flow: both mutations share one transaction.
db = sqlite3.connect(":memory:")
db.executescript(schema)
try:
    db.execute("BEGIN")
    mutate(db)
    db.commit()
except sqlite3.DatabaseError:
    db.rollback()
print("atomic_flow_after_signup_failure:", state(db))
PY

Repository: nish3451/TinyStudio.io

Length of output: 243


Make the monthly reservation and signup upsert atomic. signupResponse increments the counter at src/worker.js:414 before saveEmailSignup persists the signup at src/worker.js:426. If the upsert fails, the counter remains incremented and the intake can close with fewer than six stored signups. Use env.DB.batch() for both mutations, preserve the six-signup guard, and add a regression test that forces the signup write to fail and asserts that the monthly counter remains unchanged.

🤖 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 `@src/worker.js` at line 414, Update signupResponse to execute the monthly
counter increment and saveEmailSignup upsert atomically through env.DB.batch(),
while preserving the six-signup guard. Ensure a failed signup write rolls back
the counter, and add a regression test that forces the upsert failure and
verifies the monthly counter is unchanged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject unusable websites before reserving capacity

For /api/signups callers that bypass the browser's required/pattern constraints, an omitted or malformed website makes normalizeWebsite return null, but a valid email still reaches this increment and consumes one of the six slots. Six such requests therefore close intake while persisting only non-actionable appraisal rows with no URL to inspect; validate the normalized website before reserving capacity.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Protect the global quota from cross-site submissions

Because /api/signups never calls validateAgentRequest and has no per-IP limit or human-verification gate, a third-party page can submit six cross-origin form POSTs with distinct syntactically valid emails and websites; each reaches this increment despite its hostile Origin, fills the single global bucket, and causes every legitimate applicant to receive intake_closed for the rest of the month. Apply origin/Sec-Fetch-Site validation and abuse resistance before allowing a request to reserve one of only six global slots.

Useful? React with 👍 / 👎.


if (monthCount > MAX_APPRAISALS_PER_MONTH) {
if (wantsHtmlRedirect(request)) {
return closedIntakeResponse();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep overflow applicants on the promised next-month list

When a seventh legitimate browser submission arrives, this return occurs before saveEmailSignup, so the applicant's email and website are discarded. The existing homepage tells applicants that when the month is full, “you go on the list for the next one” (public/index.html:236-237), but this path leaves no record through which they can be contacted or scheduled next month; persist overflow as a waitlist entry without consuming a current-month slot, or revise the public contract accordingly.

AGENTS.md reference: AGENTS.md:L2-L3

Useful? React with 👍 / 👎.

}
return jsonResponse(
{ ok: false, error: "intake_closed", message: "The six appraisals for this month are taken. The intake is closed until the next." },
{ status: 409 }
);
}

await saveEmailSignup(request, env, url, email, "agent-self-serve", website);

if (wantsHtmlRedirect(request)) {
Expand Down
Loading