import { getHeader, getMethod, setResponseHeader, setResponseStatus, type H3Event, } from "h3"; import type { AgentRunSummary } from "../../agent/run-store.js"; import { normalizeThreadRepository } from "../../agent/thread-data-builder.js"; import type { ChatThread } from "../../chat-threads/store.js"; // --------------------------------------------------------------------------- // Read-only shared-thread route: renders a public HTML/JSON view of a chat // thread reachable via its share token (see `handleSharedThreadRequest`). // --------------------------------------------------------------------------- function sanitizeSharedThread(thread: ChatThread): { id: string; title: string; preview: string; messageCount: number; createdAt: number; updatedAt: number; scope: { type: string; label?: string } | null; messages: Array<{ id?: string; role: "user" | "assistant" | "system"; text: string; createdAt?: string | number; }>; } { let repo: any = {}; try { repo = normalizeThreadRepository(JSON.parse(thread.threadData)); } catch { repo = {}; } const messages = Array.isArray(repo.messages) ? repo.messages .map((entry: unknown) => sanitizeSharedMessage(entry)) .filter( ( entry: unknown, ): entry is NonNullable> => entry != null, ) : []; return { id: thread.id, title: thread.title, preview: thread.preview, messageCount: thread.messageCount, createdAt: thread.createdAt, updatedAt: thread.updatedAt, scope: thread.scope ? { type: thread.scope.type, ...(thread.scope.label ? { label: thread.scope.label } : {}), } : null, messages, }; } type SanitizedSharedThread = ReturnType; export interface SharedThreadRouteDependencies { getThreadByShareToken: (token: string) => Promise; listRunsForThread: ( threadId: string, options?: { limit?: number }, ) => Promise; } function escapeSharedThreadHtml(value: unknown): string { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function wantsSharedThreadHtml(event: H3Event): boolean { const accept = getHeader(event, "accept")?.toLowerCase() ?? ""; return accept.includes("text/html") && !accept.includes("application/json"); } function formatSharedThreadTime(value: string | number | null | undefined) { if (value == null) return ""; const date = typeof value === "number" ? new Date(value) : Number.isFinite(Number(value)) ? new Date(Number(value)) : new Date(value); return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString(); } function renderSharedThreadHtml( thread: SanitizedSharedThread, runs: AgentRunSummary[], ): string { const title = thread.title || "Shared agent session"; const messages = thread.messages .map((message) => { const time = formatSharedThreadTime(message.createdAt); return `
${escapeSharedThreadHtml(message.role)} ${time ? `` : ""}
${escapeSharedThreadHtml(message.text)}
`; }) .join(""); const runsHtml = runs.length ? `

Recent runs

    ${runs .map((run) => { const started = formatSharedThreadTime(run.startedAt); const completed = formatSharedThreadTime(run.completedAt); const detail = [ started ? `started ${started}` : "", completed ? `completed ${completed}` : "", run.errorCode ? `error ${run.errorCode}` : "", run.abortReason ? `aborted ${run.abortReason}` : "", ] .filter(Boolean) .join(" ยท "); return `
  1. ${escapeSharedThreadHtml(run.status)}${detail ? `${escapeSharedThreadHtml(detail)}` : ""}
  2. `; }) .join("")}
` : ""; return ` ${escapeSharedThreadHtml(title)}

Read-only shared agent session

${escapeSharedThreadHtml(title)}

${escapeSharedThreadHtml(thread.preview || `${thread.messageCount} message${thread.messageCount === 1 ? "" : "s"}`)}

${messages || '

No transcript messages were shared.

'} ${runsHtml}
`; } function renderSharedThreadErrorHtml(status: number, message: string): string { return `${status} ${escapeSharedThreadHtml(message)}

${status}

${escapeSharedThreadHtml(message)}

`; } function sharedThreadError(event: H3Event, status: number, message: string) { setResponseStatus(event, status); setResponseHeader(event, "Cache-Control", "private, no-store"); setResponseHeader(event, "X-Robots-Tag", "noindex, nofollow"); if (wantsSharedThreadHtml(event)) { setResponseHeader(event, "Content-Type", "text/html; charset=utf-8"); return renderSharedThreadErrorHtml(status, message); } return { error: message }; } export async function handleSharedThreadRequest( event: H3Event, deps: SharedThreadRouteDependencies, ) { const method = getMethod(event); if (method !== "GET") { return sharedThreadError(event, 405, "Method not allowed"); } const token = parseSharedThreadToken(event); if (!token) { return sharedThreadError(event, 400, "Share token is required"); } const thread = await deps.getThreadByShareToken(token); if (!thread) { return sharedThreadError(event, 404, "Shared thread not found"); } const runs = await deps.listRunsForThread(thread.id, { limit: 10 }); const payload = { thread: sanitizeSharedThread(thread), runs, }; setResponseHeader(event, "Cache-Control", "private, no-store"); setResponseHeader(event, "X-Robots-Tag", "noindex, nofollow"); if (wantsSharedThreadHtml(event)) { setResponseHeader(event, "Content-Type", "text/html; charset=utf-8"); return renderSharedThreadHtml(payload.thread, runs); } setResponseHeader(event, "Content-Type", "application/json"); return payload; } function sanitizeSharedMessage(entry: unknown): { id?: string; role: "user" | "assistant" | "system"; text: string; createdAt?: string | number; } | null { if (!entry || typeof entry !== "object") return null; const raw = entry as Record; const message = raw.message && typeof raw.message === "object" ? (raw.message as Record) : raw; const role = message.role; if (role !== "user" && role !== "assistant" && role !== "system") { return null; } const text = sharedTextFromContent(message.content).trim(); if (!text) return null; const id = typeof message.id === "string" ? message.id : undefined; const createdAt = typeof message.createdAt === "string" || typeof message.createdAt === "number" ? message.createdAt : undefined; return { ...(id ? { id } : {}), role, text, ...(createdAt !== undefined ? { createdAt } : {}), }; } function sharedTextFromContent(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; const parts: string[] = []; for (const part of content) { if (!part || typeof part !== "object") continue; const r = part as Record; if (r.type !== "text") continue; if (typeof r.text === "string") parts.push(r.text); } return parts.join(""); } function parseSharedThreadToken(event: H3Event): string | null { const candidates = [event.path, event.node?.req?.url].filter( (value): value is string => typeof value === "string" && value.length > 0, ); for (const candidate of candidates) { const path = candidate.split("?")[0]; const parts = path.replace(/^\/+/, "").split("/").filter(Boolean); const sharedIndex = parts.lastIndexOf("shared"); if (sharedIndex >= 0 && parts[sharedIndex + 1]) { return decodeURIComponent(parts[sharedIndex + 1]); } if (parts[0]) return decodeURIComponent(parts[0]); } return null; }