136 lines
7.6 KiB
JavaScript
136 lines
7.6 KiB
JavaScript
// Synthetic Core only. The application and its production proxy run unchanged
|
|
// in a disposable Next dev copy; no real accounts, .env files or OAuth services.
|
|
import { createServer } from "node:http";
|
|
import { createHash, randomBytes } from "node:crypto";
|
|
import { cp, mkdtemp, readFile, rm, symlink } from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawn } from "node:child_process";
|
|
|
|
const root = fileURLToPath(new URL("../../", import.meta.url));
|
|
const origin = "http://localhost:17323";
|
|
const snapshot = JSON.parse(await readFile(join(root, "src/data/papers.json"), "utf8"));
|
|
const sessions = new Set(), grants = new Map();
|
|
let meFailure = false, logoutFailure = false, verified = false;
|
|
let counts = {};
|
|
let paperStatus = 0, paperMalformed = false, paperDelay = 0;
|
|
function reset() {
|
|
sessions.clear(); grants.clear(); meFailure = false; logoutFailure = false; verified = false;
|
|
paperStatus = 0; paperMalformed = false; paperDelay = 0;
|
|
counts = { exchange: 0, verified: 0, logout: 0, google: 0, slack: 0 };
|
|
}
|
|
reset();
|
|
const token = () => randomBytes(32).toString("hex");
|
|
function reply(res, status, data) {
|
|
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
res.end(status === 204 ? undefined : JSON.stringify(data));
|
|
}
|
|
function session(res) {
|
|
const value = token(); sessions.add(value);
|
|
reply(res, 200, { access_token: value, expires_in: 2592000 });
|
|
}
|
|
async function body(req) {
|
|
const parts = [];
|
|
for await (const part of req) parts.push(part);
|
|
return JSON.parse(Buffer.concat(parts).toString() || "{}");
|
|
}
|
|
const server = createServer(async (req, res) => {
|
|
try {
|
|
const url = new URL(req.url, "http://localhost:17324");
|
|
if (url.pathname === "/__test/reset" && req.method === "POST") { reset(); return reply(res, 200, {}); }
|
|
if (url.pathname === "/__test/state") return reply(res, 200, { ...counts, sessions: sessions.size });
|
|
if (url.pathname === "/__test/control" && req.method === "POST") {
|
|
const data = await body(req);
|
|
if (data.expire) sessions.clear();
|
|
if (typeof data.meFailure === "boolean") meFailure = data.meFailure;
|
|
if (typeof data.logoutFailure === "boolean") logoutFailure = data.logoutFailure;
|
|
if ([0, 400, 429, 503, 504].includes(data.paperStatus)) paperStatus = data.paperStatus;
|
|
if (typeof data.paperMalformed === "boolean") paperMalformed = data.paperMalformed;
|
|
if (typeof data.paperDelay === "number") paperDelay = Math.min(5000, Math.max(0,data.paperDelay));
|
|
return reply(res, 200, {});
|
|
}
|
|
if (url.pathname.startsWith("/projects/thesisly/research/papers/")) {
|
|
const paper = snapshot.papers.find(p => p.id === url.pathname.split("/").at(-1));
|
|
return reply(res, paper ? 200 : 404, paper || { error: "not_found" });
|
|
}
|
|
if (url.pathname === "/projects/thesisly/research/papers") {
|
|
if (paperDelay) await new Promise(resolve => setTimeout(resolve, paperDelay));
|
|
if (paperStatus) return reply(res, paperStatus, { error: "synthetic research failure" });
|
|
if (paperMalformed) return reply(res, 200, { unexpected: true });
|
|
const pageSize = Number(url.searchParams.get("pageSize") || 8);
|
|
// Fixture transport data, not an implementation of Core search semantics.
|
|
return reply(res, 200, { items: snapshot.papers.slice(0, pageSize), nextCursor: "", previousCursor: "", pageSize, topics: [...new Set(snapshot.papers.flatMap(p => p.topics))] });
|
|
}
|
|
const match = /^\/projects\/thesisly\/auth\/([a-z-]+)$/.exec(url.pathname);
|
|
if (!match) return reply(res, 404, { error: "not_found" });
|
|
const action = match[1];
|
|
if (["google", "slack"].includes(action) && req.method === "GET") {
|
|
counts[action]++;
|
|
const challenge = url.searchParams.get("challenge");
|
|
if (!/^[-_a-zA-Z0-9]{43}$/.test(challenge || "")) return reply(res, 400, { error: "invalid_request" });
|
|
const code = token(); grants.set(code, challenge);
|
|
res.writeHead(302, { Location: `${origin}/auth/callback#code=${code}` }); return res.end();
|
|
}
|
|
if (action === "exchange" && req.method === "POST") {
|
|
counts.exchange++;
|
|
const code = req.headers["x-foundry-code"], verifier = req.headers["x-foundry-verifier"];
|
|
const expected = grants.get(code);
|
|
if (!expected || typeof verifier !== "string" || createHash("sha256").update(verifier).digest("base64url") !== expected) return reply(res, 400, { error: "invalid_verification" });
|
|
grants.delete(code); return session(res);
|
|
}
|
|
if (action === "register" && req.method === "POST") return reply(res, 202, { status: "verification_required" });
|
|
if (action === "verify-email" && req.method === "POST") {
|
|
counts.verified++;
|
|
const data = await body(req);
|
|
if (verified || data.token !== "fixture-email-verification") return reply(res, 400, { error: "invalid_verification" });
|
|
verified = true; return session(res);
|
|
}
|
|
if (action === "login" && req.method === "POST") {
|
|
const data = await body(req);
|
|
if (data.email !== "reader@example.test" || data.password !== "fixture-password-123") return reply(res, 401, { error: "invalid_credentials" });
|
|
return session(res);
|
|
}
|
|
const bearer = (req.headers.authorization || "").replace(/^Bearer /, "");
|
|
if (!sessions.has(bearer)) return reply(res, 401, { error: "invalid_session" });
|
|
if (action === "me" && req.method === "GET") {
|
|
if (meFailure) return reply(res, 503, { error: "temporarily_unavailable" });
|
|
res.setHeader("X-Foundry-Session-Expires-In", "2592000");
|
|
return reply(res, 200, { id: "fixture-account", email: "reader@example.test", project: "thesisly" });
|
|
}
|
|
if (action === "logout" && req.method === "POST") {
|
|
if (logoutFailure) return reply(res, 503, { error: "temporarily_unavailable" });
|
|
sessions.delete(bearer); counts.logout++; return reply(res, 204);
|
|
}
|
|
return reply(res, 405, { error: "method_not_allowed" });
|
|
} catch { reply(res, 500, { error: "fixture_error" }); }
|
|
});
|
|
|
|
const directory = await mkdtemp(join(root, ".session-test-"));
|
|
let child;
|
|
let closing = false;
|
|
async function close(code = 0) {
|
|
if (closing) return;
|
|
closing = true;
|
|
if (child && child.exitCode === null) {
|
|
child.kill("SIGTERM");
|
|
await new Promise(resolve => { child.once("exit", resolve); setTimeout(() => { child.kill("SIGKILL"); resolve(); }, 5000).unref(); });
|
|
}
|
|
server.closeAllConnections();
|
|
await new Promise(resolve => server.close(resolve));
|
|
await rm(directory, { recursive: true, force: true });
|
|
process.exit(code);
|
|
}
|
|
process.on("SIGTERM", () => void close());
|
|
process.on("SIGINT", () => void close());
|
|
try {
|
|
for (const name of ["src", "package.json", "tsconfig.json", "postcss.config.mjs", "next.config.ts", "next-env.d.ts"]) await cp(join(root, name), join(directory, name), { recursive: true });
|
|
await symlink(join(root, "node_modules"), join(directory, "node_modules"), "dir");
|
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(17324, "localhost", resolve); });
|
|
const require = createRequire(import.meta.url);
|
|
child = spawn(process.execPath, [require.resolve("next/dist/bin/next"), "dev", "--hostname", "localhost", "--port", "17323"], {
|
|
cwd: directory, stdio: "inherit", env: { ...process.env, FOUNDRY_PROJECT: "thesisly", FOUNDRY_API_ORIGIN: "http://localhost:17324", APP_ORIGIN: origin, PAPER_API_ORIGIN: process.env.THESISLY_PAPER_MODE === "sample" ? "" : "http://localhost:17324/projects/thesisly/research/", NEXT_TELEMETRY_DISABLED: "1" },
|
|
});
|
|
child.once("exit", code => { if (!closing) void close(code || 1); });
|
|
} catch (error) { console.error(error.message); await close(1); }
|