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.

132 lines (131 loc) • 4.95 kB
/** * Data-link secret resolution — copied from mcp-ui `src/utils/dataLinkSecret.ts`. * * The secret comes from the environment, never from source. Production-like * environments fail closed when it is missing, a known dev default, or too * short. A local install with no secret mints a random one and keeps it next * to the data-link store, so the published package never signs with a constant * an attacker can read out of npm. */ import { randomBytes } from 'node:crypto'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { resolveDataLinkStorageDir } from './store.js'; /** Known unsafe defaults that must never ship in production (FUS-3988 / MVP-P04). */ export const DEV_DATA_LINK_SECRETS = new Set([ '', 'dev-secret-key-for-testing', 'secret', 'changeme', 'change-me', 'password', ]); export const DATA_LINK_SECRET_ENV = 'MCP_DATA_LINK_SECRET'; export function isProductionLikeEnv(env = process.env) { const nodeEnv = (env.NODE_ENV || '').trim().toLowerCase(); const mcpEnv = (env.MCP_ENV || env.APP_ENV || '').trim().toLowerCase(); if (nodeEnv === 'production' || nodeEnv === 'prod') return true; if (mcpEnv === 'production' || mcpEnv === 'prod' || mcpEnv === 'staging') return true; // Docker/prod env files often omit NODE_ENV; treat ENV_FILE=env.prod as production-like. const envFile = (env.ENV_FILE || '').trim().toLowerCase(); if (envFile.includes('prod') || envFile.includes('staging')) return true; return false; } export function resolveDataLinkSecret(env = process.env) { return (env[DATA_LINK_SECRET_ENV] || '').trim(); } export function isUnsafeDataLinkSecret(secret) { return DEV_DATA_LINK_SECRETS.has(secret.trim()); } /** * Fail closed in production-like envs when the secret is missing or a known * default. Local/dev mints a per-install secret when the env value is missing * or unsafe. */ export function assertSafeDataLinkSecret(env = process.env) { const secret = resolveDataLinkSecret(env); const productionLike = isProductionLikeEnv(env); if (!productionLike) { if (secret && !isUnsafeDataLinkSecret(secret) && secret.length >= 16) { return { ok: true, secret }; } return { ok: true, secret: installSecret(env) }; } if (!secret) { return { ok: false, reason: `${DATA_LINK_SECRET_ENV} is empty in a production-like environment. Set a strong secret before serving data-links.`, }; } if (isUnsafeDataLinkSecret(secret)) { return { ok: false, reason: `${DATA_LINK_SECRET_ENV} matches a known development default. Rotate it in the secrets manager / host env (do not commit the new value).`, }; } if (secret.length < 16) { return { ok: false, reason: `${DATA_LINK_SECRET_ENV} is shorter than 16 characters. Use a longer random secret in production.`, }; } return { ok: true, secret }; } const INSTALL_SECRET_NAME = 'data-link.secret'; /** * One random secret per storage directory. Created on first use, mode 0600, * reused so sign and read agree across restarts of the same install. */ function installSecret(env) { const file = path.join(resolveDataLinkStorageDir(env), INSTALL_SECRET_NAME); if (fs.existsSync(file)) { let existing = ''; try { existing = fs.readFileSync(file, 'utf8').trim(); } catch { throw new Error(`Refusing to replace an unreadable data-link secret (${INSTALL_SECRET_NAME}).`); } if (existing.length >= 32 && !isUnsafeDataLinkSecret(existing)) return existing; } const secret = randomBytes(32).toString('base64url'); fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 }); const flag = fs.existsSync(file) ? 'w' : 'wx'; try { fs.writeFileSync(file, `${secret}\n`, { mode: 0o600, flag }); } catch (error) { if (error.code === 'EEXIST') return installSecret(env); throw error; } assertSecretFileMode(file); return secret; } function assertSecretFileMode(file) { if (process.platform === 'win32') return; try { fs.chmodSync(file, 0o600); const groupOrOther = fs.statSync(file).mode & 0o077; if (groupOrOther === 0) return; } catch { // Fall through and delete. } fs.rmSync(file, { force: true }); throw new Error('Refusing to keep a data-link secret that is not owner-readable only.'); } /** Throwing wrapper for the signing path — never sign with an unsafe secret. */ export function requireDataLinkSecret(env = process.env) { const result = assertSafeDataLinkSecret(env); if (!result.ok) { throw new Error(`Refusing to sign a data-link: ${result.reason}`); } return result.secret; }