Download server/server.ts from igorktech/pinned: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/spaces/igorktech/pinned/resolve/main/server/server.ts
- Command line
-
hf download hf://spaces/igorktech/pinned/server/server.ts
-
curl -L -o server.ts https://huggingface.co/spaces/igorktech/pinned/resolve/main/server/server.ts
10.7 kB
| import { createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' | |
| import { createServer, type IncomingMessage, type ServerResponse } from 'node:http' | |
| import { extname, join, resolve, sep } from 'node:path' | |
| import type { Duplex } from 'node:stream' | |
| import { pipeline } from 'node:stream/promises' | |
| import { unfurl } from 'unfurl.js' | |
| import { WebSocketServer } from 'ws' | |
| import { handleAcpUpgrade, onAgentPresence } from './acpPipe.ts' | |
| import { handleApi } from './api.ts' | |
| import { createDemoBoard, DEMO_MODE, demoBoards } from './demo.ts' | |
| import { handleCanvasMcp } from './canvasMcp.ts' | |
| import { recordAgentPresence } from './events.ts' | |
| import { publicFetch } from './netGuard.ts' | |
| import { getRoom, HttpError, patchShapes, putShapes, safeId } from './rooms.ts' | |
| // The sync server: one TLSocketRoom per room (rooms.ts), each saved to its own SQLite file under | |
| // DATA_DIR, plus the canvas MCP, the REST and SSE API with its board DB (api.ts), asset uploads on | |
| // local disk, bookmark unfurling, and the built frontend from dist/. | |
| const PORT = Number(process.env.PORT ?? 5858) | |
| const DATA_DIR = resolve(process.env.DATA_DIR ?? '.data') | |
| const UPLOADS_DIR = join(DATA_DIR, 'uploads') | |
| const DIST_DIR = resolve('dist') | |
| mkdirSync(UPLOADS_DIR, { recursive: true }) | |
| onAgentPresence(recordAgentPresence) | |
| function send(res: ServerResponse, status: number, body: string) { | |
| if (!res.headersSent) res.writeHead(status, { 'content-type': 'text/plain' }) | |
| res.end(body) | |
| } | |
| function sendJson(res: ServerResponse, body: unknown) { | |
| res.writeHead(200, { 'content-type': 'application/json' }) | |
| res.end(JSON.stringify(body)) | |
| } | |
| // we only allow image and video uploads. the content type is kept next to the file. | |
| async function handleUpload(uploadId: string, req: IncomingMessage, res: ServerResponse) { | |
| const contentType = req.headers['content-type'] ?? '' | |
| if (!contentType.startsWith('image/') && !contentType.startsWith('video/')) { | |
| return send(res, 400, 'Invalid content type') | |
| } | |
| const file = join(UPLOADS_DIR, safeId(uploadId)) | |
| // a file without its .type marker is a leftover from an upload that never finished | |
| if (existsSync(file) && !existsSync(`${file}.type`)) rmSync(file, { force: true }) | |
| try { | |
| await pipeline(req, createWriteStream(file, { flags: 'wx' })) | |
| } catch (e) { | |
| if ((e as NodeJS.ErrnoException).code === 'EEXIST') return send(res, 409, 'Upload already exists') | |
| rmSync(file, { force: true }) | |
| throw e | |
| } | |
| writeFileSync(`${file}.type`, contentType) | |
| sendJson(res, { ok: true }) | |
| } | |
| // ponytail: no Range support, so videos can't seek before they've fully loaded | |
| async function handleDownload(uploadId: string, res: ServerResponse) { | |
| const file = join(UPLOADS_DIR, safeId(uploadId)) | |
| // the .type file is written last, so this only serves finished uploads | |
| if (!existsSync(`${file}.type`)) return send(res, 404, 'Not found') | |
| res.writeHead(200, { | |
| 'content-type': readFileSync(`${file}.type`, 'utf8'), | |
| // assets are immutable, so we can cache them basically forever: | |
| 'cache-control': 'public, max-age=31536000, immutable', | |
| 'access-control-allow-origin': '*', | |
| // Prevent XSS from user-uploaded SVGs (or any file served with an executable content-type). | |
| 'content-security-policy': "default-src 'none'", | |
| 'x-content-type-options': 'nosniff', | |
| }) | |
| await pipeline(createReadStream(file), res) | |
| } | |
| // bookmarks need to extract metadata from pasted URLs. Every fetch (the page and its oEmbed | |
| // link) goes through publicFetch, so a pasted URL can't make the server call a private address. | |
| const UNFURL_HEADERS = { Accept: 'text/html, application/xhtml+xml', 'User-Agent': 'facebookexternalhit' } | |
| async function handleUnfurl(url: string | null, res: ServerResponse) { | |
| if (!url?.match(/^https?:\/\//)) return send(res, 400, 'Bad URL query parameter.') | |
| try { | |
| const { title, description, favicon, open_graph, twitter_card } = await unfurl(url, { | |
| fetch: (target) => publicFetch(target, { headers: UNFURL_HEADERS }), | |
| }) | |
| const ogImage = open_graph?.images?.[0] | |
| sendJson(res, { | |
| title, | |
| description, | |
| favicon, | |
| image: ogImage?.url ?? twitter_card?.images?.[0]?.url, | |
| imageWidth: ogImage?.width, | |
| imageHeight: ogImage?.height, | |
| }) | |
| } catch { | |
| send(res, 422, 'Failed to fetch URL.') | |
| } | |
| } | |
| const MIME: Record<string, string> = { | |
| '.html': 'text/html', | |
| '.js': 'text/javascript', | |
| '.css': 'text/css', | |
| '.json': 'application/json', | |
| '.svg': 'image/svg+xml', | |
| '.png': 'image/png', | |
| '.ico': 'image/x-icon', | |
| '.woff2': 'font/woff2', | |
| } | |
| // the built frontend; unknown paths get index.html so client-side routes like /:roomId work | |
| async function handleStatic(pathname: string, res: ServerResponse) { | |
| let file = join(DIST_DIR, decodeURIComponent(pathname)) | |
| if (!file.startsWith(DIST_DIR + sep) || !existsSync(file) || statSync(file).isDirectory()) { | |
| file = join(DIST_DIR, 'index.html') | |
| } | |
| if (!existsSync(file)) return send(res, 404, 'No frontend build found. Run `pnpm build` first.') | |
| res.writeHead(200, { 'content-type': MIME[extname(file)] ?? 'application/octet-stream' }) | |
| await pipeline(createReadStream(file), res) | |
| } | |
| // Bad input is the client's fault (400 with the reason); anything else is ours (500, logged). | |
| async function handleShapes(roomId: string, req: IncomingMessage, res: ServerResponse) { | |
| try { | |
| const body = JSON.parse(Buffer.concat(await req.toArray()).toString()) | |
| if (!Array.isArray(body)) throw new HttpError(400, 'Expected a JSON array of shapes and bindings') | |
| return sendJson(res, { ids: req.method === 'PATCH' ? patchShapes(roomId, body) : putShapes(roomId, body) }) | |
| } catch (e) { | |
| if (e instanceof HttpError) return send(res, e.status, e.message) | |
| if (e instanceof SyntaxError || (e instanceof Error && e.name === 'ValidationError')) { | |
| return send(res, 400, String(e)) | |
| } | |
| console.error(e) | |
| return send(res, 500, 'Internal error') | |
| } | |
| } | |
| function parseUrl(raw: string | undefined) { | |
| try { | |
| return new URL(raw ?? '/', 'http://localhost') | |
| } catch { | |
| return null | |
| } | |
| } | |
| // the pitch video plays in-app in demo mode; the client only shows it when the file is there | |
| function demoVideoUrl() { | |
| const file = process.env.DEMO_VIDEO | |
| return DEMO_MODE && file && existsSync(file) ? '/api/demo/video' : null | |
| } | |
| // Safari won't play an mp4 it can't seek, so Range requests are required, not optional. | |
| async function handleDemoVideo(req: IncomingMessage, res: ServerResponse) { | |
| const file = process.env.DEMO_VIDEO | |
| if (!file || !existsSync(file)) return send(res, 404, 'Not found') | |
| const size = statSync(file).size | |
| const headers = { 'content-type': 'video/mp4', 'accept-ranges': 'bytes' } | |
| const range = req.headers.range?.match(/^bytes=(\d*)-(\d*)$/) | |
| if (!range) { | |
| res.writeHead(200, { ...headers, 'content-length': size }) | |
| return await pipeline(createReadStream(file), res) | |
| } | |
| const [, from, to] = range | |
| const start = from === '' ? Math.max(0, size - Number(to)) : Number(from) | |
| const end = from === '' || to === '' ? size - 1 : Math.min(Number(to), size - 1) | |
| if (from === '' && to === '' || start > end || start >= size) { | |
| res.writeHead(416, { ...headers, 'content-range': `bytes */${size}` }) | |
| return res.end() | |
| } | |
| res.writeHead(206, { ...headers, 'content-length': end - start + 1, 'content-range': `bytes ${start}-${end}/${size}` }) | |
| await pipeline(createReadStream(file, { start, end }), res) | |
| } | |
| const server = createServer(async (req, res) => { | |
| try { | |
| const url = parseUrl(req.url) | |
| if (!url) return send(res, 400, 'Bad request') | |
| const path = url.pathname | |
| let m: RegExpMatchArray | null | |
| if (req.method === 'GET' && path === '/api/config') { | |
| return sendJson(res, { demo: DEMO_MODE, videoUrl: demoVideoUrl(), boards: DEMO_MODE ? demoBoards() : [] }) | |
| } | |
| if (DEMO_MODE) { | |
| if (req.method === 'GET' && path === '/api/demo/new') { | |
| res.writeHead(302, { location: `/${createDemoBoard(url.searchParams.get('board'))}`, 'cache-control': 'no-store' }) | |
| return res.end() | |
| } | |
| if (req.method === 'GET' && path === '/api/demo/video') return await handleDemoVideo(req, res) | |
| // the Start/new-board flow needs the live agent; in demo visitors go to the demo Home | |
| if (req.method === 'GET' && (path === '/landing/index.html' || path === '/new')) { | |
| res.writeHead(302, { location: '/landing/home.html', 'cache-control': 'no-store' }) | |
| return res.end() | |
| } | |
| // a public demo can't host anonymous files | |
| if (req.method === 'POST' && path.match(/^\/api\/uploads\/([^/]+)$/)) return send(res, 403, 'Uploads are off in the demo') | |
| } | |
| if ((m = path.match(/^\/api\/uploads\/([^/]+)$/))) { | |
| if (req.method === 'POST') return await handleUpload(m[1], req, res) | |
| if (req.method === 'GET') return await handleDownload(m[1], res) | |
| } | |
| if (req.method === 'GET' && path === '/api/unfurl') { | |
| return await handleUnfurl(url.searchParams.get('url'), res) | |
| } | |
| if ((req.method === 'POST' || req.method === 'PATCH') && (m = path.match(/^\/api\/rooms\/([^/]+)\/shapes$/))) { | |
| return await handleShapes(m[1], req, res) | |
| } | |
| if ((m = path.match(/^\/api\/mcp\/canvas\/([^/]+)$/))) return await handleCanvasMcp(req, res, m[1]) | |
| if (path.startsWith('/api/v1/')) return await handleApi(req, res, url) | |
| if (path.startsWith('/api/')) return send(res, 404, 'Not found') | |
| await handleStatic(path, res) | |
| } catch (e) { | |
| console.error(e) | |
| send(res, 500, 'Internal error') | |
| } | |
| }) | |
| function rejectUpgrade(socket: Duplex, status: number, reason: string) { | |
| socket.write(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`) | |
| socket.destroy() | |
| } | |
| // realtime sync: the client opens a websocket at /api/connect/:roomId?sessionId=... | |
| // Nothing here may throw: an uncaught error would take down every room on the server. | |
| const wss = new WebSocketServer({ noServer: true }) | |
| server.on('upgrade', (req, socket, head) => { | |
| try { | |
| const url = parseUrl(req.url) | |
| if (url?.pathname === '/api/acp') { | |
| // demo mode never spawns a harness | |
| if (DEMO_MODE) return rejectUpgrade(socket, 503, 'Service Unavailable') | |
| return handleAcpUpgrade(req, socket, head) | |
| } | |
| const roomId = url?.pathname.match(/^\/api\/connect\/([^/]+)$/)?.[1] | |
| const sessionId = url?.searchParams.get('sessionId') | |
| if (!roomId || !sessionId) return rejectUpgrade(socket, 400, 'Bad Request') | |
| wss.handleUpgrade(req, socket, head, (ws) => { | |
| try { | |
| getRoom(roomId).handleSocketConnect({ sessionId, socket: ws }) | |
| } catch (e) { | |
| console.error(`Room ${safeId(roomId)} could not be opened:`, e) | |
| ws.close(1011, 'Room unavailable') | |
| } | |
| }) | |
| } catch (e) { | |
| console.error('Bad websocket upgrade:', e) | |
| rejectUpgrade(socket, 400, 'Bad Request') | |
| } | |
| }) | |
| server.listen(PORT, () => console.log(`sync server on http://localhost:${PORT}, data in ${DATA_DIR}`)) | |