@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.
67 lines (66 loc) • 2.33 kB
JavaScript
import { signDataLink as signMigratedDataLink } from '../migrate/datalink/index.js';
/** Widget-side fetch name. Not listed on tools/list — the model sees only show_workspace. */
export const CALL_SERVER_TOOL = 'read_workspace_data';
const HANDLE_ID = /^[A-Za-z0-9_-]{1,64}$/;
const HANDLE_QUERY_KEYS = new Set(['sig', 'exp']);
/**
* Put a handle on the spec. The payload is signed, never copied onto the spec or summary.
* dataUrl must be an opaque `/v1/data-links/:id` URL (optional `sig` / `exp` only).
*/
export async function attachDataHandle(spec, payload, signDataLink = signMigratedDataLink) {
const link = await signDataLink(payload);
if (!link || typeof link.dataUrl !== 'string' || !link.dataUrl.trim()) {
return {
ok: false,
code: 'empty_dataUrl',
reason: 'signDataLink did not return a dataUrl.',
};
}
const dataUrl = link.dataUrl.trim();
if (!isOpaqueDataUrl(dataUrl)) {
return {
ok: false,
code: 'leaky_handle',
reason: 'dataUrl must be an opaque /v1/data-links/:id handle; it must not carry dataset rows.',
};
}
return {
ok: true,
spec: {
...spec,
dataUrl,
callServerTool: CALL_SERVER_TOOL,
},
};
}
/** Widget path: rows come back only by resolving the handle. */
export async function readViaHandle(spec, readDataLink) {
if (!spec.dataUrl) {
throw new Error('spec has no data handle');
}
return readDataLink({ dataUrl: spec.dataUrl });
}
/** Allowlist: http(s) `/v1/data-links/:id` with optional HMAC query keys only. */
export function isOpaqueDataUrl(dataUrl) {
let url;
try {
url = new URL(dataUrl);
}
catch {
return false;
}
if (url.protocol !== 'http:' && url.protocol !== 'https:')
return false;
if (url.username || url.password || url.hash)
return false;
const parts = url.pathname.split('/').filter((part) => part.length > 0);
if (parts.length !== 3 || parts[0] !== 'v1' || parts[1] !== 'data-links')
return false;
if (!HANDLE_ID.test(parts[2]))
return false;
for (const key of url.searchParams.keys()) {
if (!HANDLE_QUERY_KEYS.has(key))
return false;
}
return true;
}