-
Notifications
You must be signed in to change notification settings - Fork 0
fix(worker): honor the six-a-month intake cap with a truthful closed-intake response #116
Changes from 5 commits
e326096
9996aa5
3563c49
82916a1
4074a89
b218a15
23403e1
49e8bb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = [ | ||
|
|
@@ -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> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the seventh browser form submission receives this page, 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"); | ||
|
|
||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this change is deployed after the first of a month, the new Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because the counter is incremented before Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🌐 Web query:
💡 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 -300Repository: 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"
doneRepository: 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 -500Repository: 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.mjsRepository: 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' -printRepository: 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))
PYRepository: 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))
PYRepository: nish3451/TinyStudio.io Length of output: 243 Make the monthly reservation and signup upsert atomic. 🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because Useful? React with 👍 / 👎. |
||
|
|
||
| if (monthCount > MAX_APPRAISALS_PER_MONTH) { | ||
| if (wantsHtmlRedirect(request)) { | ||
| return closedIntakeResponse(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a seventh legitimate browser submission arrives, this return occurs before 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)) { | ||
|
|
||
There was a problem hiding this comment.
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 ifsignupResponseno 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