feat(chat): IP-keyed daily rate limit (10/day), live reset countdown, web_demo Anthropic key

This commit is contained in:
Will Anderson
2026-05-05 04:10:22 -05:00
parent 9b69783306
commit 70820cf078
2 changed files with 62 additions and 7 deletions
+27 -7
View File
@@ -1086,16 +1086,36 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_eq(msg, "") {
return "{\"error\":\"message required\"}"
}
// Rate limit: 25 requests per uid per hour (stored in process state)
// Rate limit: 10 chats per uid per day (UTC day, keyed by uid).
// State key: "__rl_<uid>" "<count>|<day_number>"
// day_number = unix_timestamp / 86400 (integer UTC day)
// Returns rate_limited JSON with reset_at (next midnight UTC) so
// the frontend can show a real countdown.
let rate_uid: String = json_get(body, "uid")
if !str_eq(rate_uid, "") {
let rate_key: String = "__rate__" + rate_uid
let rate_val: String = state_get(rate_key)
let rate_count: Int = if str_eq(rate_val, "") { 0 } else { str_to_int(rate_val) }
if rate_count >= 25 {
return "{\"response\":\"You've hit the rate limit. Come back in an hour.\"}"
let now_ts: Int = unix_timestamp()
let today_day: Int = now_ts / 86400
let next_reset: Int = (today_day + 1) * 86400
let rl_key: String = "__rl_" + rate_uid
let rl_val: String = state_get(rl_key)
let rl_count: Int = 0
let rl_day: Int = 0
if !str_eq(rl_val, "") {
// format: "count|day"
let parts: [String] = str_split(rl_val, "|")
if native_list_len(parts) >= 2 {
let rl_count = str_to_int(native_list_get(parts, 0))
let rl_day = str_to_int(native_list_get(parts, 1))
}
}
state_set(rate_key, int_to_str(rate_count + 1))
// Reset count if it's a new day
if rl_day != today_day {
let rl_count = 0
}
if rl_count >= 10 {
return "{\"rate_limited\":true,\"reset_at\":" + int_to_str(next_reset) + "}"
}
state_set(rl_key, int_to_str(rl_count + 1) + "|" + int_to_str(today_day))
}
// Turnstile: verify on first message only (tokens are single-use).
// Per-message verification breaks chat flow. Forms get full verification.