th3w1zard1's picture
Deploy trask-http from community-bots@1522b1f5c5e8f27536c525d3d1b71f5175563359
ff234fa verified
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadTraskHttpServerConfig } from "@openkotor/config";
import { createLogger } from "@openkotor/core";
import { JsonTraskQueryRepository, resolveDataFile } from "@openkotor/persistence";
import {
buildBrowserCorsAllowedOrigins,
createNodeApiHost,
resolveCorsHeaders,
} from "@openkotor/platform";
import { createChunkSearchProvider } from "@openkotor/retrieval";
import { createResearchWizardClient, setTraskResearchLogSink } from "@openkotor/trask";
import { createTraskHttpRouter, type TraskHttpAuth } from "@openkotor/trask-http";
import express, { type Request, type Response } from "express";
const logger = createLogger("trask-http-server");
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..", "..", "..");
const extractBearerToken = (authorization: string | undefined): string | null => {
if (!authorization || !authorization.startsWith("Bearer ")) return null;
return authorization.slice("Bearer ".length).trim() || null;
};
const createWebAuth = (
config: ReturnType<typeof loadTraskHttpServerConfig>,
): TraskHttpAuth<{ id: string; persistQueries?: boolean }> => ({
requireAuth: (handler) => async (req: Request, res: Response) => {
if (config.webApiKey) {
const bearer = extractBearerToken(req.headers.authorization);
const headerKey =
typeof req.headers["x-trask-api-key"] === "string" ? req.headers["x-trask-api-key"].trim() : undefined;
const ok = bearer === config.webApiKey || headerKey === config.webApiKey;
if (!ok) {
res.status(401).json({ error: "Invalid or missing API key." });
return;
}
await handler(req, res, { id: config.webDefaultUserId, persistQueries: true });
return;
}
if (config.webAllowAnonymous) {
// Holocron polls GET /thread after 202 /ask; anonymous must persist queries or research never completes in-browser.
await handler(req, res, { id: config.webDefaultUserId, persistQueries: true });
return;
}
res.status(401).json({
error: "Set TRASK_WEB_API_KEY or TRASK_WEB_ALLOW_ANONYMOUS=1 for local development.",
});
},
});
const config = loadTraskHttpServerConfig();
// Resolve relative data paths from repo root so they work regardless of which
// directory pnpm launches the process from (e.g. apps/trask-http-server/ vs repo root).
const resolveFromRoot = (p: string) => (path.isAbsolute(p) ? p : path.resolve(repoRoot, p));
const queryRepository = new JsonTraskQueryRepository(resolveDataFile(resolveFromRoot(config.dataDir), "trask-queries.json"));
/** Legacy FileChunkStore queue surface only; Holocron compose uses Chroma retrieve (`TRASK_INDEXER_BASE_URL`). */
const searchProvider = createChunkSearchProvider(resolveFromRoot(config.chunkDir));
const webResearch = createResearchWizardClient(config.researchWizard, config.ai);
const researchLogVerbose = (process.env.TRASK_RESEARCH_LOG_VERBOSE ?? "").trim().toLowerCase();
if (researchLogVerbose === "1" || researchLogVerbose === "true" || researchLogVerbose === "yes") {
setTraskResearchLogSink((line, level) => {
if (level === "debug") {
logger.debug(line);
} else {
logger.info(line);
}
});
}
const runtime = {
searchProvider,
webResearch,
queryRepository,
};
const app = express();
/** In-memory Spark KV shim so qa-webui static builds stop hammering 404 on `/__spark-kv/*`. */
const sparkKvStore = new Map<string, string>();
const readSparkKvBody = (req: Request): Promise<string> =>
new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
req.on("error", reject);
});
app.use((req, res, next) => {
const pathname = (req.path.split("?")[0] ?? "").replace(/\/+$/, "") || "/";
if (!pathname.startsWith("/__spark-kv")) {
next();
return;
}
let subpath = pathname.slice("/__spark-kv".length);
if (subpath.startsWith("/")) {
subpath = subpath.slice(1);
}
const key = subpath ? decodeURIComponent(subpath.split("/")[0]!) : "";
void (async () => {
try {
if (req.method === "GET" && !key) {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify([...sparkKvStore.keys()]));
return;
}
if (req.method === "GET" && key) {
const value = sparkKvStore.get(key);
if (value === undefined) {
res.status(404).end();
return;
}
res.setHeader("Content-Type", "text/plain");
res.end(value);
return;
}
if (req.method === "POST" && key) {
const body = await readSparkKvBody(req);
sparkKvStore.set(key, body);
res.status(200).end();
return;
}
if (req.method === "DELETE" && key) {
sparkKvStore.delete(key);
res.status(204).end();
return;
}
res.status(405).end();
} catch {
res.status(500).end();
}
})();
});
app.use(express.json());
const allowedCorsOrigins = buildBrowserCorsAllowedOrigins({
publicWebOrigin: config.publicWebOrigin,
localPorts: [5174, 5173, 4174, 4173, 3000],
});
app.use((req, res, next) => {
const cors = resolveCorsHeaders({ method: req.method, origin: req.headers.origin }, allowedCorsOrigins, {
allowHeaders: "Content-Type,Authorization,X-Trask-Api-Key",
});
for (const [name, value] of Object.entries(cors.headers)) {
res.setHeader(name, value);
}
if (cors.isPreflight) {
res.sendStatus(204);
return;
}
next();
});
app.use(
"/api/trask",
createTraskHttpRouter({
runtime,
auth: createWebAuth(config),
}),
);
app.get("/health", (_req, res) => {
res.status(200).json({ ok: true, service: "trask-http-server" });
});
const distFromEnv = process.env.TRASK_WEBUI_DIST_PATH?.trim();
const defaultDist = path.join(repoRoot, "apps", "holocron-web", "dist");
const webUiDist = distFromEnv ? path.resolve(distFromEnv) : defaultDist;
if (existsSync(webUiDist)) {
app.use(express.static(webUiDist));
app.use((req, res, next) => {
if (req.method !== "GET" && req.method !== "HEAD") return next();
if (req.path.startsWith("/api") || req.path.startsWith("/__spark-kv")) return next();
res.sendFile(path.join(webUiDist, "index.html"));
});
logger.info(`Serving Holocron web static files from ${webUiDist}`);
} else {
logger.warn(`Holocron web dist not found at ${webUiDist}; API-only mode (TRASK_WEBUI_DIST_PATH to override).`);
app.get("/", (_req, res) => {
res
.status(200)
.type("text/plain")
.send("Trask HTTP API is running (API-only). Holocron static UI was not bundled; use /api/trask and /health.");
});
}
const { server, listen } = createNodeApiHost({
requestListener: app,
createHub: () => ({}),
});
listen(config.port, () => {
logger.info(`Trask HTTP API listening on port ${config.port}`);
});
process.on("SIGINT", () => {
server.close(() => process.exit(0));
});