UNPKG

@fusedashlabs/ui9000-workspace

Version:

UI9000 workspace MCP. One tool, show_workspace, draws a chart from your table. Set TYPESAFE_API_KEY to let Jev choose the chart.

167 lines (166 loc) • 5.32 kB
/** * Filesystem TTL store — copied from mcp-ui `src/utils/dataLinkStore.ts`. * * Same role as mcp-ui's `.data/`: one JSON file per link id, each wrapping the * payload in a `{ value, exp }` envelope so an expired file is dropped on the * next touch rather than lingering. Reads and writes are sync on purpose — the * store is small and the signing path must not interleave. * * Deviation from mcp-ui: the stored record carries an opaque `payload` instead * of mcp-ui's `{ config, data }` split, because this package's contract * (`SignDataLink = (payload: unknown) => SignedLink`) signs arbitrary payloads. * The envelope, file layout, and expiry mechanics are unchanged. */ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; /** `packages/core` — the root a relative STORAGE_DIR resolves against. */ function packageRoot() { return fileURLToPath(new URL('../../../', import.meta.url)); } /** * Resolve the on-disk TTL store directory. * Cursor/Claude stdio often have cwd=$HOME, so `path.resolve(cwd, '.data')` * would write to ~/.data while the server reads the package `.data` — the * links then 404. Default: `<packageRoot>/.data` unless STORAGE_DIR is absolute. */ export function resolveDataLinkStorageDir(env = process.env, root = packageRoot()) { const raw = (env.STORAGE_DIR || '.data').trim() || '.data'; if (path.isAbsolute(raw)) return raw; return path.resolve(root, raw); } export const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; export class TtlStore { defaultTtlMs; dataDir; constructor(defaultTtlMs = DEFAULT_TTL_MS, dataDir) { this.defaultTtlMs = defaultTtlMs; this.dataDir = dataDir ?? resolveDataLinkStorageDir(); this.ensureDir(); } ensureDir() { if (!fs.existsSync(this.dataDir)) { fs.mkdirSync(this.dataDir, { recursive: true }); } } filePathFor(id) { return path.join(this.dataDir, `${id}.json`); } set(id, payload, customTtlMs) { const ttlMs = customTtlMs || this.defaultTtlMs; const now = Date.now(); const createdAt = new Date(now).toISOString(); const value = { id, createdAt, ttlSeconds: Math.floor(ttlMs / 1000), payload, }; const envelope = { value, exp: now + ttlMs }; // Fail closed: a signed URL with no file on disk is a 404. mcp-ui swallowed // this write; we do not — signDataLink must not return a dead handle. this.ensureDir(); fs.writeFileSync(this.filePathFor(id), JSON.stringify(envelope), 'utf8'); this.cleanup(); } get(id) { this.cleanup(); const file = this.filePathFor(id); if (!fs.existsSync(file)) return null; try { const raw = fs.readFileSync(file, 'utf8'); const parsed = JSON.parse(raw); if (Date.now() > parsed.exp) { this.unlinkQuietly(file); return null; } return parsed.value; } catch (err) { console.error('Failed to read data link:', err); return null; } } has(id) { return this.get(id) !== null; } delete(id) { const file = this.filePathFor(id); if (!fs.existsSync(file)) return false; try { fs.unlinkSync(file); return true; } catch { return false; } } size() { this.cleanup(); return this.linkFiles().length; } /** Drop every link. Used by tests and by store teardown. */ destroy() { for (const file of this.linkFiles()) { this.unlinkQuietly(file); } } linkFiles() { try { if (!fs.existsSync(this.dataDir)) return []; return fs .readdirSync(this.dataDir) .filter((f) => f.endsWith('.json')) .map((f) => path.join(this.dataDir, f)); } catch { return []; } } unlinkQuietly(file) { try { fs.unlinkSync(file); } catch { // A concurrent reader already removed it — expiry is idempotent. } } /** Sweep expired (and unreadable) files so the directory cannot grow forever. */ cleanup() { const now = Date.now(); for (const file of this.linkFiles()) { try { const parsed = JSON.parse(fs.readFileSync(file, 'utf8')); if (now > parsed.exp) this.unlinkQuietly(file); } catch { // Corrupt or truncated file: it can never be served, so drop it. this.unlinkQuietly(file); } } } } let store = null; export function getDataLinkStore() { if (!store) { store = new TtlStore(); } return store; } /** Test-only: drop the singleton so STORAGE_DIR can be re-read. */ export function resetDataLinkStoreForTests() { if (store) { try { store.destroy(); } catch { // Teardown is best-effort. } } store = null; }