Download src/canvas/documents.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 13.9 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/canvas/documents.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/canvas/documents.ts
-
curl -L -o documents.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/canvas/documents.ts
13.9 kB
| /** Core Canvas document materialization and hosted-path resolution. */ | |
| import { randomUUID } from "node:crypto"; | |
| import fs from "node:fs/promises"; | |
| import path from "node:path"; | |
| import { resolveStateDir } from "../config/paths.js"; | |
| import { sanitizeUntrustedFileName } from "../infra/fs-safe-advanced.js"; | |
| import { root as fsRoot } from "../infra/fs-safe.js"; | |
| import { escapeHtml } from "../shared/html-escape.js"; | |
| import { resolveUserPath } from "../utils.js"; | |
| import { CANVAS_DOCUMENTS_PATH } from "./constants.js"; | |
| const CANVAS_DOCUMENT_READ_MAX_BYTES = 2 * 1024 * 1024; | |
| type CanvasDocumentKind = "html_bundle" | "url_embed" | "document" | "image" | "video_asset"; | |
| type CanvasDocumentAsset = { | |
| logicalPath: string; | |
| sourcePath: string; | |
| contentType?: string; | |
| }; | |
| type CanvasDocumentEntrypoint = | |
| | { type: "html"; value: string } | |
| | { type: "path"; value: string } | |
| | { type: "url"; value: string }; | |
| type CanvasDocumentCreateInput = { | |
| id?: string; | |
| kind: CanvasDocumentKind; | |
| title?: string; | |
| preferredHeight?: number; | |
| entrypoint?: CanvasDocumentEntrypoint; | |
| assets?: CanvasDocumentAsset[]; | |
| surface?: "assistant_message" | "tool_card" | "sidebar"; | |
| retentionScope?: string; | |
| /** Serve with a CSP sandbox header so direct opens get an opaque origin. */ | |
| cspSandbox?: "scripts"; | |
| }; | |
| export type CanvasDocumentManifest = { | |
| id: string; | |
| kind: CanvasDocumentKind; | |
| title?: string; | |
| preferredHeight?: number; | |
| createdAt: string; | |
| entryUrl: string; | |
| localEntrypoint?: string; | |
| externalUrl?: string; | |
| surface?: "assistant_message" | "tool_card" | "sidebar"; | |
| retentionScope?: string; | |
| cspSandbox?: "scripts"; | |
| assets: Array<{ | |
| logicalPath: string; | |
| contentType?: string; | |
| }>; | |
| }; | |
| function isPdfPathLike(value: string): boolean { | |
| return /\.pdf(?:[?#].*)?$/i.test(value.trim()); | |
| } | |
| function buildPdfWrapper(url: string): string { | |
| const escaped = escapeHtml(url); | |
| return `<!doctype html><html><body style="margin:0;background:#e5e7eb;"><object data="${escaped}" type="application/pdf" style="width:100%;height:100vh;border:0;"><iframe src="${escaped}" style="width:100%;height:100vh;border:0;"></iframe><p style="padding:16px;font:14px system-ui,sans-serif;">Unable to render PDF preview. <a href="${escaped}" target="_blank" rel="noopener noreferrer">Open PDF</a>.</p></object></body></html>`; | |
| } | |
| function hasControlCharacter(value: string): boolean { | |
| for (const char of value) { | |
| const code = char.charCodeAt(0); | |
| if (code < 0x20 || code === 0x7f) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function normalizeLogicalPath(value: string): string { | |
| const normalized = value.replaceAll("\\", "/").replace(/^\/+/, ""); | |
| const parts = normalized.split("/").filter(Boolean); | |
| if ( | |
| parts.length === 0 || | |
| parts.some( | |
| (part) => part === "." || part === ".." || part.includes(":") || hasControlCharacter(part), | |
| ) | |
| ) { | |
| throw new Error("canvas document logicalPath invalid"); | |
| } | |
| return parts.join("/"); | |
| } | |
| function canvasDocumentId(): string { | |
| return `cv_${randomUUID().replaceAll("-", "")}`; | |
| } | |
| function normalizeCanvasDocumentId(value: string): string { | |
| const normalized = value.trim(); | |
| if ( | |
| !normalized || | |
| normalized === "." || | |
| normalized === ".." || | |
| !/^[A-Za-z0-9._-]+$/.test(normalized) | |
| ) { | |
| throw new Error("canvas document id invalid"); | |
| } | |
| return normalized; | |
| } | |
| /** Stable root for existing and newly created Canvas documents. */ | |
| export function resolveCanvasDocumentsDir(stateDir = resolveStateDir()): string { | |
| return path.resolve(stateDir, "canvas", "documents"); | |
| } | |
| /** Reads the managed HTML entrypoint for a core Canvas document. */ | |
| export async function readCanvasDocumentHtmlSource( | |
| documentId: string, | |
| options?: { stateDir?: string; maxBytes?: number }, | |
| ): Promise<{ html: string; cspSandbox?: "scripts" }> { | |
| const id = normalizeCanvasDocumentId(documentId); | |
| // Keep the document directory inside the guarded root so aliases cannot select another document. | |
| const root = await fsRoot(resolveCanvasDocumentsDir(options?.stateDir), { | |
| maxBytes: CANVAS_DOCUMENT_READ_MAX_BYTES, | |
| }); | |
| const manifest = await root.readJson<Partial<CanvasDocumentManifest>>(`${id}/manifest.json`); | |
| if (manifest.id !== id || typeof manifest.localEntrypoint !== "string") { | |
| throw new Error(`canvas document has no local entrypoint: ${id}`); | |
| } | |
| const entrypoint = normalizeLogicalPath(manifest.localEntrypoint); | |
| if (!entrypoint.toLowerCase().endsWith(".html")) { | |
| throw new Error(`canvas document entrypoint is not HTML: ${id}`); | |
| } | |
| return { | |
| html: await root.readText(`${id}/${entrypoint}`, { | |
| maxBytes: options?.maxBytes ?? CANVAS_DOCUMENT_READ_MAX_BYTES, | |
| }), | |
| ...(manifest.cspSandbox === "scripts" ? { cspSandbox: "scripts" as const } : {}), | |
| }; | |
| } | |
| async function pruneCanvasDocumentsForScope(params: { | |
| documentsDir: string; | |
| retentionScope: string; | |
| maxDocuments: number; | |
| }): Promise<void> { | |
| const entries = await fs.readdir(params.documentsDir, { withFileTypes: true }); | |
| const scopedDocuments = ( | |
| await Promise.all( | |
| entries | |
| .filter((entry) => entry.isDirectory()) | |
| .map(async (entry) => { | |
| try { | |
| const manifest = JSON.parse( | |
| await fs.readFile( | |
| path.join(params.documentsDir, entry.name, "manifest.json"), | |
| "utf8", | |
| ), | |
| ) as { createdAt?: unknown; retentionScope?: unknown }; | |
| if ( | |
| manifest.retentionScope !== params.retentionScope || | |
| typeof manifest.createdAt !== "string" | |
| ) { | |
| return null; | |
| } | |
| return { id: entry.name, createdAt: manifest.createdAt }; | |
| } catch { | |
| return null; | |
| } | |
| }), | |
| ) | |
| ).filter((entry): entry is { id: string; createdAt: string } => entry !== null); | |
| const deleteCount = Math.max(0, scopedDocuments.length - params.maxDocuments); | |
| const oldest = scopedDocuments | |
| .toSorted( | |
| (left, right) => | |
| left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id), | |
| ) | |
| .slice(0, deleteCount); | |
| await Promise.all( | |
| oldest.map((entry) => | |
| fs.rm(path.join(params.documentsDir, entry.id), { recursive: true, force: true }), | |
| ), | |
| ); | |
| } | |
| /** Resolves the on-disk directory for one Canvas document id. */ | |
| function resolveCanvasDocumentDir(documentId: string, options?: { stateDir?: string }): string { | |
| return path.join(resolveCanvasDocumentsDir(options?.stateDir), documentId); | |
| } | |
| /** Builds the hosted URL path for a Canvas document entrypoint. */ | |
| function buildCanvasDocumentEntryUrl(documentId: string, entrypoint: string): string { | |
| const normalizedEntrypoint = normalizeLogicalPath(entrypoint); | |
| const encodedEntrypoint = normalizedEntrypoint | |
| .split("/") | |
| .map((segment) => encodeURIComponent(segment)) | |
| .join("/"); | |
| return `${CANVAS_DOCUMENTS_PATH}/${encodeURIComponent(documentId)}/${encodedEntrypoint}`; | |
| } | |
| /** Maps a Canvas hosted document URL path back to its managed local file. */ | |
| export function resolveCanvasHttpPathToLocalPath( | |
| requestPath: string, | |
| options?: { stateDir?: string }, | |
| ): string | null { | |
| const trimmed = requestPath.trim(); | |
| const prefix = `${CANVAS_DOCUMENTS_PATH}/`; | |
| if (!trimmed.startsWith(prefix)) { | |
| return null; | |
| } | |
| const pathWithoutQuery = trimmed.replace(/[?#].*$/, ""); | |
| const relative = pathWithoutQuery.slice(prefix.length); | |
| const segments: string[] = []; | |
| for (const segment of relative.split("/")) { | |
| if (!segment) { | |
| continue; | |
| } | |
| try { | |
| segments.push(decodeURIComponent(segment)); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| if (segments.length < 2) { | |
| return null; | |
| } | |
| const [rawDocumentId, ...entrySegments] = segments; | |
| if (!rawDocumentId) { | |
| return null; | |
| } | |
| try { | |
| const documentId = normalizeCanvasDocumentId(rawDocumentId); | |
| const normalizedEntrypoint = normalizeLogicalPath(entrySegments.join("/")); | |
| const documentsDir = resolveCanvasDocumentsDir(options?.stateDir); | |
| const candidatePath = path.resolve( | |
| resolveCanvasDocumentDir(documentId, options), | |
| normalizedEntrypoint, | |
| ); | |
| if (!candidatePath.startsWith(`${documentsDir}${path.sep}`)) { | |
| return null; | |
| } | |
| return candidatePath; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| type CanvasDocumentRoot = Awaited<ReturnType<typeof fsRoot>>; | |
| async function copyAssets( | |
| root: CanvasDocumentRoot, | |
| assets: CanvasDocumentAsset[] | undefined, | |
| workspaceDir: string, | |
| ): Promise<CanvasDocumentManifest["assets"]> { | |
| const copied: CanvasDocumentManifest["assets"] = []; | |
| for (const asset of assets ?? []) { | |
| const logicalPath = normalizeLogicalPath(asset.logicalPath); | |
| const sourcePath = asset.sourcePath.startsWith("~") | |
| ? resolveUserPath(asset.sourcePath) | |
| : path.isAbsolute(asset.sourcePath) | |
| ? path.resolve(asset.sourcePath) | |
| : path.resolve(workspaceDir, asset.sourcePath); | |
| await root.copyIn(logicalPath, sourcePath); | |
| copied.push({ | |
| logicalPath, | |
| ...(asset.contentType ? { contentType: asset.contentType } : {}), | |
| }); | |
| } | |
| return copied; | |
| } | |
| async function materializeEntrypoint( | |
| rootDir: string, | |
| root: CanvasDocumentRoot, | |
| input: CanvasDocumentCreateInput, | |
| workspaceDir: string, | |
| ): Promise<Pick<CanvasDocumentManifest, "entryUrl" | "localEntrypoint" | "externalUrl">> { | |
| const entrypoint = input.entrypoint; | |
| if (!entrypoint) { | |
| throw new Error("canvas document entrypoint required"); | |
| } | |
| if (entrypoint.type === "html") { | |
| const fileName = "index.html"; | |
| await root.write(fileName, entrypoint.value); | |
| return { | |
| localEntrypoint: fileName, | |
| entryUrl: buildCanvasDocumentEntryUrl(path.basename(rootDir), fileName), | |
| }; | |
| } | |
| if (entrypoint.type === "url") { | |
| if (input.kind === "document" && isPdfPathLike(entrypoint.value)) { | |
| const fileName = "index.html"; | |
| await root.write(fileName, buildPdfWrapper(entrypoint.value)); | |
| return { | |
| localEntrypoint: fileName, | |
| externalUrl: entrypoint.value, | |
| entryUrl: buildCanvasDocumentEntryUrl(path.basename(rootDir), fileName), | |
| }; | |
| } | |
| return { externalUrl: entrypoint.value, entryUrl: entrypoint.value }; | |
| } | |
| const resolvedPath = entrypoint.value.startsWith("~") | |
| ? resolveUserPath(entrypoint.value) | |
| : path.isAbsolute(entrypoint.value) | |
| ? path.resolve(entrypoint.value) | |
| : path.resolve(workspaceDir, entrypoint.value); | |
| if (input.kind === "image" || input.kind === "video_asset") { | |
| const copiedName = sanitizeUntrustedFileName(path.basename(resolvedPath), "asset"); | |
| await root.copyIn(copiedName, resolvedPath); | |
| const wrapper = | |
| input.kind === "image" | |
| ? `<!doctype html><html><body style="margin:0;background:#0f172a;display:flex;align-items:center;justify-content:center;"><img src="${escapeHtml(copiedName)}" style="max-width:100%;max-height:100vh;object-fit:contain;" /></body></html>` | |
| : `<!doctype html><html><body style="margin:0;background:#0f172a;"><video src="${escapeHtml(copiedName)}" controls autoplay style="width:100%;height:100vh;object-fit:contain;background:#000;"></video></body></html>`; | |
| await root.write("index.html", wrapper); | |
| return { | |
| localEntrypoint: "index.html", | |
| entryUrl: buildCanvasDocumentEntryUrl(path.basename(rootDir), "index.html"), | |
| }; | |
| } | |
| const fileName = sanitizeUntrustedFileName(path.basename(resolvedPath), "document"); | |
| await root.copyIn(fileName, resolvedPath); | |
| if (input.kind === "document" && isPdfPathLike(fileName)) { | |
| await root.write("index.html", buildPdfWrapper(fileName)); | |
| return { | |
| localEntrypoint: "index.html", | |
| entryUrl: buildCanvasDocumentEntryUrl(path.basename(rootDir), "index.html"), | |
| }; | |
| } | |
| return { | |
| localEntrypoint: fileName, | |
| entryUrl: buildCanvasDocumentEntryUrl(path.basename(rootDir), fileName), | |
| }; | |
| } | |
| /** Creates a Canvas document directory, copies assets, and writes its manifest. */ | |
| export async function createCanvasDocument( | |
| input: CanvasDocumentCreateInput, | |
| options?: { | |
| stateDir?: string; | |
| workspaceDir?: string; | |
| maxDocumentsPerScope?: number; | |
| }, | |
| ): Promise<CanvasDocumentManifest> { | |
| const workspaceDir = options?.workspaceDir ?? process.cwd(); | |
| const id = input.id?.trim() ? normalizeCanvasDocumentId(input.id) : canvasDocumentId(); | |
| const rootDir = resolveCanvasDocumentDir(id, { stateDir: options?.stateDir }); | |
| await fs.rm(rootDir, { recursive: true, force: true }).catch(() => undefined); | |
| await fs.mkdir(rootDir, { recursive: true }); | |
| const root = await fsRoot(rootDir); | |
| const assets = await copyAssets(root, input.assets, workspaceDir); | |
| const entry = await materializeEntrypoint(rootDir, root, input, workspaceDir); | |
| const manifest: CanvasDocumentManifest = { | |
| id, | |
| kind: input.kind, | |
| ...(input.title?.trim() ? { title: input.title.trim() } : {}), | |
| ...(typeof input.preferredHeight === "number" | |
| ? { preferredHeight: input.preferredHeight } | |
| : {}), | |
| ...(input.surface ? { surface: input.surface } : {}), | |
| ...(input.retentionScope ? { retentionScope: input.retentionScope } : {}), | |
| ...(input.cspSandbox ? { cspSandbox: input.cspSandbox } : {}), | |
| createdAt: new Date().toISOString(), | |
| entryUrl: entry.entryUrl, | |
| ...(entry.localEntrypoint ? { localEntrypoint: entry.localEntrypoint } : {}), | |
| ...(entry.externalUrl ? { externalUrl: entry.externalUrl } : {}), | |
| assets, | |
| }; | |
| await root.writeJson("manifest.json", manifest, { space: 2 }); | |
| if (input.retentionScope && options?.maxDocumentsPerScope) { | |
| // Bounded transcript widgets cannot grow managed Canvas storage without limit. | |
| await pruneCanvasDocumentsForScope({ | |
| documentsDir: resolveCanvasDocumentsDir(options.stateDir), | |
| retentionScope: input.retentionScope, | |
| maxDocuments: options.maxDocumentsPerScope, | |
| }); | |
| } | |
| return manifest; | |
| } | |