Skip to content

Commit 57b6187

Browse files
authored
Split server concerns into focused modules (#5)
Co-authored-by: Jamie Parr <JamieP-205@users.noreply.github.com>
1 parent 6936986 commit 57b6187

9 files changed

Lines changed: 536 additions & 373 deletions

File tree

v2/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ Manual CLI production deploys are reserved for recovery or a controlled migratio
104104

105105
- `app.js` - public chat, multi-chat controls, admin drafting and conversation management
106106
- `netlify/functions/_context.js` - pack validation, audience filtering and ranking
107-
- `netlify/functions/_lib.js` - authentication, providers, Blobs, migration and API routes
107+
- `netlify/functions/_lib.js` - API route coordination, Blobs access and migration
108+
- `netlify/functions/_http.js` and `_security.js` - request validation,
109+
cookie parsing and password primitives
110+
- `netlify/functions/_memory.js`, `_provider.js` and `_prompts.js` -
111+
visitor memory, provider adapters and persona policy
108112
- `tools/build-private-context.js` - local private-pack builder
109113
- `test/` - authentication, context, privacy and deployment tests
110114

v2/netlify/functions/_http.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
class HttpError extends Error {
2+
constructor(statusCode, message) {
3+
super(message);
4+
this.name = "HttpError";
5+
this.statusCode = statusCode;
6+
}
7+
}
8+
9+
function json(statusCode, body, extraHeaders = {}) {
10+
return {
11+
statusCode,
12+
headers: {
13+
"Content-Type": "application/json; charset=utf-8",
14+
"Cache-Control": "no-store, max-age=0",
15+
"X-Content-Type-Options": "nosniff",
16+
...extraHeaders
17+
},
18+
body: JSON.stringify(body)
19+
};
20+
}
21+
22+
function parseBody(event, maxBytes = 64_000) {
23+
const raw = String(event.body || "");
24+
if (Buffer.byteLength(raw, "utf8") > maxBytes) {
25+
throw new HttpError(413, "Request is too large.");
26+
}
27+
try {
28+
return raw ? JSON.parse(raw) : {};
29+
} catch {
30+
throw new HttpError(400, "Request body must be valid JSON.");
31+
}
32+
}
33+
34+
function requireMethod(event, method) {
35+
if (event.httpMethod !== method) {
36+
throw new HttpError(405, `Use ${method} for this endpoint.`);
37+
}
38+
}
39+
40+
function sameOriginOk(event) {
41+
const origin = event.headers.origin || event.headers.Origin;
42+
if (!origin) return true;
43+
try {
44+
return new URL(origin).host === (event.headers.host || event.headers.Host);
45+
} catch {
46+
return false;
47+
}
48+
}
49+
50+
function requireWriteRequest(event) {
51+
requireMethod(event, "POST");
52+
if (!sameOriginOk(event)) {
53+
throw new HttpError(403, "Origin check failed.");
54+
}
55+
}
56+
57+
function cleanText(value, max = 1_000) {
58+
return String(value ?? "")
59+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")
60+
.trim()
61+
.slice(0, max);
62+
}
63+
64+
function normaliseUsername(value) {
65+
return cleanText(value, 24).toLowerCase();
66+
}
67+
68+
function validUsername(value) {
69+
return /^[a-z0-9_]{3,24}$/.test(value)
70+
&& value !== "jamie"
71+
&& !value.startsWith("guest_");
72+
}
73+
74+
function parseCookies(header) {
75+
const cookies = {};
76+
for (const part of String(header || "").split(";")) {
77+
const index = part.indexOf("=");
78+
if (index < 1) continue;
79+
const name = part.slice(0, index).trim();
80+
if (!name) continue;
81+
const rawValue = part.slice(index + 1).trim();
82+
try {
83+
cookies[name] = decodeURIComponent(rawValue);
84+
} catch {
85+
// A malformed cookie should invalidate that value, not crash the API.
86+
}
87+
}
88+
return cookies;
89+
}
90+
91+
module.exports = {
92+
HttpError,
93+
cleanText,
94+
json,
95+
normaliseUsername,
96+
parseBody,
97+
parseCookies,
98+
requireMethod,
99+
requireWriteRequest,
100+
sameOriginOk,
101+
validUsername
102+
};
103+

0 commit comments

Comments
 (0)