@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.
65 lines (64 loc) • 2.54 kB
JavaScript
/**
* HMAC-SHA256 data-link signatures — copied from mcp-ui `src/utils/signature.ts`.
*
* The algorithm is load-bearing for interop: a link signed by mcp-ui must verify
* here and vice versa. Do not "improve" the canonical string (`${id}.${exp}`),
* the base64url digest encoding, or the epoch-seconds `exp`. Changing any of
* them silently invalidates every link the other side issued.
*
* mcp-ui's exported `signDataLink` (id/exp/secret -> digest) is named
* `computeDataLinkSignature` here so it does not collide with this package's
* public `signDataLink(payload) -> SignedLink`. The body is unchanged.
*/
import { createHmac, timingSafeEqual } from 'node:crypto';
/** mcp-ui `signDataLink`: base64url HMAC-SHA256 over `${id}.${exp}`. */
export function computeDataLinkSignature({ id, exp, secret }) {
const payload = `${id}.${exp}`;
const hmac = createHmac('sha256', secret);
hmac.update(payload);
return hmac.digest('base64url');
}
/** Constant-time compare, and expired links never verify. */
export function verifyDataLinkSignature(id, exp, sig, secret) {
if (Date.now() / 1000 > exp) {
return false;
}
const expectedSig = computeDataLinkSignature({ id, exp, secret });
const expectedBuffer = Buffer.from(expectedSig, 'base64url');
const providedBuffer = Buffer.from(sig, 'base64url');
if (expectedBuffer.length !== providedBuffer.length) {
return false;
}
return timingSafeEqual(expectedBuffer, providedBuffer);
}
/** Opaque handle shape: `/v1/data-links/:id?sig=&exp=` and nothing else. */
export function createSignedDataUrl(baseUrl, id, exp, secret) {
const sig = computeDataLinkSignature({ id, exp, secret });
return `${baseUrl}/v1/data-links/${id}?sig=${sig}&exp=${exp}`;
}
export function parseSignedUrlParams(url) {
try {
const urlObj = new URL(url);
const pathParts = urlObj.pathname.split('/');
const id = pathParts[pathParts.length - 1];
const sig = urlObj.searchParams.get('sig');
const expStr = urlObj.searchParams.get('exp');
if (!id || !sig || !expStr) {
return null;
}
const exp = parseInt(expStr, 10);
if (isNaN(exp)) {
return null;
}
return { id, sig, exp };
}
catch {
return null;
}
}
export function generateExpiration(hoursFromNow = 24) {
return Math.floor(Date.now() / 1000) + hoursFromNow * 60 * 60;
}
export function isExpired(timestamp) {
return Date.now() / 1000 > timestamp;
}