Spaces:
Sleeping
Sleeping
Download src/app.js from AustinWealth/cc-space: direct link, hf CLI and curl.
- Browser
- Download file 5.87 kB
-
https://huggingface.co/spaces/AustinWealth/cc-space/resolve/main/src/app.js
- Command line
-
hf download hf://spaces/AustinWealth/cc-space/src/app.js
-
curl -L -o app.js https://huggingface.co/spaces/AustinWealth/cc-space/resolve/main/src/app.js
5.87 kB
| import express from "express"; | |
| import { existsSync, readFileSync } from "node:fs"; | |
| import { timingSafeEqual } from "node:crypto"; | |
| import path from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| import { answer } from "./answer.js"; | |
| import { COORDINATOR_EMAIL, KB_VERSION, LOOKUP_RATE_LIMIT, MAX_MESSAGE_CHARS, RATE_LIMIT } from "./constants.js"; | |
| import { createInsights } from "./insights.js"; | |
| import { gatesRequested, skipGates } from "./localtest.js"; | |
| import { lookup } from "./lookup.js"; | |
| import { verifyRecaptcha } from "./recaptcha.js"; | |
| import { buildIndex } from "./retrieve.js"; | |
| const here = path.dirname(fileURLToPath(import.meta.url)); | |
| // The coordinator lists dates in server/data/deadlines.json: [{ "label": "...", "date": "YYYY-MM-DD" }]. | |
| // Only dates that have not passed are shown. An empty file shows nothing. | |
| function upcomingDeadlines(file = path.resolve(here, "../data/deadlines.json")) { | |
| try { | |
| const today = new Date().toISOString().slice(0, 10); | |
| return JSON.parse(readFileSync(file, "utf8")) | |
| .filter((d) => typeof d.label === "string" && /^\d{4}-\d{2}-\d{2}$/.test(d.date) && d.date >= today) | |
| .sort((a, b) => a.date.localeCompare(b.date)) | |
| .slice(0, 3); | |
| } catch { | |
| return []; | |
| } | |
| } | |
| const sameKey = (a, b) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b)); | |
| export function createApp({ kb, llm, apiKey, verify = verifyRecaptcha, log = console.log, allowedOrigin = process.env.ALLOWED_ORIGIN, env = process.env } = {}) { | |
| const index = buildIndex(kb); | |
| const insights = createInsights(kb.segments.map((s) => s.id)); | |
| const allowed = String(allowedOrigin || "").split(",").map((o) => o.trim()).filter(Boolean); | |
| const app = express(); | |
| app.disable("x-powered-by"); | |
| app.set("trust proxy", 1); | |
| app.use(express.json({ limit: "16kb" })); | |
| // Log method, path, status and time only. Never the request body. | |
| app.use((req, res, next) => { | |
| const t0 = Date.now(); | |
| res.on("finish", () => log(`${req.method} ${req.path} ${res.statusCode} ${Date.now() - t0}ms`)); | |
| next(); | |
| }); | |
| app.use((req, res, next) => { | |
| const origin = req.headers.origin; | |
| if (allowedOrigin && origin && allowed.includes(origin)) { | |
| res.setHeader("Access-Control-Allow-Origin", origin); | |
| res.setHeader("Vary", "Origin"); | |
| res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); | |
| res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); | |
| } | |
| res.setHeader("X-Content-Type-Options", "nosniff"); | |
| res.setHeader("Referrer-Policy", "no-referrer"); | |
| if (req.method === "OPTIONS") return res.sendStatus(204); | |
| next(); | |
| }); | |
| const makeLimiter = ({ windowMs, max }) => { | |
| const hits = new Map(); | |
| return (req, res, next) => { | |
| if (skipGates(req, env)) return next(); | |
| const now = Date.now(); | |
| const rec = hits.get(req.ip); | |
| if (!rec || now - rec.start > windowMs) hits.set(req.ip, { start: now, n: 1 }); | |
| else if (++rec.n > max) { | |
| return res.status(429).json({ error: "Too many questions at once. Please wait a minute and try again." }); | |
| } | |
| next(); | |
| }; | |
| }; | |
| const chatLimiter = makeLimiter(RATE_LIMIT); | |
| const lookupLimiter = makeLimiter(LOOKUP_RATE_LIMIT); | |
| const validate = (req, res, next) => { | |
| const { message } = req.body ?? {}; | |
| if (typeof message !== "string" || !message.trim()) { | |
| return res.status(400).json({ error: "Type a question first." }); | |
| } | |
| if (message.length > MAX_MESSAGE_CHARS) { | |
| return res.status(400).json({ error: `Please keep it under ${MAX_MESSAGE_CHARS} characters.` }); | |
| } | |
| next(); | |
| }; | |
| app.get("/api/kb", (_req, res) => { | |
| const { version, glossary, groups, segments } = kb; | |
| res.json({ | |
| version, | |
| glossary, | |
| groups, | |
| coordinatorEmail: COORDINATOR_EMAIL, | |
| maxChars: MAX_MESSAGE_CHARS, | |
| popular: insights.popular(), | |
| deadlines: upcomingDeadlines(), | |
| recaptchaSiteKey: gatesRequested(env) ? null : env.RECAPTCHA_SITE_KEY || null, | |
| segments: segments.map(({ id, group, title, icon, blurb, steps }) => ({ id, group, title, icon, blurb, steps })), | |
| }); | |
| }); | |
| // Step 1: is there a written document for this? Free, no model call. | |
| app.post("/api/lookup", lookupLimiter, validate, (req, res) => { | |
| res.json({ doc: lookup(index, req.body.message.trim()) }); | |
| }); | |
| // Step 2 (when the document did not help): the chatbot. Rate limited and reCAPTCHA gated. | |
| app.post("/api/chat", chatLimiter, validate, async (req, res) => { | |
| const check = skipGates(req, env) ? { ok: true, reason: "local-testing" } : await verify(req.body.captchaToken, req.ip); | |
| if (!check.ok) { | |
| return res.status(403).json({ error: "We could not confirm you are a person. Please try again.", reason: check.reason }); | |
| } | |
| const { message, history } = req.body; | |
| const out = await answer({ index, message: message.trim(), history, llm, ...(apiKey !== undefined && { apiKey }) }); | |
| res.json(out); | |
| }); | |
| // Anonymous counts so the coordinator can see which answers get read and which fail. | |
| app.post("/api/event", lookupLimiter, (req, res) => { | |
| insights.record(req.body?.type, req.body?.id); | |
| res.sendStatus(204); | |
| }); | |
| app.get("/api/stats", (req, res) => { | |
| const key = env.STATS_KEY; | |
| const given = String(req.headers.authorization || "").replace(/^Bearer /, ""); | |
| if (!key) return res.sendStatus(404); | |
| if (!given || !sameKey(given, key)) return res.status(401).json({ error: "Wrong key." }); | |
| res.json(insights.snapshot()); | |
| }); | |
| app.get("/api/health", (_req, res) => res.json({ ok: true, kbVersion: KB_VERSION })); | |
| const dist = path.resolve(here, "../../web/dist"); | |
| if (existsSync(dist)) { | |
| app.use(express.static(dist)); | |
| app.get("*", (_req, res) => res.sendFile(path.join(dist, "index.html"))); | |
| } | |
| return app; | |
| } | |