103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
|
|
import { decorateErrorPayloadForHttp, logErrorEnvelope } from "./error-envelope.ts";
|
|
|
|
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
|
|
export function readBody(request, limitBytes = DEFAULT_BODY_LIMIT_BYTES) {
|
|
const limit = Number.isInteger(limitBytes) && limitBytes > 0 ? limitBytes : DEFAULT_BODY_LIMIT_BYTES;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let body = "";
|
|
request.setEncoding("utf8");
|
|
|
|
request.on("data", (chunk) => {
|
|
body += chunk;
|
|
if (Buffer.byteLength(body, "utf8") > limit) {
|
|
request.destroy(new Error(`request body exceeds ${limit} bytes`));
|
|
}
|
|
});
|
|
|
|
request.on("end", () => resolve(body));
|
|
request.on("error", reject);
|
|
});
|
|
}
|
|
|
|
export function sendJson(response, statusCode, body) {
|
|
if (response.headersSent || response.writableEnded) return false;
|
|
const payload = decorateErrorPayloadForHttp(body, { response, statusCode });
|
|
logErrorEnvelope(payload, { statusCode });
|
|
const headers: Record<string, string> = {
|
|
"content-type": "application/json; charset=utf-8",
|
|
"cache-control": "no-store"
|
|
};
|
|
const retryAfterSeconds = retryAfterHeaderSeconds(payload);
|
|
if (retryAfterSeconds !== null) headers["retry-after"] = String(retryAfterSeconds);
|
|
response.writeHead(statusCode, headers);
|
|
response.end(JSON.stringify(payload) + "\n");
|
|
return true;
|
|
}
|
|
|
|
function retryAfterHeaderSeconds(payload) {
|
|
const retryAfterMs = Number(payload?.error?.retryAfterMs ?? payload?.retryAfterMs);
|
|
if (!Number.isFinite(retryAfterMs) || retryAfterMs <= 0) return null;
|
|
return Math.max(1, Math.ceil(retryAfterMs / 1000));
|
|
}
|
|
|
|
export function sendRedirect(response, location, body = {}) {
|
|
response.writeHead(302, {
|
|
"content-type": "application/json; charset=utf-8",
|
|
"cache-control": "no-store",
|
|
location
|
|
});
|
|
response.end(JSON.stringify({ redirectTo: location, ...body }) + "\n");
|
|
}
|
|
|
|
export function getHeader(request, name) {
|
|
const value = request.headers[name.toLowerCase()];
|
|
if (Array.isArray(value)) return value[0];
|
|
return value;
|
|
}
|
|
|
|
export function firstHeaderValue(request, name) {
|
|
return getHeader(request, name);
|
|
}
|
|
|
|
export function truthyFlag(value) {
|
|
return /^(?:1|true|yes|on|enabled)$/iu.test(String(value ?? "").trim());
|
|
}
|
|
|
|
export function safeTraceId(value) {
|
|
const text = String(value ?? "").trim();
|
|
return /^trc_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
|
|
}
|
|
|
|
export function safeSessionId(value) {
|
|
const text = String(value ?? "").trim();
|
|
return /^ses_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
|
|
}
|
|
|
|
export function safeConversationId(value) {
|
|
const text = String(value ?? "").trim();
|
|
return /^cnv_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
|
|
}
|
|
|
|
export function safeOpaqueId(value) {
|
|
const text = String(value ?? "").trim();
|
|
return /^[A-Za-z0-9_.:-]{3,180}$/u.test(text) ? text : null;
|
|
}
|
|
|
|
export function parsePositiveInteger(value, fallback) {
|
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
export function positiveInteger(value, fallback) {
|
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
export function text(value) {
|
|
if (value === null || value === undefined) return "";
|
|
return String(value);
|
|
}
|