Security and performance fixes addressing a comprehensive review: - Server-issued HMAC-signed session cookies; client-supplied session_id ignored. Prevents session hijacking via body substitution. - Sliding-window rate limiter per IP and per session. - SessionStore with LRU eviction, idle TTL, per-session threading locks, and a hard turn cap. Bounds memory and serializes concurrent turns for the same session so FastAPI's threadpool cannot corrupt history. - Tool-use loop capped at settings.max_tool_use_iterations; Anthropic client gets an explicit timeout. No more infinite-loop credit burn. - Every tool argument is regex-validated, length-capped, and control-character-stripped. asserts replaced with ValueError so -O cannot silently disable the checks. - PII-safe warning logs: session IDs and reply bodies are hashed, never logged in clear. - hmac.compare_digest for email comparison (constant-time). - Strict Content-Security-Policy plus X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy via middleware. - Explicit handlers for anthropic.RateLimitError, APIConnectionError, APIStatusError, ValueError; static dir resolved from __file__. - Prompt cache breakpoints on the last tool schema and the last message so per-turn input cost scales linearly, not quadratically. - TypedDict handler argument shapes; direct block.name/block.id access. - functools.lru_cache on _get_client. - Anchored word-boundary regexes for out-of-scope detection to kill false positives on phrases like "I'd recommend contacting...". Literate program: - Bookly.lit.md is now the single source of truth for the five core Python files. Tangles byte-for-byte; verified via tangle.ts --verify. - Prose walkthrough, three mermaid diagrams, narrative per module. - Woven to static/architecture.html with the app's palette (background #f5f3ee) via scripts/architecture-header.html. - New GET /architecture route serves the HTML with a relaxed CSP that allows pandoc's inline styles. Available at bookly.codyborders.com/architecture. - scripts/rebuild_architecture_html.sh regenerates the HTML after edits. - code_reviews/2026-04-15-1433-code-review.md captures the review that drove these changes. All 37 tests pass.
88 lines
2.8 KiB
JavaScript
88 lines
2.8 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const messagesEl = document.getElementById("messages");
|
|
const formEl = document.getElementById("composer");
|
|
const inputEl = document.getElementById("input");
|
|
const sendEl = document.getElementById("send");
|
|
|
|
const GREETING =
|
|
"Hi! I'm the Bookly support assistant. I can help you check on an order, start a return, or answer questions about shipping, returns, or password reset. How can I help today?";
|
|
|
|
function appendMessage(role, text) {
|
|
const el = document.createElement("div");
|
|
el.className = "message message--" + role;
|
|
// SECURITY: always use textContent here, never innerHTML. The reply
|
|
// comes from the model and must be treated as untrusted data.
|
|
el.textContent = text;
|
|
messagesEl.appendChild(el);
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
return el;
|
|
}
|
|
|
|
function appendTypingIndicator() {
|
|
const el = document.createElement("div");
|
|
el.className = "message message--assistant message--typing";
|
|
el.setAttribute("aria-label", "Assistant is typing");
|
|
const dotCount = 3;
|
|
for (let i = 0; i < dotCount; i += 1) {
|
|
el.appendChild(document.createElement("span"));
|
|
}
|
|
messagesEl.appendChild(el);
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
return el;
|
|
}
|
|
|
|
async function sendMessage(text) {
|
|
// The session is tracked server-side via an HttpOnly cookie. We do not
|
|
// send a session_id in the body and cannot read the cookie from JS.
|
|
const response = await fetch("/api/chat", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ message: text }),
|
|
});
|
|
if (response.status === 429) {
|
|
throw new Error("rate_limited");
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error("Server returned " + response.status);
|
|
}
|
|
const data = await response.json();
|
|
return data.reply;
|
|
}
|
|
|
|
formEl.addEventListener("submit", async function (event) {
|
|
event.preventDefault();
|
|
const text = inputEl.value.trim();
|
|
if (!text) return;
|
|
|
|
appendMessage("user", text);
|
|
inputEl.value = "";
|
|
inputEl.disabled = true;
|
|
sendEl.disabled = true;
|
|
|
|
const typing = appendTypingIndicator();
|
|
try {
|
|
const reply = await sendMessage(text);
|
|
typing.remove();
|
|
appendMessage("assistant", reply);
|
|
} catch (err) {
|
|
typing.remove();
|
|
const message =
|
|
err && err.message === "rate_limited"
|
|
? "You're sending messages very fast. Please wait a moment and try again."
|
|
: "Sorry, I couldn't reach the server. Please try again in a moment.";
|
|
appendMessage("assistant", message);
|
|
console.error(err);
|
|
} finally {
|
|
inputEl.disabled = false;
|
|
sendEl.disabled = false;
|
|
inputEl.focus();
|
|
}
|
|
});
|
|
|
|
appendMessage("assistant", GREETING);
|
|
inputEl.focus();
|
|
})();
|