Files
yingxiangli-duanju/server.mjs
2026-07-30 08:58:38 +08:00

741 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createServer } from "node:http";
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { extname, join, normalize } from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import { AsyncLocalStorage } from "node:async_hooks";
import { createHash, randomUUID } from "node:crypto";
import {
extractVideoTaskId,
extractVideoUrl,
normalizeAssetReference,
normalizeVideoStatus,
} from "./lib/normalize.mjs";
const root = fileURLToPath(new URL(".", import.meta.url));
const publicDir = join(root, "public");
const requestContext = new AsyncLocalStorage();
loadLocalEnv();
const port = Number(process.env.PORT || 4173);
const host = process.env.HOST || "127.0.0.1";
const apiToken = process.env.KK_API_KEY || process.env.KK_VIDEO_TOKEN || "";
const mockMode = process.env.DEMO_MOCK === "1" || !apiToken;
const auditLogDir = process.env.AUDIT_LOG_DIR || join(root, "logs");
const auditHashSalt = process.env.AUDIT_HASH_SALT || "yingxiangli-demo-audit";
const mockAssets = [];
const mockVideos = new Map();
const assetReviewTasks = new Map();
const server = createServer((request, response) => {
const startedAt = Date.now();
const requestId = randomUUID();
const sessionId = safeIdentifier(request.headers["x-session-id"]);
const requestPath = safeRequestPath(request.url);
const context = { requestId, sessionId };
response.setHeader("X-Request-Id", requestId);
response.once("finish", () => {
writeAuditLog({
level: response.statusCode >= 500 ? "error" : response.statusCode >= 400 ? "warn" : "info",
event: "http.request",
requestId,
sessionId,
method: request.method,
path: requestPath,
status: response.statusCode,
durationMs: Date.now() - startedAt,
ipHash: hashAuditValue(clientIp(request)),
userAgent: String(request.headers["user-agent"] || "").slice(0, 300),
referer: sanitizeUrlForLog(request.headers.referer || ""),
});
});
requestContext.run(context, async () => {
try {
const url = new URL(request.url, `http://${request.headers.host || "localhost"}`);
if (request.method === "OPTIONS") {
response.writeHead(204, corsHeaders());
response.end();
return;
}
if (url.pathname.startsWith("/api/")) {
await handleApi(request, response, url);
return;
}
serveStatic(response, url.pathname);
} catch (error) {
writeAuditLog({
level: "error",
event: "http.error",
requestId,
sessionId,
method: request.method,
path: requestPath,
error: sanitizeLogData({ name: error.name, message: error.message, status: error.status }),
});
if (response.headersSent) response.destroy(error);
else sendJson(response, error.status || 500, { error: error.message || "服务异常" });
}
});
});
server.listen(port, host, () => {
console.log(`AI短剧数字人 Demo: http://${host === "0.0.0.0" ? "localhost" : host}:${port}`);
console.log(`运行模式: ${mockMode ? "Mock" : "Live"}`);
});
async function handleApi(request, response, url) {
if (request.method === "POST" && url.pathname === "/api/events") {
const body = await readJson(request);
const events = (Array.isArray(body.events) ? body.events : [body]).slice(0, 50);
const context = requestContext.getStore() || {};
for (const item of events) {
writeAuditLog({
level: "info",
event: "client.operation",
requestId: context.requestId,
sessionId: safeIdentifier(context.sessionId || body.sessionId || item?.sessionId),
clientId: safeIdentifier(body.clientId || item?.clientId),
operation: String(item?.operation || item?.event || "unknown").slice(0, 120),
module: String(item?.module || "").slice(0, 50),
shotId: safeIdentifier(item?.shotId),
details: sanitizeLogData(item?.details || {}),
clientTimestamp: String(item?.timestamp || "").slice(0, 40),
});
}
sendJson(response, 202, { ok: true, accepted: events.length });
return;
}
if (request.method === "GET" && url.pathname === "/api/config") {
sendJson(response, 200, {
mockMode,
assetReady: mockMode || Boolean(apiToken),
videoReady: mockMode || Boolean(apiToken),
apiVersion: "v3",
missing: !mockMode && !apiToken ? ["KK_API_KEY"] : [],
});
return;
}
if (request.method === "POST" && url.pathname === "/api/assets/review") {
const body = await readJson(request);
const imageInputs = Array.isArray(body.images)
? body.images.map((item) => typeof item === "string" ? { url: item } : item)
: [{ url: body.url }];
if (imageInputs.length < 1 || imageInputs.length > 9) {
throw new ClientError("素材图片数量必须为 19 张");
}
for (const item of imageInputs) validatePublicUrl(item.url);
if (mockMode) {
const now = new Date().toISOString();
const labels = { front: "正面", side: "侧面", back: "背面" };
const items = imageInputs.map((input, index) => {
const id = `mat_demo_${Date.now()}_${index + 1}`;
const viewLabel = labels[input.view] || `参考图${index + 1}`;
const rejected = /reject|fail/i.test(input.url);
if (!rejected) {
mockAssets.unshift({
asset_id: id,
purpose: `${body.name || "数字人形象"} · ${viewLabel}`,
type: "image",
status: "ready",
original_url: input.url,
View: input.view || "reference",
CreateTime: now,
UpdateTime: now,
});
}
return {
source_url: input.url,
view: input.view || "reference",
asset_type: "Image",
submit_review_status: rejected ? 0 : 1,
downstream_asset_id: rejected ? "" : id,
asset_url: rejected ? "" : `asset://${id}`,
error_code: rejected ? "MOCK_REVIEW_REJECTED" : "",
error_message: rejected ? `${viewLabel}图未通过素材审核Mock 驳回)` : "",
};
});
sendJson(response, 200, {
code: 200,
status: "completed",
result: { items },
mock: true,
});
return;
}
requireToken(apiToken, "KK_API_KEY");
const labels = { front: "正面", side: "侧面", back: "背面" };
const uploaded = await Promise.all(imageInputs.map(async (input, index) => {
const viewLabel = labels[input.view] || `参考图${index + 1}`;
const upstream = await callJson("https://ai-api.kkidc.com/v1/assets/upload", {
method: "POST",
headers: bearerHeaders(),
body: JSON.stringify({
url: input.url,
type: "image",
purpose: `${body.name || "数字人形象"} · ${viewLabel}`,
}),
});
if (upstream.status >= 400 || upstream.body?.success === false || !upstream.body?.data?.asset_id) {
throw new ClientError(extractUpstreamMessage(upstream.body) || `${viewLabel}图提交失败`, upstream.status || 400);
}
return {
sourceUrl: input.url,
view: input.view || "reference",
assetId: upstream.body.data.asset_id,
groupId: upstream.body.data.group_id || "",
status: String(upstream.body.data.status || "pending").toLowerCase(),
errorMessage: "",
};
}));
const taskId = `asset-review-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const task = { id: taskId, items: uploaded, createdAt: Date.now() };
assetReviewTasks.set(taskId, task);
const result = formatAssetReviewTask(task);
sendJson(response, result.status === "completed" ? 200 : 202, result);
return;
}
if (request.method === "GET" && url.pathname.startsWith("/api/assets/tasks/")) {
requireToken(apiToken, "KK_API_KEY");
const taskId = decodeURIComponent(url.pathname.split("/").pop());
const task = assetReviewTasks.get(taskId);
if (!task) throw new ClientError("素材审核任务不存在或本地服务已重启,请在素材库刷新状态", 404);
await refreshAssetReviewTask(task);
sendJson(response, 200, formatAssetReviewTask(task));
return;
}
if (request.method === "GET" && url.pathname === "/api/assets") {
if (mockMode) {
sendJson(response, 200, {
code: 0,
message: "ok",
data: { items: mockAssets, total: mockAssets.length, page: 1, page_size: mockAssets.length },
mock: true,
});
return;
}
requireToken(apiToken, "KK_API_KEY");
const upstreamUrl = new URL("https://ai-api.kkidc.com/v1/assets");
for (const key of ["page", "page_size", "purpose", "sort_order", "group_id"]) {
for (const value of url.searchParams.getAll(key)) upstreamUrl.searchParams.append(key, value);
}
const upstream = await callJson(upstreamUrl, { headers: bearerHeaders(false) });
sendJson(response, upstream.status, upstream.body, upstream.trackId);
return;
}
if (request.method === "POST" && url.pathname === "/api/videos") {
const body = await readJson(request);
const payload = buildVideoPayload(body);
if (mockMode) {
const taskId = `cgt-demo-${Date.now()}`;
mockVideos.set(taskId, { createdAt: Date.now(), request: body });
const mockResponse = {
id: taskId,
task_id: taskId,
object: "video",
model: body.model,
status: "processing",
progress: 0,
mock: true,
};
sendJson(response, 200, mockResponse);
return;
}
requireToken(apiToken, "KK_API_KEY");
const upstream = await callJson("https://ai-api.kkidc.com/sd/api/v3/contents/generations/tasks", {
method: "POST",
headers: bearerHeaders(),
body: JSON.stringify(payload),
});
sendJson(response, upstream.status, upstream.body, upstream.trackId);
return;
}
const videoFileMatch = request.method === "GET" && url.pathname.match(/^\/api\/videos\/([^/]+)\/video\.mp4$/);
if (videoFileMatch) {
await streamVideoTask(response, decodeURIComponent(videoFileMatch[1]));
return;
}
if (request.method === "GET" && url.pathname.startsWith("/api/videos/")) {
const taskId = url.pathname.split("/").pop();
if (mockMode) {
const task = mockVideos.get(taskId);
if (!task) throw new ClientError("Mock 任务不存在", 404);
const elapsed = Date.now() - task.createdAt;
const completed = elapsed > 4500;
sendJson(response, 200, {
id: taskId,
task_id: taskId,
object: "video",
model: task.request.model,
status: completed ? "completed" : "processing",
progress: completed ? 100 : Math.min(90, Math.round(elapsed / 50)),
mock: true,
});
return;
}
requireToken(apiToken, "KK_API_KEY");
const upstream = await callJson(`https://ai-api.kkidc.com/sd/api/v3/contents/generations/tasks/${encodeURIComponent(taskId)}`, {
headers: bearerHeaders(false),
});
const normalized = {
...upstream.body,
_normalized: {
taskId: extractVideoTaskId(upstream.body),
status: normalizeVideoStatus(upstream.body),
videoUrl: extractVideoUrl(upstream.body),
},
};
sendJson(response, upstream.status, normalized, upstream.trackId);
return;
}
sendJson(response, 404, { error: "接口不存在" });
}
async function streamVideoTask(response, taskId) {
if (mockMode) throw new ClientError("Mock 视频没有可下载的真实文件", 404);
requireToken(apiToken, "KK_API_KEY");
const startedAt = Date.now();
const context = requestContext.getStore() || {};
writeAuditLog({ level: "info", event: "video.proxy.start", requestId: context.requestId, sessionId: context.sessionId, taskId });
const task = await callJson(`https://ai-api.kkidc.com/sd/api/v3/contents/generations/tasks/${encodeURIComponent(taskId)}`, {
headers: bearerHeaders(false),
});
if (task.status < 200 || task.status >= 300) {
throw new ClientError(task.body?.message || "无法读取视频任务", task.status);
}
const videoUrl = extractVideoUrl(task.body);
if (!videoUrl || !/^https?:\/\//i.test(videoUrl)) throw new ClientError("视频任务尚无可下载的结果", 409);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2 * 60 * 1000);
try {
const upstream = await fetch(videoUrl, { signal: controller.signal });
if (!upstream.ok || !upstream.body) throw new ClientError("服务商视频链接已失效,请重新查询任务", upstream.status || 502);
const contentType = upstream.headers.get("content-type") || "video/mp4";
const extension = /quicktime|mov/i.test(contentType) ? "mov" : "mp4";
const safeTaskId = taskId.replace(/[^a-z0-9_-]/gi, "").slice(-48) || "video";
response.writeHead(200, {
"Content-Type": contentType,
"Content-Disposition": `inline; filename="yingxiangli-${safeTaskId}.${extension}"`,
"Cache-Control": "private, no-store",
});
await pipeline(Readable.fromWeb(upstream.body), response);
writeAuditLog({
level: "info",
event: "video.proxy.finish",
requestId: context.requestId,
sessionId: context.sessionId,
taskId,
durationMs: Date.now() - startedAt,
contentType,
contentLength: Number(upstream.headers.get("content-length") || 0) || null,
});
} catch (error) {
writeAuditLog({
level: "error",
event: "video.proxy.error",
requestId: context.requestId,
sessionId: context.sessionId,
taskId,
durationMs: Date.now() - startedAt,
error: sanitizeLogData({ name: error.name, message: error.message }),
});
throw error;
} finally {
clearTimeout(timeout);
}
}
async function callJson(url, options = {}) {
const startedAt = Date.now();
const context = requestContext.getStore() || {};
const method = options.method || "GET";
const target = sanitizeUrlForLog(String(url));
let requestBody = null;
if (typeof options.body === "string") {
try { requestBody = JSON.parse(options.body); }
catch { requestBody = { bodyLength: options.body.length }; }
}
writeAuditLog({
level: "info",
event: "upstream.start",
requestId: context.requestId,
sessionId: context.sessionId,
method,
target,
request: sanitizeLogData(requestBody),
});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 90000);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
const text = await response.text();
let body;
try {
body = text ? JSON.parse(text) : {};
} catch {
body = { error: "上游返回了非 JSON 内容", detail: text.slice(0, 500) };
}
const result = {
status: response.status,
body,
trackId: response.headers.get("x-track-id") || "",
};
writeAuditLog({
level: response.ok ? "info" : "warn",
event: "upstream.finish",
requestId: context.requestId,
sessionId: context.sessionId,
method,
target,
status: response.status,
durationMs: Date.now() - startedAt,
trackId: result.trackId,
response: sanitizeLogData(body),
});
return result;
} catch (error) {
writeAuditLog({
level: "error",
event: "upstream.error",
requestId: context.requestId,
sessionId: context.sessionId,
method,
target,
durationMs: Date.now() - startedAt,
error: sanitizeLogData({ name: error.name, message: error.message }),
});
throw error;
} finally {
clearTimeout(timeout);
}
}
function serveStatic(response, pathname) {
const requested = pathname === "/" ? "index.html" : pathname.replace(/^\//, "");
const safePath = normalize(requested).replace(/^(\.\.(\/|\\|$))+/, "");
const filePath = join(publicDir, safePath);
if (!filePath.startsWith(publicDir) || !existsSync(filePath)) {
sendJson(response, 404, { error: "页面不存在" });
return;
}
const mime = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".svg": "image/svg+xml",
}[extname(filePath)] || "application/octet-stream";
response.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-store" });
response.end(readFileSync(filePath));
}
function loadLocalEnv() {
const envPath = join(root, ".env.local");
if (!existsSync(envPath)) return;
const lines = readFileSync(envPath, "utf8").split(/\r?\n/);
for (const line of lines) {
const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
if (!match || process.env[match[1]]) continue;
process.env[match[1]] = match[2].replace(/^['"]|['"]$/g, "");
}
}
async function readJson(request) {
const chunks = [];
let size = 0;
for await (const chunk of request) {
size += chunk.length;
if (size > 1_000_000) throw new ClientError("请求体过大", 413);
chunks.push(chunk);
}
try {
return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
} catch {
throw new ClientError("请求体不是有效 JSON");
}
}
function validatePublicUrl(value) {
let url;
try {
url = new URL(value);
} catch {
throw new ClientError("请输入公网可访问的图片 URL");
}
if (!["http:", "https:"].includes(url.protocol)) throw new ClientError("素材仅支持 http/https URL");
if (!/\.(jpe?g|png|webp|bmp|tiff?|gif|heic|heif)(\?.*)?$/i.test(url.href)) {
throw new ClientError("URL 必须带受支持的图片扩展名,例如 .png 或 .jpg");
}
}
function requireToken(value, name) {
if (!value) throw new ClientError(`服务端缺少 ${name} 配置`, 503);
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function resolveResolution(model = "", requested = "720p") {
return requested === "480p" ? "480p" : "720p";
}
function buildVideoPayload(body) {
const assetReferences = Array.isArray(body.assetReferences)
? body.assetReferences
: [body.assetReference].filter(Boolean);
const extraImageReferences = Array.isArray(body.extraImageReferences) ? body.extraImageReferences : [];
const videoReferences = Array.isArray(body.videoReferences) ? body.videoReferences : [];
const imageReferences = [...assetReferences, ...extraImageReferences];
if (assetReferences.length < 1 || assetReferences.some((item) => !/^asset:\/\/(?:mat_|asset-)/i.test(item))) {
throw new ClientError("请先选择审核通过的人物素材asset://mat_...");
}
if (imageReferences.length > 9) {
throw new ClientError(`人物图片与其他参考图片合计最多 9 张,当前为 ${imageReferences.length}`);
}
if (extraImageReferences.some((item) => !isPublicMediaUrl(item, "image"))) {
throw new ClientError("其他参考图片必须是公网可访问的 http/https 图片 URL");
}
if (videoReferences.length > 3 || videoReferences.some((item) => !isPublicMediaUrl(item, "video"))) {
throw new ClientError("参考视频最多 3 条,且必须是公网可访问的 MP4/MOV URL");
}
if (!String(body.prompt || "").trim()) throw new ClientError("请填写分镜提示词");
return {
model: resolveVideoModel(body.model),
content: [
{ type: "text", text: body.prompt },
...imageReferences.map((reference) => ({
type: "image_url",
image_url: { url: reference },
role: "reference_image",
})),
...videoReferences.map((reference) => ({
type: "video_url",
video_url: { url: reference },
role: "reference_video",
})),
],
duration: clamp(Number(body.duration || 5), 4, 15),
resolution: resolveResolution(body.model, body.resolution),
ratio: body.ratio || "9:16",
generate_audio: body.generateAudio !== false,
watermark: Boolean(body.watermark),
};
}
function isPublicMediaUrl(value, kind) {
try {
const url = new URL(value);
if (!["http:", "https:"].includes(url.protocol)) return false;
const pattern = kind === "video" ? /\.(mp4|mov)(\?.*)?$/i : /\.(jpe?g|png|webp|bmp|tiff?|gif|heic|heif)(\?.*)?$/i;
return pattern.test(url.href);
} catch {
return false;
}
}
function resolveVideoModel(value = "") {
const aliases = {
"seed-2": "doubao-seedance-2-0-260128",
"seed-2-fast": "doubao-seedance-2-0-fast-260128",
"seed-2-mini": "doubao-seedance-2-0-mini-260615",
};
if (aliases[value]) return aliases[value];
if (/^doubao-seedance-2-0-(?:fast-260128|mini-260615|260128)$/.test(value)) return value;
return "doubao-seedance-2-0-260128";
}
function bearerHeaders(includeContentType = true) {
return {
Authorization: `Bearer ${apiToken}`,
...(includeContentType ? { "Content-Type": "application/json" } : {}),
};
}
function extractUpstreamMessage(body) {
if (!body) return "";
if (typeof body === "string") return body;
return body.error?.message || body.error || body.message || body.detail || "";
}
async function refreshAssetReviewTask(task) {
await Promise.all(task.items.map(async (item) => {
if (["ready", "failed"].includes(item.status)) return;
const upstreamUrl = new URL(`https://ai-api.kkidc.com/v1/assets/${encodeURIComponent(item.assetId)}`);
if (item.groupId) upstreamUrl.searchParams.set("group_id", item.groupId);
const upstream = await callJson(upstreamUrl, { headers: bearerHeaders(false) });
if (upstream.status >= 400 || upstream.body?.success === false) {
item.status = "failed";
item.errorMessage = extractUpstreamMessage(upstream.body) || "素材状态查询失败";
return;
}
item.status = String(upstream.body?.data?.status || item.status || "pending").toLowerCase();
item.errorMessage = upstream.body?.data?.error_message || upstream.body?.message || "";
}));
}
function formatAssetReviewTask(task) {
const allFinal = task.items.every((item) => ["ready", "failed"].includes(item.status));
return {
code: allFinal ? 200 : 202,
status: allFinal ? "completed" : "processing",
task_id: task.id,
result: {
items: task.items.map((item) => ({
source_url: item.sourceUrl,
view: item.view,
asset_type: "Image",
submit_review_status: item.status === "ready" ? 1 : item.status === "failed" ? 0 : null,
downstream_asset_id: item.assetId,
asset_url: item.status === "ready" ? `asset://${item.assetId}` : "",
group_id: item.groupId,
status: item.status,
error_code: item.status === "failed" ? "ASSET_REVIEW_FAILED" : "",
error_message: item.errorMessage,
})),
},
};
}
function writeAuditLog(entry) {
try {
mkdirSync(auditLogDir, { recursive: true, mode: 0o700 });
const day = new Date().toISOString().slice(0, 10);
const filePath = join(auditLogDir, `audit-${day}-${process.pid}.jsonl`);
const record = sanitizeLogData({
timestamp: new Date().toISOString(),
pid: process.pid,
...entry,
});
appendFileSync(filePath, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
} catch (error) {
console.error("审计日志写入失败", error.message);
}
}
function sanitizeLogData(value, key = "", depth = 0) {
if (value == null || typeof value === "number" || typeof value === "boolean") return value;
if (depth > 7) return "[MAX_DEPTH]";
if (/authorization|api.?key|token|secret|password|cookie/i.test(key)) return "[REDACTED]";
if (typeof value === "string") {
if (/^bearer\s+/i.test(value)) return "[REDACTED]";
if (/prompt|(^|_)text$/i.test(key)) return summarizeText(value);
if (/url|referer/i.test(key) || /^https?:\/\//i.test(value)) return sanitizeUrlForLog(value);
return value.length > 2000 ? `${value.slice(0, 2000)}…[TRUNCATED:${value.length}]` : value;
}
if (Array.isArray(value)) return value.slice(0, 50).map((item) => sanitizeLogData(item, key, depth + 1));
if (typeof value === "object") {
const result = {};
for (const [childKey, childValue] of Object.entries(value).slice(0, 100)) {
result[childKey] = sanitizeLogData(childValue, childKey, depth + 1);
}
return result;
}
return String(value);
}
function summarizeText(value) {
const text = String(value || "");
return { length: text.length, sha256: hashAuditValue(text).slice(0, 16) };
}
function sanitizeUrlForLog(value) {
if (!value) return "";
try {
const url = new URL(String(value));
return {
protocol: url.protocol,
host: url.host,
pathname: url.pathname.slice(0, 500),
queryKeys: [...new Set([...url.searchParams.keys()])].slice(0, 30),
hasQuery: Boolean(url.search),
};
} catch {
return String(value).slice(0, 500);
}
}
function safeRequestPath(value) {
try {
const url = new URL(value, "http://localhost");
const keys = [...new Set([...url.searchParams.keys()])].slice(0, 30);
return `${url.pathname}${keys.length ? `?${keys.join("&")}` : ""}`;
} catch {
return String(value || "").slice(0, 500);
}
}
function safeIdentifier(value) {
return String(value || "").replace(/[^a-z0-9_.:-]/gi, "").slice(0, 180);
}
function clientIp(request) {
const forwarded = String(request.headers["x-forwarded-for"] || "").split(",")[0].trim();
return forwarded || request.socket.remoteAddress || "unknown";
}
function hashAuditValue(value) {
return createHash("sha256").update(`${auditHashSalt}:${String(value || "")}`).digest("hex");
}
function sendJson(response, status, body, trackId = "") {
response.writeHead(status, {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
...corsHeaders(),
...(trackId ? { "X-Track-Id": trackId } : {}),
});
response.end(JSON.stringify(body));
}
function corsHeaders() {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, X-Session-Id",
"Access-Control-Expose-Headers": "X-Request-Id",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Private-Network": "true",
};
}
class ClientError extends Error {
constructor(message, status = 400) {
super(message);
this.status = status;
}
}
process.on("uncaughtException", (error) => {
writeAuditLog({ level: "fatal", event: "process.uncaught_exception", error: sanitizeLogData({ name: error.name, message: error.message, stack: error.stack }) });
console.error(error);
});
process.on("unhandledRejection", (error) => {
writeAuditLog({ level: "error", event: "process.unhandled_rejection", error: sanitizeLogData({ message: String(error), stack: error?.stack }) });
console.error(error);
});
export { normalizeAssetReference };