feat(chat): IP-keyed daily rate limit (10/day), live reset countdown, web_demo Anthropic key
This commit is contained in:
@@ -276,6 +276,41 @@ fn main() -> Void {
|
||||
});
|
||||
var d = await r.json();
|
||||
if (thinking) thinking.remove();
|
||||
|
||||
// Server-side rate limit — show a live countdown to reset
|
||||
if (d.rate_limited && d.reset_at) {
|
||||
var _showRateTimer = function() {
|
||||
var now = Math.floor(Date.now() / 1000);
|
||||
var secsLeft = Math.max(0, d.reset_at - now);
|
||||
var hh = Math.floor(secsLeft / 3600);
|
||||
var mm = Math.floor((secsLeft % 3600) / 60);
|
||||
var ss = secsLeft % 60;
|
||||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||||
var ts = hh > 0 ? (hh + ':' + pad(mm) + ':' + pad(ss)) : (pad(mm) + ':' + pad(ss));
|
||||
return 'You\'ve had 10 conversations today. Come back in ' + ts + '.';
|
||||
};
|
||||
addMsg('ai', _showRateTimer());
|
||||
// Update the last ai message with a live ticker
|
||||
var _timerInterval = setInterval(function() {
|
||||
var thMsgsInner = document.getElementById('neuron-demo-msgs');
|
||||
if (!thMsgsInner) { clearInterval(_timerInterval); return; }
|
||||
var aiMsgs = thMsgsInner.querySelectorAll('.neuron-msg-ai');
|
||||
var lastAi = aiMsgs[aiMsgs.length - 1];
|
||||
if (lastAi) { lastAi.textContent = _showRateTimer(); }
|
||||
if (Math.floor(Date.now() / 1000) >= d.reset_at) {
|
||||
clearInterval(_timerInterval);
|
||||
if (lastAi) { lastAi.textContent = 'You\'re all set — conversations reset. Say hello!'; }
|
||||
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
|
||||
if (btn) { btn.disabled = false; }
|
||||
}
|
||||
}, 1000);
|
||||
if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; }
|
||||
if (btn) { btn.disabled = true; }
|
||||
if (btn) { btn.disabled = false; }
|
||||
if (input) { input.focus(); }
|
||||
return;
|
||||
}
|
||||
|
||||
_um(session, d.sn, d.se);
|
||||
var reply = d.response || d.reply || d.message || '';
|
||||
var isError = !reply || reply === 'Stepped out for a moment. Try again.';
|
||||
|
||||
+27
-7
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user