import path from "node:path"; import crypto from "node:crypto"; import express, { type NextFunction, type Request, type Response } from "express"; import fs from "fs-extra"; import { config } from "./config.js"; import { store } from "./db.js"; import { captureQueue } from "./queue.js"; import { newCaptureId } from "./capture.js"; import { validateChatGptShareUrl } from "./url.js"; import { slugify } from "./slug.js"; import { artifactAssetsDir, artifactDir, artifactIndexPath, deleteAllArtifactRoots, deleteArtifactsFor, screenshotPath, selfContainedHtml, zipArtifact } from "./artifacts.js"; const app = express(); const retentionOptions = new Set([3, 7, 10, 30]); const ownerCookieName = "aishare_owner"; app.disable("x-powered-by"); app.enable("strict routing"); app.use(express.json({ limit: "1mb" })); function route(pathname = ""): string { if (!pathname) return config.basePath || "/"; const suffix = pathname.startsWith("/") ? pathname : `/${pathname}`; return `${config.basePath}${suffix}` || "/"; } function publicPath(slug: string): string { return `${config.basePath}/${slug}/` || `/${slug}/`; } function requireAdmin(req: Request, res: Response, next: NextFunction): void { if (!config.adminToken) { res.status(500).json({ error: "AISHARE_ADMIN_TOKEN is not configured." }); return; } const header = req.header("authorization") || ""; const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : req.header("x-admin-token"); if (token && token === config.adminToken) { next(); return; } res.status(401).json({ error: "Admin token required." }); } function isAdminRequest(req: Request): boolean { const header = req.header("authorization") || ""; const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : req.header("x-admin-token"); return Boolean(config.adminToken && token && token === config.adminToken); } function cookieValue(req: Request, name: string): string | undefined { const cookie = req.header("cookie") || ""; for (const part of cookie.split(";")) { const [key, ...value] = part.trim().split("="); if (key === name) return decodeURIComponent(value.join("=")); } return undefined; } app.use((req, res, next) => { let ownerToken = cookieValue(req, ownerCookieName); if (!ownerToken || !/^[A-Za-z0-9_-]{32,128}$/.test(ownerToken)) { ownerToken = crypto.randomBytes(32).toString("base64url"); const cookiePath = config.basePath || "/"; const secure = req.secure || req.header("x-forwarded-proto") === "https"; res.cookie(ownerCookieName, ownerToken, { httpOnly: true, sameSite: "lax", secure, path: cookiePath, maxAge: 365 * 24 * 60 * 60 * 1000 }); } (req as Request & { ownerToken?: string }).ownerToken = ownerToken; res.locals.ownerToken = ownerToken; next(); }); function canManageCapture(req: Request, row: NonNullable>): boolean { const ownerToken = (req as Request & { ownerToken?: string }).ownerToken; return isAdminRequest(req) || Boolean(row.owner_token && row.owner_token === ownerToken); } function publicCapture(row: ReturnType) { if (!row) return null; return { id: row.id, sourceUrl: row.source_url, provider: row.provider, slug: row.slug, title: row.title, status: row.status, errorPublic: row.error_public, createdAt: row.created_at, updatedAt: row.updated_at, capturedAt: row.captured_at, retentionDays: row.retention_days, expiresAt: row.expires_at, publicUrl: row.slug ? publicPath(row.slug) : null }; } function safeSendFrom(root: string, requested: string, res: Response): void { const clean = path.normalize(requested).replace(/^(\.\.(\/|\\|$))+/, ""); const target = path.resolve(root, clean); const resolvedRoot = path.resolve(root); if (!target.startsWith(resolvedRoot)) { res.status(400).send("Invalid path"); return; } res.sendFile(target); } app.use(route("/assets"), express.static(path.resolve("dist-client/assets"), { immutable: true, maxAge: "1y" })); app.use(route("/admin-assets"), express.static(path.resolve("dist-client"), { immutable: true, maxAge: "1y" })); app.get(route("/admin"), (_req, res) => { res.redirect(302, route() || "/"); }); function sendApp(_req: Request, res: Response): void { res.sendFile(path.resolve("dist-client/index.html")); } app.get(route("/api/health"), (_req, res) => { res.json({ ok: true }); }); app.get(route("/api/session"), (req, res) => { res.json({ isAdmin: isAdminRequest(req) }); }); app.post(route("/api/captures"), (req, res) => { try { const sourceUrl = validateChatGptShareUrl(String(req.body?.sourceUrl || "")); const admin = isAdminRequest(req); const requestedSlug = admin && req.body?.slug ? slugify(String(req.body.slug)) : null; const retentionDays = Number(req.body?.retentionDays ?? 3); if (!retentionOptions.has(retentionDays)) throw new Error("Retention must be 3, 7, 10, or 30 days."); const row = store.createCapture({ id: newCaptureId(), sourceUrl, requestedSlug, ownerToken: admin ? null : String(res.locals.ownerToken), retentionDays }); captureQueue.enqueue(row.id); res.status(202).json({ capture: publicCapture(row) }); } catch (error) { res.status(400).json({ error: error instanceof Error ? error.message : String(error) }); } }); app.get(route("/api/captures"), (req, res) => { const page = Number.parseInt(String(req.query.page || "1"), 10); const pageSize = Number.parseInt(String(req.query.pageSize || "20"), 10); const admin = isAdminRequest(req); const result = admin ? store.listCaptures(page, pageSize) : store.listCapturesForOwner(String(res.locals.ownerToken), page, pageSize); res.json({ page, pageSize, total: result.total, isAdmin: admin, captures: result.rows.map(publicCapture) }); }); app.get(route("/api/captures/:id"), (req, res) => { const row = store.getCapture(req.params.id); if (!row || !canManageCapture(req, row)) { res.status(404).json({ error: "Capture not found." }); return; } res.json({ capture: publicCapture(row), errorDetail: row.error_detail, metadata: row.metadata_json ? JSON.parse(row.metadata_json) : null }); }); app.post(route("/api/captures/:id/retry"), (req, res) => { const row = store.getCapture(req.params.id); if (!row || !canManageCapture(req, row)) { res.status(404).json({ error: "Capture not found." }); return; } if (row.status === "ready") { res.status(409).json({ error: "Successful captures are immutable. Create a new capture instead." }); return; } store.retryCapture(row.id); captureQueue.enqueue(row.id); res.json({ capture: publicCapture(store.getCapture(row.id)) }); }); app.delete(route("/api/captures/:id"), async (req, res) => { const existing = store.getCapture(req.params.id); if (!existing || !canManageCapture(req, existing)) { res.status(404).json({ error: "Capture not found." }); return; } const row = store.deleteCapture(req.params.id)!; await deleteArtifactsFor(row); res.json({ ok: true }); }); app.delete(route("/api/captures"), requireAdmin, async (_req, res) => { store.deleteAllCaptures(); await deleteAllArtifactRoots(); res.json({ ok: true }); }); app.get(route("/:slug/assets/*"), (req, res) => { const wildcard = (req.params as Record)[0] || ""; safeSendFrom(artifactAssetsDir(req.params.slug), wildcard, res); }); app.get(route("/:slug/screenshots/:variant.png"), (req, res) => { res.sendFile(screenshotPath(req.params.slug, req.params.variant)); }); app.get(route("/:slug/screenshot.png"), (req, res) => { const device = req.query.device === "mobile" ? "mobile" : "desktop"; const theme = req.query.theme === "dark" ? "dark" : "light"; res.sendFile(screenshotPath(req.params.slug, `${device}-${theme}`)); }); app.get(route("/:slug/download.html"), (req, res) => { const row = store.getCaptureBySlug(req.params.slug); if (!row) { res.status(404).send("Not found"); return; } res.download(artifactIndexPath(req.params.slug), `${req.params.slug}.html`); }); app.get(route("/:slug/download"), async (req, res, next) => { try { const row = store.getCaptureBySlug(req.params.slug); if (!row) { res.status(404).send("Not found"); return; } try { const html = await selfContainedHtml(req.params.slug); res.setHeader("Content-Type", "text/html; charset=utf-8"); res.attachment(`${req.params.slug}.html`); res.send(html); } catch { const zipPath = path.join(config.cacheDir, `${req.params.slug}.zip`); await zipArtifact(req.params.slug, zipPath); res.download(zipPath, `${req.params.slug}.zip`); } } catch (error) { next(error); } }); app.get(route("/:slug/download.zip"), async (req, res, next) => { try { const row = store.getCaptureBySlug(req.params.slug); if (!row) { res.status(404).send("Not found"); return; } const zipPath = path.join(config.cacheDir, `${req.params.slug}.zip`); await zipArtifact(req.params.slug, zipPath); res.download(zipPath, `${req.params.slug}.zip`); } catch (error) { next(error); } }); app.get(route("/:slug/"), async (req, res) => { const row = store.getCaptureBySlug(req.params.slug); if (!row) { res.status(404).send("Not found"); return; } const index = artifactIndexPath(req.params.slug); if (await fs.pathExists(index)) { res.sendFile(index); return; } res .status(row.status === "failed" ? 500 : 202) .send(`${row.title || "AI Share"}

${row.error_public || `Capture is ${row.status}.`}

`); }); app.get(route("/:slug"), (req, res) => { res.redirect(308, `${req.originalUrl.replace(/[?#].*$/, "")}/`); }); if (config.basePath) { app.get(route(), (_req, res) => { res.redirect(308, route("/")); }); app.get(route("/"), sendApp); } else { app.get("/", sendApp); } app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => { console.error(error); res.status(500).json({ error: "Internal server error." }); }); await fs.ensureDir(config.htmlDir); await fs.ensureDir(config.screenshotDir); await fs.ensureDir(config.cacheDir); async function deleteExpiredCaptures(): Promise { for (const row of store.expiredCaptures()) { const deleted = store.deleteCapture(row.id); if (deleted) await deleteArtifactsFor(deleted); } } await deleteExpiredCaptures(); captureQueue.restorePending(); setInterval(() => { void deleteExpiredCaptures().catch((error) => console.error("Failed to delete expired captures", error)); }, 60 * 60 * 1000).unref(); app.listen(config.port, "0.0.0.0", () => { console.log(`aishare listening on 0.0.0.0:${config.port}`); });