carbon-tokenization / backend /src /create-app.ts
tfrere's picture
tfrere HF Staff
feat(storage): first-class data - no silent failures in the persistence pipeline
7a42df5
import express from "express";
import { createServer } from "http";
import { WebSocketServer } from "ws";
import { Hocuspocus } from "@hocuspocus/server";
import { Database } from "@hocuspocus/extension-database";
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
import { join } from "path";
import * as Y from "yjs";
import { extractToken, isOAuthEnabled } from "./auth.js";
import { isHfStorageEnabled, setUserToken, pullDocument } from "./hf-storage.js";
import { citationsRouter } from "./citations.js";
import { getDataDir, docPath, sanitizeName } from "./utils.js";
import { debouncedSave, ensurePublishedRestored } from "./persistence.js";
import { createAuthRouter, createRequireEditor } from "./routes/auth.js";
import { createChatRouter } from "./routes/chat.js";
import { createPublishRouter } from "./routes/publish.js";
import { createUploadRouter } from "./routes/upload.js";
import { createDatasetProxyRouter } from "./routes/dataset-proxy.js";
import { createStorageRouter } from "./routes/storage.js";
export { debouncedSave, resetSaveTimers, resetPublishedRestored } from "./persistence.js";
const DEFAULT_DOC_NAME = "default";
/**
* Full inline SVG of the official Hugging Face brand logo (smiling face
* with both hugging hands). Drawn from
* https://huggingface.co/front/assets/huggingface_logo-noborder.svg
* so the page has zero network dependencies.
*/
const HF_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 88" width="22" height="20" aria-hidden="true" focusable="false">
<path fill="#FFD21E" d="M47.21 76.5a34.75 34.75 0 1 0 0-69.5 34.75 34.75 0 0 0 0 69.5Z"/>
<path fill="#FF9D0B" d="M81.96 41.75a34.75 34.75 0 1 0-69.5 0 34.75 34.75 0 0 0 69.5 0Zm-73.5 0a38.75 38.75 0 1 1 77.5 0 38.75 38.75 0 0 1-77.5 0Z"/>
<path fill="#3A3B45" d="M58.5 32.3c1.28.44 1.78 3.06 3.07 2.38a5 5 0 1 0-6.76-2.07c.61 1.15 2.55-.72 3.7-.32ZM34.95 32.3c-1.28.44-1.79 3.06-3.07 2.38a5 5 0 1 1 6.76-2.07c-.61 1.15-2.56-.72-3.7-.32Z"/>
<path fill="#FF323D" d="M46.96 56.29c9.83 0 13-8.76 13-13.26 0-2.34-1.57-1.6-4.09-.36-2.33 1.15-5.46 2.74-8.9 2.74-7.19 0-13-6.88-13-2.38s3.16 13.26 13 13.26Z"/>
<path fill="#3A3B45" fill-rule="evenodd" d="M39.43 54a8.7 8.7 0 0 1 5.3-4.49c.4-.12.81.57 1.24 1.28.4.68.82 1.37 1.24 1.37.45 0 .9-.68 1.33-1.35.45-.7.89-1.38 1.32-1.25a8.61 8.61 0 0 1 5 4.17c3.73-2.94 5.1-7.74 5.1-10.7 0-2.34-1.57-1.6-4.09-.36l-.14.07c-2.31 1.15-5.39 2.67-8.77 2.67s-6.45-1.52-8.77-2.67c-2.6-1.29-4.23-2.1-4.23.29 0 3.05 1.46 8.06 5.47 10.97Z" clip-rule="evenodd"/>
<path fill="#FF9D0B" d="M70.71 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM24.21 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5Z"/>
<path fill="#FF9D0B" d="M17.52 48c-1.62 0-3.06.66-4.07 1.87a5.97 5.97 0 0 0-1.33 3.76 7.1 7.1 0 0 0-1.94-.3c-1.55 0-2.95.59-3.94 1.66a5.8 5.8 0 0 0-.8 7 5.3 5.3 0 0 0-1.79 2.82c-.24.9-.48 2.8.8 4.74a5.22 5.22 0 0 0-.37 5.02c1.02 2.32 3.57 4.14 8.52 6.1 3.07 1.22 5.89 2 5.91 2.01a44.33 44.33 0 0 0 10.93 1.6c5.86 0 10.05-1.8 12.46-5.34 3.88-5.69 3.33-10.9-1.7-15.92-2.77-2.78-4.62-6.87-5-7.77-.78-2.66-2.84-5.62-6.25-5.62a5.7 5.7 0 0 0-4.6 2.46c-1-1.26-1.98-2.25-2.86-2.82A7.4 7.4 0 0 0 17.52 48Zm0 4c.51 0 1.14.22 1.82.65 2.14 1.36 6.25 8.43 7.76 11.18.5.92 1.37 1.31 2.14 1.31 1.55 0 2.75-1.53.15-3.48-3.92-2.93-2.55-7.72-.68-8.01.08-.02.17-.02.24-.02 1.7 0 2.45 2.93 2.45 2.93s2.2 5.52 5.98 9.3c3.77 3.77 3.97 6.8 1.22 10.83-1.88 2.75-5.47 3.58-9.16 3.58-3.81 0-7.73-.9-9.92-1.46-.11-.03-13.45-3.8-11.76-7 .28-.54.75-.76 1.34-.76 2.38 0 6.7 3.54 8.57 3.54.41 0 .7-.17.83-.6.79-2.85-12.06-4.05-10.98-8.17.2-.73.71-1.02 1.44-1.02 3.14 0 10.2 5.53 11.68 5.53.11 0 .2-.03.24-.1.74-1.2.33-2.04-4.9-5.2-5.21-3.16-8.88-5.06-6.8-7.33.24-.26.58-.38 1-.38 3.17 0 10.66 6.82 10.66 6.82s2.02 2.1 3.25 2.1c.28 0 .52-.1.68-.38.86-1.46-8.06-8.22-8.56-11.01-.34-1.9.24-2.85 1.31-2.85Z"/>
<path fill="#FFD21E" d="M38.6 76.69c2.75-4.04 2.55-7.07-1.22-10.84-3.78-3.77-5.98-9.3-5.98-9.3s-.82-3.2-2.69-2.9c-1.87.3-3.24 5.08.68 8.01 3.91 2.93-.78 4.92-2.29 2.17-1.5-2.75-5.62-9.82-7.76-11.18-2.13-1.35-3.63-.6-3.13 2.2.5 2.79 9.43 9.55 8.56 11-.87 1.47-3.93-1.71-3.93-1.71s-9.57-8.71-11.66-6.44c-2.08 2.27 1.59 4.17 6.8 7.33 5.23 3.16 5.64 4 4.9 5.2-.75 1.2-12.28-8.53-13.36-4.4-1.08 4.11 11.77 5.3 10.98 8.15-.8 2.85-9.06-5.38-10.74-2.18-1.7 3.21 11.65 6.98 11.76 7.01 4.3 1.12 15.25 3.49 19.08-2.12Z"/>
<path fill="#FF9D0B" d="M77.4 48c1.62 0 3.07.66 4.07 1.87a5.97 5.97 0 0 1 1.33 3.76 7.1 7.1 0 0 1 1.95-.3c1.55 0 2.95.59 3.94 1.66a5.8 5.8 0 0 1 .8 7 5.3 5.3 0 0 1 1.78 2.82c.24.9.48 2.8-.8 4.74a5.22 5.22 0 0 1 .37 5.02c-1.02 2.32-3.57 4.14-8.51 6.1-3.08 1.22-5.9 2-5.92 2.01a44.33 44.33 0 0 1-10.93 1.6c-5.86 0-10.05-1.8-12.46-5.34-3.88-5.69-3.33-10.9 1.7-15.92 2.78-2.78 4.63-6.87 5.01-7.77.78-2.66 2.83-5.62 6.24-5.62a5.7 5.7 0 0 1 4.6 2.46c1-1.26 1.98-2.25 2.87-2.82A7.4 7.4 0 0 1 77.4 48Zm0 4c-.51 0-1.13.22-1.82.65-2.13 1.36-6.25 8.43-7.76 11.18a2.43 2.43 0 0 1-2.14 1.31c-1.54 0-2.75-1.53-.14-3.48 3.91-2.93 2.54-7.72.67-8.01a1.54 1.54 0 0 0-.24-.02c-1.7 0-2.45 2.93-2.45 2.93s-2.2 5.52-5.97 9.3c-3.78 3.77-3.98 6.8-1.22 10.83 1.87 2.75 5.47 3.58 9.15 3.58 3.82 0 7.73-.9 9.93-1.46.1-.03 13.45-3.8 11.76-7-.29-.54-.75-.76-1.34-.76-2.38 0-6.71 3.54-8.57 3.54-.42 0-.71-.17-.83-.6-.8-2.85 12.05-4.05 10.97-8.17-.19-.73-.7-1.02-1.44-1.02-3.14 0-10.2 5.53-11.68 5.53-.1 0-.19-.03-.23-.1-.74-1.2-.34-2.04 4.88-5.2 5.23-3.16 8.9-5.06 6.8-7.33-.23-.26-.57-.38-.98-.38-3.18 0-10.67 6.82-10.67 6.82s-2.02 2.1-3.24 2.1a.74.74 0 0 1-.68-.38c-.87-1.46 8.05-8.22 8.55-11.01.34-1.9-.24-2.85-1.31-2.85Z"/>
<path fill="#FFD21E" d="M56.33 76.69c-2.75-4.04-2.56-7.07 1.22-10.84 3.77-3.77 5.97-9.3 5.97-9.3s.82-3.2 2.7-2.9c1.86.3 3.23 5.08-.68 8.01-3.92 2.93.78 4.92 2.28 2.17 1.51-2.75 5.63-9.82 7.76-11.18 2.13-1.35 3.64-.6 3.13 2.2-.5 2.79-9.42 9.55-8.55 11 .86 1.47 3.92-1.71 3.92-1.71s9.58-8.71 11.66-6.44c2.08 2.27-1.58 4.17-6.8 7.33-5.23 3.16-5.63 4-4.9 5.2.75 1.2 12.28-8.53 13.36-4.4 1.08 4.11-11.76 5.3-10.97 8.15.8 2.85 9.05-5.38 10.74-2.18 1.69 3.21-11.65 6.98-11.76 7.01-4.31 1.12-15.26 3.49-19.08-2.12Z"/>
</svg>`;
interface LoginPageOptions {
title: string;
description: string;
/**
* Optional callout block shown between the description and the
* sign-in button. Used to surface "you're authenticated but you
* didn't grant access to the <org> organization" hints. HTML must
* already be escaped by the caller.
*/
noteHtml?: string;
/** Button label override (default: "Sign in with Hugging Face"). */
buttonLabel?: string;
/** Button href override (default: "/oauth/authorize"). */
buttonHref?: string;
}
function renderLoginPage({
title,
description,
noteHtml,
buttonLabel,
buttonHref,
}: LoginPageOptions): string {
const safeTitle = escapeHtml(title);
const safeDescription = escapeHtml(description);
const safeButtonLabel = escapeHtml(buttonLabel || "Sign in with Hugging Face");
const safeButtonHref = escapeHtml(buttonHref || "/oauth/authorize");
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${safeTitle} - Research Article Template Editor</title>
<style>
:root {
--primary-color: oklch(0.75 0.12 47);
--primary-color-hover: oklch(from var(--primary-color) calc(l - 0.06) c h);
--primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h);
--page-bg: #fafaf9;
--text-color: #1a1a1a;
--muted-color: rgba(0, 0, 0, .58);
--font-stack: "Source Sans Pro", -apple-system, BlinkMacSystemFont,
"SF Pro Display", "SF Pro Text", "Segoe UI", system-ui, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--page-bg: #0c0d10;
--text-color: rgba(255, 255, 255, .92);
--muted-color: rgba(255, 255, 255, .58);
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
font-family: var(--font-stack);
background:
radial-gradient(
ellipse 720px 520px at 50% -10%,
color-mix(in srgb, var(--primary-color) 18%, transparent),
transparent 65%
),
radial-gradient(
ellipse 480px 360px at 100% 110%,
color-mix(in srgb, var(--primary-color) 9%, transparent),
transparent 70%
),
var(--page-bg);
color: var(--text-color);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.login {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
max-width: 480px;
width: 100%;
}
.login__title {
color: var(--text-color);
font-size: clamp(1.75rem, 1.4rem + 1.6vw, 2.25rem);
font-weight: 600;
letter-spacing: -0.025em;
line-height: 1.1;
margin-bottom: 14px;
}
.login__description {
max-width: 380px;
font-size: 1.0625rem;
line-height: 1.5;
color: var(--muted-color);
margin-bottom: 40px;
}
.login__btn {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 28px;
font-family: inherit;
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.005em;
color: var(--text-color);
background: transparent;
border: 1.5px solid var(--primary-color);
border-radius: 999px;
text-decoration: none;
cursor: pointer;
transition:
transform 180ms cubic-bezier(.2, .9, .3, 1),
background-color 180ms ease,
border-color 180ms ease;
}
.login__btn:hover {
transform: translateY(-1px);
background: color-mix(in srgb, var(--primary-color) 10%, transparent);
border-color: var(--primary-color-hover);
}
.login__btn:active {
transform: translateY(0);
background: color-mix(in srgb, var(--primary-color) 16%, transparent);
}
.login__btn:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 4px;
}
.login__btn svg {
flex-shrink: 0;
display: block;
}
.login__note {
max-width: 480px;
margin: 0 0 32px;
padding: 16px 20px;
text-align: left;
background: color-mix(in srgb, var(--primary-color) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--primary-color) 30%, transparent);
border-radius: 12px;
color: var(--text-color);
font-size: 0.9375rem;
line-height: 1.55;
}
.login__note strong { font-weight: 600; }
.login__note code {
padding: 1px 6px;
background: color-mix(in srgb, var(--text-color) 8%, transparent);
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.875em;
}
.login__note ol {
margin: 8px 0 0;
padding-left: 20px;
}
.login__note ol li + li { margin-top: 4px; }
</style>
</head>
<body>
<main class="login">
<h1 class="login__title">${safeTitle}</h1>
<p class="login__description">${safeDescription}</p>
${noteHtml ? `<div class="login__note">${noteHtml}</div>` : ""}
<a class="login__btn" href="${safeButtonHref}" target="_blank" rel="noopener">
${HF_LOGO_SVG}
<span>${safeButtonLabel}</span>
</a>
</main>
</body>
</html>`;
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function sendLoginPage(res: express.Response) {
res.status(200).send(
renderLoginPage({
title: "This article is not yet published",
description: "Sign in with your Hugging Face account to access the editor.",
}),
);
}
/**
* Pick the right "you can't edit" message for the /editor login page
* based on what we know about the requester:
*
* - No user at all (no cookie / invalid token): plain sign-in prompt.
* - User authenticated, Space owned by an org, but the org isn't in
* the OAuth grant scope: explain that they need to tick the org
* checkbox on HF's consent screen, and link straight to a
* force-consent login URL.
* - User authenticated, org grant is fine, but they're not in the
* write/admin members of the org (or it's a personal Space they
* don't own): tell them they don't have permission - no point
* re-prompting the consent screen, an admin needs to add them.
*/
function buildEditorLoginPage(
user:
| { name: string; accessIssue?: "no-org-grant" | "not-member"; spaceOrg?: string }
| null,
): LoginPageOptions {
if (!user) {
return {
title: "Editor access",
description: "Sign in with your Hugging Face account to start editing.",
};
}
const safeHandle = escapeHtml(user.name);
const safeOrg = user.spaceOrg ? escapeHtml(user.spaceOrg) : "";
if (user.accessIssue === "no-org-grant" && safeOrg) {
return {
title: "Permission needed",
description: `You're signed in as @${safeHandle}, but this app can't see your membership of the ${safeOrg} organization.`,
noteHtml: [
`<strong>To unlock editing</strong>, re-authorize the app and tick the <code>${safeOrg}</code> organization on the consent screen:`,
`<ol>`,
`<li>Click <em>Authorize again</em> below.</li>`,
`<li>On the Hugging Face consent screen, scroll to <em>Organizations</em>.</li>`,
`<li>Toggle access for <code>${safeOrg}</code> on, then confirm.</li>`,
`</ol>`,
].join(""),
buttonLabel: "Authorize again",
buttonHref: "/oauth/authorize?prompt=consent",
};
}
return {
title: "No write access",
description:
`You're signed in as @${safeHandle}, but your account doesn't have write access to this Space` +
(safeOrg ? ` (owned by ${safeOrg}).` : "."),
noteHtml:
`Ask an admin of <code>${safeOrg || "the Space"}</code> to add you as a write member. ` +
`If you have multiple Hugging Face accounts, you can also sign out and try again with the right one.`,
buttonLabel: "Sign in with another account",
buttonHref: "/oauth/authorize?prompt=consent",
};
}
export function createApp() {
const DATA_DIR = getDataDir();
mkdirSync(DATA_DIR, { recursive: true });
const oauthEnabled = isOAuthEnabled();
// ---------- Hocuspocus (Y.js collaboration server) ----------
const hocuspocus = new Hocuspocus({
async onAuthenticate({ token, context }: { token: string; context: any }) {
if (!oauthEnabled) return;
const { resolveUser } = await import("./auth.js");
// Two-source pattern: the HocuspocusProvider client sends `token`
// via the WS sub-protocol, but our cookie is httpOnly so the
// client can't read it and sends "" instead. Fall back to the
// cookie the upgrade handler stuffed into context.token.
const authToken = token || context?.token;
// Surface enough info in the Space logs to triage "Disconnected"
// reports without leaking the token itself. We log:
// - whether each source produced a token (just truthiness)
// - the resolved user (or null) + accessIssue when present
// - the SPACE_ID owner being checked, to spot org mismatches
const tokenSource = token
? "client"
: context?.token
? "cookie"
: "none";
const tokenLen = authToken ? authToken.length : 0;
const user = await resolveUser(authToken);
const spaceOwner = (process.env.SPACE_ID || "").split("/")[0] || "(none)";
if (!user) {
console.warn(
`[ws-auth] reject: no user resolved` +
` source=${tokenSource} tokenLen=${tokenLen}` +
` spaceOwner=${spaceOwner}`,
);
throw new Error("Unauthorized: invalid or missing HF token");
}
if (!user.canEdit) {
console.warn(
`[ws-auth] reject: ${user.name} can't write to ${spaceOwner}` +
` issue=${user.accessIssue ?? "unknown"}` +
` source=${tokenSource}`,
);
throw new Error(
`Unauthorized: ${user.name} has no write access to ${spaceOwner}` +
(user.accessIssue ? ` (${user.accessIssue})` : ""),
);
}
console.log(
`[ws-auth] accept user=${user.name} spaceOwner=${spaceOwner}` +
` source=${tokenSource}`,
);
if (authToken) setUserToken(authToken);
return { user };
},
extensions: [
new Database({
fetch: async ({ documentName }: { documentName: string }) => {
try {
const p = docPath(documentName);
if (existsSync(p)) {
const buf = readFileSync(p);
console.log(`[persist] fetch "${documentName}" from disk: ${buf.length} bytes`);
return buf;
}
console.log(`[persist] fetch "${documentName}": no file on disk`);
if (isHfStorageEnabled()) {
const data = await pullDocument(documentName);
if (data) {
writeFileSync(p, data);
console.log(`[persist] pulled ${documentName} from HF`);
return Buffer.from(data);
}
}
} catch (err) {
console.error(`[persist] fetch "${documentName}" failed:`, (err as Error).message);
}
return null;
},
store: async () => {},
}),
{
async onChange({ documentName, document }: { documentName: string; document: any }) {
console.log(`[persist] onChange "${documentName}"`);
debouncedSave(documentName, document);
},
async afterLoadDocument({ documentName }: { documentName: string }) {
console.log(`[persist] loaded "${documentName}"`);
},
} as any,
],
});
// ---------- Express app ----------
const app = express();
const httpServer = createServer(app);
app.use(express.json({ limit: "1mb" }));
const authCtx = { oauthEnabled };
const requireEditor = createRequireEditor(authCtx);
app.use(createAuthRouter(authCtx));
app.use(createChatRouter(requireEditor));
app.use("/api/citations", citationsRouter);
app.use(createPublishRouter({ oauthEnabled, hocuspocus }));
app.use(createUploadRouter());
app.use(createStorageRouter({ oauthEnabled }));
// Reverse proxy for private-dataset assets. Mounted before any
// static serving so `/d/*` always wins, never falls through to a
// 404 from express.static.
app.use(createDatasetProxyRouter());
// ---------- Collab WebSocket ----------
const wss = new WebSocketServer({ noServer: true });
httpServer.on("upgrade", (req, socket, head) => {
const url = req.url || "";
if (url === "/collab" || url.startsWith("/collab/") || url.startsWith("/collab?")) {
console.log(`[ws] upgrade request for ${url}`);
wss.handleUpgrade(req, socket, head, (ws) => {
ws.setMaxListeners(Infinity);
ws.on("error", (error) => {
console.error("[ws] socket error:", error.message);
});
if (process.env.NODE_ENV !== "production") {
ws.on("message", (data: Buffer, isBinary: boolean) => {
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data as any);
console.log(`[ws-debug] msg ${buf.length}B binary=${isBinary} first20=${buf.slice(0, 20).toString("hex")}`);
});
}
const token = extractToken(req.headers.cookie);
// Diagnostic for "Disconnected" reports: confirms whether the
// browser actually attached our session cookie to the WS
// upgrade. On HF Spaces, some gating setups occasionally
// strip cookies on WS upgrades even when they're sent for
// plain HTTP, which manifests as a working /editor route
// but a permanently-failing WS auth.
const cookieHeader = req.headers.cookie || "";
console.log(
`[ws] upgrade cookies=${cookieHeader.length}B hasToken=${Boolean(token)}`,
);
hocuspocus.handleConnection(ws, req, { token });
});
} else {
console.log(`[ws] rejected upgrade for ${url}`);
socket.destroy();
}
});
// ---------- Static assets ----------
app.use("/uploads", express.static(join(DATA_DIR, "uploads")));
app.use("/published", express.static(join(DATA_DIR, "published")));
const staticDir =
process.env.NODE_ENV === "production"
? join(DATA_DIR, "..", "frontend-dist")
: join(DATA_DIR, "..", "..", "frontend", "dist");
function getPublishedPath(docName: string): string {
return join(DATA_DIR, "published", docName, "index.html");
}
function getPublishedAssetPath(docName: string, filename: string): string {
return join(DATA_DIR, "published", docName, filename);
}
if (existsSync(staticDir)) {
app.use(express.static(staticDir, { index: false }));
// ---- LLM-friendly endpoints --------------------------------------
// The publisher generates a Markdown twin of the article (`llms.txt`)
// following the https://llmstxt.org/ convention. We expose it at the
// Space root so external agents/crawlers (Claude, Perplexity, ...) can
// consume the article without having to parse the heavy HTML page.
// `robots.txt` advertises this endpoint.
app.get("/llms.txt", async (_req, res) => {
const llmsPath = getPublishedAssetPath(DEFAULT_DOC_NAME, "llms.txt");
if (!existsSync(llmsPath)) {
res.status(404).type("text/plain").send("Not yet published");
return;
}
res.type("text/markdown; charset=utf-8");
res.sendFile(llmsPath);
});
app.get("/robots.txt", (_req, res) => {
res
.type("text/plain; charset=utf-8")
.send(
[
"User-agent: *",
"Allow: /",
"",
"LLMs-Txt: /llms.txt",
"",
].join("\n"),
);
});
app.get("/editor", async (req, res) => {
if (oauthEnabled) {
const { resolveUser } = await import("./auth.js");
const token = extractToken(req.headers.cookie);
const user = await resolveUser(token);
if (!user || !user.canEdit) {
res.status(200).send(renderLoginPage(buildEditorLoginPage(user)));
return;
}
}
res.sendFile(join(staticDir, "index.html"));
});
app.get("*", async (req, res) => {
if (!oauthEnabled) {
res.sendFile(join(staticDir, "index.html"));
return;
}
const visitorToken = extractToken(req.headers.cookie) ?? undefined;
await ensurePublishedRestored(visitorToken);
const publishedPath = getPublishedPath(DEFAULT_DOC_NAME);
if (existsSync(publishedPath)) {
res.sendFile(publishedPath);
return;
}
sendLoginPage(res);
});
}
return { app, httpServer, hocuspocus, wss };
}