@databrainhq/mcp-server 0.3.0-rc.10 → 0.3.0-rc.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/dist/apps/server.d.ts +113 -47
- package/dist/apps/server.js +5 -4
- package/dist/apps/stdio.js +6 -5
- package/dist/apps/stdio.js.map +1 -1
- package/dist/bundle-TDQXWIPM.js +24 -0
- package/dist/bundle-TDQXWIPM.js.map +1 -0
- package/dist/chunk-347U6GXR.js +884 -0
- package/dist/chunk-347U6GXR.js.map +1 -0
- package/dist/{chunk-N7X762VU.js → chunk-LACJHQYH.js} +4 -4
- package/dist/{chunk-ERTAKB2H.js → chunk-OLOC2MTA.js} +960 -683
- package/dist/chunk-OLOC2MTA.js.map +1 -0
- package/dist/{chunk-OCBTNAAO.js → chunk-Q5EYDKSA.js} +550 -1272
- package/dist/chunk-Q5EYDKSA.js.map +1 -0
- package/dist/{chunk-CUPL7CAN.js → chunk-V3UW3B6U.js} +2 -2
- package/dist/{chunk-5IGP6A6T.js → chunk-ZTHGF27R.js} +225 -34
- package/dist/chunk-ZTHGF27R.js.map +1 -0
- package/dist/cli.js +6 -4
- package/dist/cli.js.map +1 -1
- package/dist/custom-chart/custom-runtime.js +63 -0
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/{main-KZRHWE3K.js → main-IUV24DMP.js} +47 -24
- package/dist/main-IUV24DMP.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-5IGP6A6T.js.map +0 -1
- package/dist/chunk-ERTAKB2H.js.map +0 -1
- package/dist/chunk-OCBTNAAO.js.map +0 -1
- package/dist/main-KZRHWE3K.js.map +0 -1
- /package/dist/{chunk-N7X762VU.js.map → chunk-LACJHQYH.js.map} +0 -0
- /package/dist/{chunk-CUPL7CAN.js.map → chunk-V3UW3B6U.js.map} +0 -0
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/index.ts","../src/cli/dev-server.ts"],"sourcesContent":["import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';\nimport { basename, join, resolve } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport { DatabrainClient } from '../client/databrain-client.js';\nimport { buildProject, validateProject } from '../custom-chart/bundle.js';\nimport { apiProblems, createCustomChartsApi, planUpload } from '../custom-chart/api.js';\nimport { scaffoldProject } from '../custom-chart/scaffold.js';\nimport type { Problem } from '../custom-chart/types.js';\nimport { DatabrainApiError } from '../utils/errors.js';\nimport { redactSecrets, registerSecret } from '../utils/logger.js';\nimport { MCP_SERVER_VERSION } from '../version.js';\nimport { startDevServer } from './dev-server.js';\n\n/**\n * The `databrain` bin (DAT-925). One command family today: custom-chart.\n * Output is for people: short lines, problems as `path code message`,\n * exit code 0 on success and 1 on any problem.\n */\n\nconst USAGE = `databrain ${MCP_SERVER_VERSION}\n\nUsage:\n databrain custom-chart init <dir> [--key <key>] [--name <name>] [--force]\n databrain custom-chart validate [dir]\n databrain custom-chart build [dir]\n databrain custom-chart upload [dir] [--publish] [--yes]\n databrain custom-chart dev [dir] [--port <port>]\n\nCommands:\n init Create a chart project from the template (manifest.json, chart.ts, chart.css, icon.svg, guide).\n validate Check manifest.json, icon.svg and chart.css against the rules. No build.\n build Compile chart.ts with esbuild and write dist/index.js, manifest.json, icon.svg, index.css, bundle.zip.\n upload Build, validate on the server, show the plan, then upload as a private chart or a preview version.\n Needs DATABRAIN_SERVICE_TOKEN (and DATABRAIN_API_URL for self hosted). --publish publishes it.\n dev Serve dist/index.js with sample-data.json in a local sandbox page for a quick look.\n\nOptions:\n --yes Skip the confirmation prompt on upload.\n --publish Publish the uploaded version right away.\n --force Let init write into a folder that is not empty.\n --help Show this help.\n --version Show the version.\n`;\n\ninterface ParsedArgs {\n positional: string[];\n flags: Record<string, string | boolean>;\n}\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n const positional: string[] = [];\n const flags: Record<string, string | boolean> = {};\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg.startsWith('--')) {\n const [name, inlineValue] = arg.slice(2).split('=', 2);\n if (inlineValue !== undefined) {\n flags[name] = inlineValue;\n } else if (['key', 'name', 'port'].includes(name) && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {\n flags[name] = argv[++i];\n } else {\n flags[name] = true;\n }\n } else {\n positional.push(arg);\n }\n }\n return { positional, flags };\n}\n\n// The service token must never reach a terminal or a CI log, wherever it is\n// quoted from (a server error body that echoes the Authorization header, a\n// wrapped fetch error, a problem message). It is registered with the logger\n// in commandUpload; every line printed here is scrubbed the same way.\nconst out = (line: string): void => {\n process.stdout.write(redactSecrets(line) + '\\n');\n};\nconst err = (line: string): void => {\n process.stderr.write(redactSecrets(line) + '\\n');\n};\n\nexport function formatProblemLines(problems: Problem[]): string[] {\n return problems.map((p) => `${p.path || '/'} ${p.code} ${p.message}`);\n}\n\nfunction printProblems(problems: Problem[]): void {\n for (const line of formatProblemLines(problems)) err(line);\n}\n\nfunction keyFromDir(dir: string): string {\n const raw = basename(resolve(dir)).toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');\n const key = raw.length === 0 ? 'my-chart' : /^[a-z]/.test(raw) ? raw : `chart-${raw}`;\n return key.slice(0, 64);\n}\n\nfunction nameFromKey(key: string): string {\n const words = key.split(/[-_]+/).filter(Boolean);\n return words.map((word, index) => (index === 0 ? word[0].toUpperCase() + word.slice(1) : word)).join(' ');\n}\n\nfunction commandInit(args: ParsedArgs): number {\n const dir = args.positional[0];\n if (!dir) {\n err('init needs a folder: databrain custom-chart init <dir>');\n return 1;\n }\n const target = resolve(dir);\n if (existsSync(target) && readdirSync(target).length > 0 && args.flags.force !== true) {\n err(`${target} is not empty. Pick another folder or pass --force.`);\n return 1;\n }\n const key = typeof args.flags.key === 'string' ? args.flags.key : keyFromDir(target);\n const name = typeof args.flags.name === 'string' ? args.flags.name : nameFromKey(key);\n const result = scaffoldProject({ key, name });\n if (!result.ok) {\n printProblems(result.problems);\n return 1;\n }\n mkdirSync(target, { recursive: true });\n for (const [file, content] of Object.entries(result.files)) {\n writeFileSync(join(target, file), content);\n }\n out(`Created chart project ${key} in ${target}`);\n for (const file of Object.keys(result.files)) out(` ${file}`);\n out('');\n out('Next:');\n out(` cd ${dir}`);\n out(' edit chart.ts (read AGENTS.md first)');\n out(' databrain custom-chart validate');\n out(' databrain custom-chart build');\n out(' databrain custom-chart upload');\n return 0;\n}\n\nfunction commandValidate(args: ParsedArgs): number {\n const dir = resolve(args.positional[0] ?? '.');\n const result = validateProject(dir);\n if (!result.ok) {\n err(`${result.problems.length} problem${result.problems.length === 1 ? '' : 's'} in ${dir}:`);\n printProblems(result.problems);\n return 1;\n }\n const manifest = result.project.manifest as { id: string; slots: unknown[]; options: unknown[] };\n out(`Valid: custom:${manifest.id} (${manifest.slots.length} slots, ${manifest.options.length} options) in ${dir}`);\n return 0;\n}\n\nasync function commandBuild(args: ParsedArgs): Promise<number> {\n const dir = resolve(args.positional[0] ?? '.');\n const result = await buildProject(dir);\n if (!result.ok || !result.outputs) {\n err(`Build failed with ${result.problems.length} problem${result.problems.length === 1 ? '' : 's'}:`);\n printProblems(result.problems);\n return 1;\n }\n out(`Built custom:${result.outputs.manifest.id}`);\n for (const path of result.written) out(` ${path}`);\n out(` contentHash ${result.outputs.contentHash}`);\n out(` size ${result.outputs.sizeBytes} bytes (bundle ${Buffer.byteLength(result.outputs.bundleJs, 'utf8')} of 2097152)`);\n return 0;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n if (!process.stdin.isTTY) {\n err('stdin is not a terminal. Pass --yes to upload without a prompt.');\n return false;\n }\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = await new Promise<string>((resolveAnswer) => rl.question(question, resolveAnswer));\n rl.close();\n return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';\n}\n\nasync function commandUpload(args: ParsedArgs): Promise<number> {\n const dir = resolve(args.positional[0] ?? '.');\n const serviceToken = process.env.DATABRAIN_SERVICE_TOKEN;\n if (!serviceToken) {\n err('DATABRAIN_SERVICE_TOKEN is not set. Create one in Settings > Service Tokens and export it, then try again.');\n return 1;\n }\n registerSecret(serviceToken);\n const apiUrl = process.env.DATABRAIN_API_URL || 'https://api.usedatabrain.com';\n if (!process.env.DATABRAIN_API_URL) out(`DATABRAIN_API_URL is not set, using ${apiUrl}`);\n\n const built = await buildProject(dir);\n if (!built.ok || !built.outputs) {\n err('Build failed:');\n printProblems(built.problems);\n return 1;\n }\n const { manifest, bundleJs, bundleCss, iconSvg } = built.outputs;\n const key = manifest.id;\n out(`Built custom:${key} (${built.outputs.contentHash.slice(0, 12)}, ${built.outputs.sizeBytes} bytes)`);\n\n const client = new DatabrainClient({ serviceToken, apiUrl, demoDomain: 'https://demo.usedatabrain.com' });\n const api = createCustomChartsApi(client);\n try {\n const validation = await api.validate({ manifest, bundleJs, bundleCss, iconSvg });\n if (!validation.ok) {\n err('The server refused the bundle:');\n printProblems(validation.problems);\n return 1;\n }\n out('Server validation: ok');\n const existing = (await api.list()).find((chart) => chart.key === key);\n const plan = planUpload(key, existing);\n out(`Plan: ${plan.summary}`);\n if (args.flags.publish === true) out('Then: publish that version (every dashboard using this key will render it).');\n\n if (args.flags.yes !== true) {\n const go = await confirm('Continue? [y/N] ');\n if (!go) {\n out('Cancelled. Nothing was uploaded.');\n return 1;\n }\n }\n\n const uploaded = await api.upload({ manifest, bundleJs, bundleCss, iconSvg });\n const versionNumber = uploaded.version?.versionNumber ?? '?';\n if (uploaded.created === 'none') out(`This exact bundle is already v${versionNumber} of custom:${key}.`);\n else if (uploaded.created === 'chart') out(`Created custom:${key} (v${versionNumber}, private).`);\n else out(`Uploaded v${versionNumber} of custom:${key}. It is in preview until you publish it.`);\n\n if (args.flags.publish === true) {\n const published = await api.publish(key, uploaded.version?.id);\n out(`Published v${versionNumber} of custom:${key} (status ${published.chart?.status ?? 'published'}).`);\n } else if (uploaded.created !== 'none') {\n out(`Publish it with: databrain custom-chart upload --publish, the publish_custom_chart tool, or Settings > Developers > Custom charts.`);\n }\n return 0;\n } catch (error) {\n if (error instanceof DatabrainApiError) {\n err(`Error ${error.status || ''} ${error.code}: ${error.message}`.replace(/\\s+/g, ' ').trim());\n printProblems(apiProblems(error));\n return 1;\n }\n err(`Unexpected error: ${(error as Error).message}`);\n return 1;\n }\n}\n\nasync function commandDev(args: ParsedArgs): Promise<number> {\n const dir = resolve(args.positional[0] ?? '.');\n const port = typeof args.flags.port === 'string' ? Number(args.flags.port) : 4174;\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n err('--port must be a number between 1 and 65535.');\n return 1;\n }\n const built = await buildProject(dir);\n if (!built.ok) {\n err('Build failed:');\n printProblems(built.problems);\n return 1;\n }\n const server = await startDevServer({ dir, port });\n out(`Custom chart dev page: http://127.0.0.1:${server.port}/ (Ctrl+C to stop)`);\n out('The page rebuilds on every reload. Edit chart.ts, save, reload.');\n await new Promise<void>((resolveDone) => {\n const stop = () => {\n server.close();\n resolveDone();\n };\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n });\n return 0;\n}\n\nexport async function runCli(argv: string[]): Promise<number> {\n const args = parseArgs(argv);\n if (args.flags.version === true) {\n out(MCP_SERVER_VERSION);\n return 0;\n }\n const [family, command, ...rest] = args.positional;\n if (args.flags.help === true || !family) {\n out(USAGE);\n return family ? 0 : 1;\n }\n if (family !== 'custom-chart') {\n err(`Unknown command \"${family}\".`);\n out(USAGE);\n return 1;\n }\n const sub: ParsedArgs = { positional: rest, flags: args.flags };\n switch (command) {\n case 'init':\n return commandInit(sub);\n case 'validate':\n return commandValidate(sub);\n case 'build':\n return commandBuild(sub);\n case 'upload':\n return commandUpload(sub);\n case 'dev':\n return commandDev(sub);\n default:\n err(command ? `Unknown custom-chart command \"${command}\".` : 'custom-chart needs a command.');\n out(USAGE);\n return 1;\n }\n}\n\nconst invokedDirectly =\n typeof process.argv[1] === 'string' && /(^|[\\\\/])(cli\\.(js|mjs|ts)|databrain)$/.test(process.argv[1]);\nif (invokedDirectly) {\n runCli(process.argv.slice(2)).then(\n (code) => {\n process.exitCode = code;\n },\n (error: Error) => {\n err(`Unexpected error: ${error.message}`);\n process.exitCode = 1;\n },\n );\n}\n","import { createServer, type Server } from 'node:http';\nimport { buildProject, readProject } from '../custom-chart/bundle.js';\nimport type { CustomChartManifest, CustomChartOption } from '../custom-chart/types.js';\n\n/**\n * `databrain custom-chart dev`: a local page that hosts dist/index.js the way\n * the product does, in a sandboxed frame that only sees the sandbox protocol.\n * It is a tiny copy of the plugin relay, enough to see the chart draw, click\n * and resize with sample-data.json. Like the product host it injects\n * chart.css into the frame; the bundle itself carries no css. It is not the\n * product sandbox: no CSP, no relay frame, no limits. Never point it at real\n * data.\n */\nexport interface DevServerOptions {\n dir: string;\n port: number;\n}\n\nconst LIGHT_THEME = {\n fontFamily: 'Inter, system-ui, sans-serif',\n fontSize: 12,\n textColor: '#1f2937',\n textSecondaryColor: '#6b7280',\n background: '#ffffff',\n surfaceColor: '#f9fafb',\n borderColor: '#e5e7eb',\n borderRadius: 6,\n primaryColor: '#3366cc',\n palette: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#990099', '#0099c6', '#dd4477', '#66aa00'],\n};\n\nconst DARK_THEME = {\n ...LIGHT_THEME,\n textColor: '#f3f4f6',\n textSecondaryColor: '#9ca3af',\n background: '#111827',\n surfaceColor: '#1f2937',\n borderColor: '#374151',\n primaryColor: '#60a5fa',\n palette: ['#60a5fa', '#f87171', '#fbbf24', '#34d399', '#c084fc', '#22d3ee', '#f472b6', '#a3e635'],\n};\n\nfunction defaultOptions(manifest: CustomChartManifest): Record<string, string | number | boolean> {\n const options: Record<string, string | number | boolean> = {};\n for (const option of manifest.options as CustomChartOption[]) options[option.id] = option.default;\n return options;\n}\n\n/** Deterministic sample data from the manifest slots when sample-data.json is absent. */\nfunction generateSampleData(manifest: CustomChartManifest): { columns: unknown[]; rows: unknown[][] } {\n const labels = ['North', 'South', 'East', 'West', 'Central', 'Coast', 'Island', 'Valley'];\n const columns = manifest.slots.map((slot, index) => ({\n id: `${slot.id}_${index}`,\n label: slot.label,\n type: slot.types[0],\n filterable: slot.role === 'dimension',\n slotId: slot.id,\n }));\n const rows = labels.map((label, row) =>\n manifest.slots.map((slot, column) => {\n const type = slot.types[0];\n if (type === 'string') return `${label}${column > 0 ? ` ${column}` : ''}`;\n if (type === 'boolean') return (row + column) % 2 === 0;\n return Math.round(1000 - row * 97 + column * 53 + ((row * 31 + column * 17) % 90));\n }),\n );\n return { columns, rows };\n}\n\nconst PAGE = `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Custom chart dev</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin: 0; font: 14px system-ui, sans-serif; display: grid; grid-template-rows: auto 1fr; height: 100vh; }\n header { display: flex; gap: 12px; align-items: center; padding: 10px 16px; border-bottom: 1px solid #8884; }\n header code { font-size: 12px; opacity: 0.8; }\n main { display: grid; grid-template-columns: 1fr 320px; min-height: 0; }\n #stage { padding: 16px; display: flex; align-items: flex-start; justify-content: center; overflow: auto; }\n #frame { border: 1px solid #8886; border-radius: 8px; background: transparent; }\n aside { border-left: 1px solid #8884; padding: 12px 16px; overflow: auto; }\n aside h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; margin: 0 0 8px; opacity: 0.7; }\n #log { font: 12px ui-monospace, monospace; white-space: pre-wrap; margin: 0; }\n select, button { font: inherit; }\n</style>\n</head>\n<body>\n<header>\n <strong id=\"title\">Custom chart</strong>\n <code id=\"key\"></code>\n <label>Theme <select id=\"theme\"><option value=\"light\">light</option><option value=\"dark\">dark</option></select></label>\n <label>Size <select id=\"size\"><option value=\"600x320\">600 x 320</option><option value=\"900x420\">900 x 420</option><option value=\"320x240\">320 x 240</option></select></label>\n <button id=\"reload\">Rebuild and reload</button>\n</header>\n<main>\n <div id=\"stage\"><iframe id=\"frame\" sandbox=\"allow-scripts\" width=\"600\" height=\"320\" title=\"Custom chart\"></iframe></div>\n <aside><h2>Events</h2><pre id=\"log\">Waiting for ready...</pre></aside>\n</main>\n<script>\n(async () => {\n const log = document.getElementById('log');\n const frame = document.getElementById('frame');\n const themeSelect = document.getElementById('theme');\n const sizeSelect = document.getElementById('size');\n const say = (line) => { log.textContent = (line + '\\\\n' + log.textContent).slice(0, 8000); };\n const [manifest, bundle, sample, themes, css] = await Promise.all([\n fetch('/manifest.json').then((r) => r.json()),\n fetch('/bundle.js').then((r) => r.ok ? r.text() : r.text().then((t) => { throw new Error(t); })).catch((e) => { say('BUILD ERROR\\\\n' + e.message); return null; }),\n fetch('/sample-data.json').then((r) => r.json()),\n fetch('/themes.json').then((r) => r.json()),\n fetch('/index.css').then((r) => r.text()),\n ]);\n document.getElementById('title').textContent = manifest.name;\n document.getElementById('key').textContent = 'custom:' + manifest.id;\n if (!bundle) return;\n let revision = 0;\n const data = () => ({\n columns: sample.columns, rows: sample.rows,\n options: Object.assign({}, sample.defaults, sample.options || {}),\n theme: themes[themeSelect.value], language: 'en'\n });\n const send = () => frame.contentWindow.postMessage({ protocol: 'dbn-chart-sandbox', version: 1, channel: 'dev', type: 'data', revision: ++revision, data: data() }, '*');\n addEventListener('message', (event) => {\n if (event.source !== frame.contentWindow) return;\n const m = event.data || {};\n if (m.type === 'ready') { say('ready'); send(); return; }\n if (m.type === 'resize' && Number.isFinite(m.height)) frame.height = Math.max(80, Math.min(1200, m.height));\n say(m.type + ' ' + JSON.stringify(m, (k, v) => (k === 'protocol' || k === 'version' || k === 'channel' || k === 'type') ? undefined : v));\n });\n const boot = '(' + (function () {\n var channel = 'dev'; var revision = -1; var latest = null; var listener = null;\n var send = function (payload) { parent.postMessage(Object.assign({ protocol: 'dbn-chart-sandbox', version: 1, channel: channel }, payload), '*'); };\n window.dbnSandbox = Object.freeze({\n onData: function (cb) { listener = cb; send({ type: 'ready' }); if (latest) cb(latest); },\n click: function (rowIndex, columnId) { send({ type: 'click', revision: revision, rowIndex: rowIndex, columnId: columnId }); },\n filter: function (columnId, values) { send({ type: 'filter', revision: revision, columnId: columnId, values: values }); },\n resize: function (height) { send({ type: 'resize', height: height }); }\n });\n addEventListener('error', function (e) { send({ type: 'error', code: 'runtime', message: String(e.message || e) }); });\n addEventListener('message', function (event) {\n var m = event.data; if (!m || m.type !== 'data' || m.revision <= revision) return;\n revision = m.revision; latest = m.data; if (listener) listener(latest);\n });\n }).toString() + ')();';\n const child = '<!doctype html><html><head><meta charset=\"utf-8\"><style>html,body{margin:0;width:100%;height:100%;overflow:hidden}body{font:14px system-ui}#chart{width:100%;height:100%}</style><style>' + css.replace(/<\\\\/style/gi, '') + '</style></head><body><div id=\"chart\"></div><script>' + boot + '<\\\\/script><script>' + bundle.replace(/<\\\\/script/gi, '<\\\\\\\\/script') + '<\\\\/script></body></html>';\n const mount = () => { frame.srcdoc = child; };\n themeSelect.addEventListener('change', () => { if (revision > 0) send(); });\n sizeSelect.addEventListener('change', () => { const [w, h] = sizeSelect.value.split('x'); frame.width = w; frame.height = h; if (revision > 0) send(); });\n document.getElementById('reload').addEventListener('click', () => location.reload());\n mount();\n})();\n</script>\n</body>\n</html>\n`;\n\nexport async function startDevServer(options: DevServerOptions): Promise<{ port: number; close: () => void; server: Server }> {\n const server = createServer(async (req, res) => {\n const url = new URL(req.url ?? '/', 'http://127.0.0.1');\n const json = (status: number, body: unknown) => {\n res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(JSON.stringify(body));\n };\n try {\n if (url.pathname === '/') {\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(PAGE);\n return;\n }\n if (url.pathname === '/bundle.js') {\n const result = await buildProject(options.dir, { write: false });\n if (!result.ok || !result.outputs) {\n res.writeHead(422, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(result.problems.map((p) => `${p.path || '/'} ${p.code} ${p.message}`).join('\\n'));\n return;\n }\n res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(result.outputs.bundleJs);\n return;\n }\n const project = readProject(options.dir);\n const manifest = project.manifest as CustomChartManifest | undefined;\n if (url.pathname === '/manifest.json') {\n json(manifest ? 200 : 422, manifest ?? { problems: project.problems });\n return;\n }\n if (url.pathname === '/sample-data.json') {\n if (!manifest) {\n json(422, { problems: project.problems });\n return;\n }\n const sample = (project.sampleData as { columns?: unknown[]; rows?: unknown[][]; options?: unknown } | undefined) ?? generateSampleData(manifest);\n json(200, { ...sample, defaults: defaultOptions(manifest) });\n return;\n }\n if (url.pathname === '/index.css') {\n // The host injects bundleCss into the frame; this page plays the host.\n res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(project.css ?? '');\n return;\n }\n if (url.pathname === '/themes.json') {\n json(200, { light: LIGHT_THEME, dark: DARK_THEME });\n return;\n }\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('not found');\n } catch (err) {\n res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end((err as Error).message);\n }\n });\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, '127.0.0.1', () => resolve());\n });\n const address = server.address();\n const port = typeof address === 'object' && address ? address.port : options.port;\n return { port, server, close: () => server.close() };\n}\n"],"mappings":"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","names":["err","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli/index.ts","../src/cli/dev-server.ts"],"sourcesContent":["import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';\nimport { basename, join, resolve } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport { DatabrainClient } from '../client/databrain-client.js';\nimport { buildProject, validateProject } from '../custom-chart/bundle.js';\nimport { apiProblems, createCustomChartsApi, planUpload } from '../custom-chart/api.js';\nimport { scaffoldProject } from '../custom-chart/scaffold.js';\nimport type { Problem } from '../custom-chart/types.js';\nimport { DatabrainApiError } from '../utils/errors.js';\nimport { redactSecrets, registerSecret } from '../utils/logger.js';\nimport { MCP_SERVER_VERSION } from '../version.js';\nimport { startDevServer } from './dev-server.js';\n\n/**\n * The `databrain` bin (DAT-925). One command family today: custom-chart.\n * Output is for people: short lines, problems as `path code message`,\n * exit code 0 on success and 1 on any problem.\n */\n\nconst USAGE = `databrain ${MCP_SERVER_VERSION}\n\nUsage:\n databrain custom-chart init <dir> [--key <key>] [--name <name>] [--force]\n databrain custom-chart validate [dir]\n databrain custom-chart build [dir]\n databrain custom-chart upload [dir] [--publish] [--yes]\n databrain custom-chart dev [dir] [--port <port>]\n\nCommands:\n init Create a chart project from the template (manifest.json, chart.ts, chart.css, icon.svg, guide).\n validate Check manifest.json, icon.svg and chart.css against the rules. No build.\n build Compile chart.ts with esbuild and write dist/index.js, manifest.json, icon.svg, index.css, bundle.zip.\n upload Build, validate on the server, show the plan, then upload as a private chart or a preview version.\n Needs DATABRAIN_SERVICE_TOKEN (and DATABRAIN_API_URL for self hosted). --publish publishes it.\n dev Serve dist/index.js with sample-data.json in a local sandbox page for a quick look.\n\nOptions:\n --yes Skip the confirmation prompt on upload.\n --publish Publish the uploaded version right away.\n --force Let init write into a folder that is not empty.\n --help Show this help.\n --version Show the version.\n`;\n\ninterface ParsedArgs {\n positional: string[];\n flags: Record<string, string | boolean>;\n}\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n const positional: string[] = [];\n const flags: Record<string, string | boolean> = {};\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg.startsWith('--')) {\n const [name, inlineValue] = arg.slice(2).split('=', 2);\n if (inlineValue !== undefined) {\n flags[name] = inlineValue;\n } else if (['key', 'name', 'port'].includes(name) && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {\n flags[name] = argv[++i];\n } else {\n flags[name] = true;\n }\n } else {\n positional.push(arg);\n }\n }\n return { positional, flags };\n}\n\n// The service token must never reach a terminal or a CI log, wherever it is\n// quoted from (a server error body that echoes the Authorization header, a\n// wrapped fetch error, a problem message). It is registered with the logger\n// in commandUpload; every line printed here is scrubbed the same way.\nconst out = (line: string): void => {\n process.stdout.write(redactSecrets(line) + '\\n');\n};\nconst err = (line: string): void => {\n process.stderr.write(redactSecrets(line) + '\\n');\n};\n\nexport function formatProblemLines(problems: Problem[]): string[] {\n return problems.map((p) => `${p.path || '/'} ${p.code} ${p.message}`);\n}\n\nfunction printProblems(problems: Problem[]): void {\n for (const line of formatProblemLines(problems)) err(line);\n}\n\nfunction keyFromDir(dir: string): string {\n const raw = basename(resolve(dir)).toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');\n const key = raw.length === 0 ? 'my-chart' : /^[a-z]/.test(raw) ? raw : `chart-${raw}`;\n return key.slice(0, 64);\n}\n\nfunction nameFromKey(key: string): string {\n const words = key.split(/[-_]+/).filter(Boolean);\n return words.map((word, index) => (index === 0 ? word[0].toUpperCase() + word.slice(1) : word)).join(' ');\n}\n\nfunction commandInit(args: ParsedArgs): number {\n const dir = args.positional[0];\n if (!dir) {\n err('init needs a folder: databrain custom-chart init <dir>');\n return 1;\n }\n const target = resolve(dir);\n if (existsSync(target) && readdirSync(target).length > 0 && args.flags.force !== true) {\n err(`${target} is not empty. Pick another folder or pass --force.`);\n return 1;\n }\n const key = typeof args.flags.key === 'string' ? args.flags.key : keyFromDir(target);\n const name = typeof args.flags.name === 'string' ? args.flags.name : nameFromKey(key);\n const result = scaffoldProject({ key, name });\n if (!result.ok) {\n printProblems(result.problems);\n return 1;\n }\n mkdirSync(target, { recursive: true });\n for (const [file, content] of Object.entries(result.files)) {\n writeFileSync(join(target, file), content);\n }\n out(`Created chart project ${key} in ${target}`);\n for (const file of Object.keys(result.files)) out(` ${file}`);\n out('');\n out('Next:');\n out(` cd ${dir}`);\n out(' edit chart.ts (read AGENTS.md first)');\n out(' databrain custom-chart validate');\n out(' databrain custom-chart build');\n out(' databrain custom-chart upload');\n return 0;\n}\n\nfunction commandValidate(args: ParsedArgs): number {\n const dir = resolve(args.positional[0] ?? '.');\n const result = validateProject(dir);\n if (!result.ok) {\n err(`${result.problems.length} problem${result.problems.length === 1 ? '' : 's'} in ${dir}:`);\n printProblems(result.problems);\n return 1;\n }\n const manifest = result.project.manifest as { id: string; slots: unknown[]; options: unknown[] };\n out(`Valid: custom:${manifest.id} (${manifest.slots.length} slots, ${manifest.options.length} options) in ${dir}`);\n return 0;\n}\n\nasync function commandBuild(args: ParsedArgs): Promise<number> {\n const dir = resolve(args.positional[0] ?? '.');\n const result = await buildProject(dir);\n if (!result.ok || !result.outputs) {\n err(`Build failed with ${result.problems.length} problem${result.problems.length === 1 ? '' : 's'}:`);\n printProblems(result.problems);\n return 1;\n }\n out(`Built custom:${result.outputs.manifest.id}`);\n for (const path of result.written) out(` ${path}`);\n out(` contentHash ${result.outputs.contentHash}`);\n out(` size ${result.outputs.sizeBytes} bytes (bundle ${Buffer.byteLength(result.outputs.bundleJs, 'utf8')} of 2097152)`);\n return 0;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n if (!process.stdin.isTTY) {\n err('stdin is not a terminal. Pass --yes to upload without a prompt.');\n return false;\n }\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = await new Promise<string>((resolveAnswer) => rl.question(question, resolveAnswer));\n rl.close();\n return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';\n}\n\nasync function commandUpload(args: ParsedArgs): Promise<number> {\n const dir = resolve(args.positional[0] ?? '.');\n const serviceToken = process.env.DATABRAIN_SERVICE_TOKEN;\n if (!serviceToken) {\n err('DATABRAIN_SERVICE_TOKEN is not set. Create one in Settings > Service Tokens and export it, then try again.');\n return 1;\n }\n registerSecret(serviceToken);\n const apiUrl = process.env.DATABRAIN_API_URL || 'https://api.usedatabrain.com';\n if (!process.env.DATABRAIN_API_URL) out(`DATABRAIN_API_URL is not set, using ${apiUrl}`);\n\n const built = await buildProject(dir);\n if (!built.ok || !built.outputs) {\n err('Build failed:');\n printProblems(built.problems);\n return 1;\n }\n const { manifest, bundleJs, bundleCss, iconSvg } = built.outputs;\n const key = manifest.id;\n out(`Built custom:${key} (${built.outputs.contentHash.slice(0, 12)}, ${built.outputs.sizeBytes} bytes)`);\n\n const client = new DatabrainClient({ serviceToken, apiUrl, demoDomain: 'https://demo.usedatabrain.com' });\n const api = createCustomChartsApi(client);\n try {\n const validation = await api.validate({ manifest, bundleJs, bundleCss, iconSvg });\n if (!validation.ok) {\n err('The server refused the bundle:');\n printProblems(validation.problems);\n return 1;\n }\n out('Server validation: ok');\n const existing = (await api.list()).find((chart) => chart.key === key);\n const plan = planUpload(key, existing);\n out(`Plan: ${plan.summary}`);\n if (args.flags.publish === true) out('Then: publish that version (every dashboard using this key will render it).');\n\n if (args.flags.yes !== true) {\n const go = await confirm('Continue? [y/N] ');\n if (!go) {\n out('Cancelled. Nothing was uploaded.');\n return 1;\n }\n }\n\n const uploaded = await api.upload({ manifest, bundleJs, bundleCss, iconSvg });\n const versionNumber = uploaded.version?.versionNumber ?? '?';\n if (uploaded.created === 'none') out(`This exact bundle is already v${versionNumber} of custom:${key}.`);\n else if (uploaded.created === 'chart') out(`Created custom:${key} (v${versionNumber}, private).`);\n else out(`Uploaded v${versionNumber} of custom:${key}. It is in preview until you publish it.`);\n\n if (args.flags.publish === true) {\n const published = await api.publish(key, uploaded.version?.id);\n out(`Published v${versionNumber} of custom:${key} (status ${published.chart?.status ?? 'published'}).`);\n } else if (uploaded.created !== 'none') {\n out(`Publish it with: databrain custom-chart upload --publish, the publish_custom_chart tool, or Settings > Developers > Custom charts.`);\n }\n return 0;\n } catch (error) {\n if (error instanceof DatabrainApiError) {\n err(`Error ${error.status || ''} ${error.code}: ${error.message}`.replace(/\\s+/g, ' ').trim());\n printProblems(apiProblems(error));\n return 1;\n }\n err(`Unexpected error: ${(error as Error).message}`);\n return 1;\n }\n}\n\nasync function commandDev(args: ParsedArgs): Promise<number> {\n const dir = resolve(args.positional[0] ?? '.');\n const port = typeof args.flags.port === 'string' ? Number(args.flags.port) : 4174;\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n err('--port must be a number between 1 and 65535.');\n return 1;\n }\n const built = await buildProject(dir);\n if (!built.ok) {\n err('Build failed:');\n printProblems(built.problems);\n return 1;\n }\n const server = await startDevServer({ dir, port });\n out(`Custom chart dev page: http://127.0.0.1:${server.port}/ (Ctrl+C to stop)`);\n out('The page rebuilds on every reload. Edit chart.ts, save, reload.');\n await new Promise<void>((resolveDone) => {\n const stop = () => {\n server.close();\n resolveDone();\n };\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n });\n return 0;\n}\n\nexport async function runCli(argv: string[]): Promise<number> {\n const args = parseArgs(argv);\n if (args.flags.version === true) {\n out(MCP_SERVER_VERSION);\n return 0;\n }\n const [family, command, ...rest] = args.positional;\n if (args.flags.help === true || !family) {\n out(USAGE);\n return family ? 0 : 1;\n }\n if (family !== 'custom-chart') {\n err(`Unknown command \"${family}\".`);\n out(USAGE);\n return 1;\n }\n const sub: ParsedArgs = { positional: rest, flags: args.flags };\n switch (command) {\n case 'init':\n return commandInit(sub);\n case 'validate':\n return commandValidate(sub);\n case 'build':\n return commandBuild(sub);\n case 'upload':\n return commandUpload(sub);\n case 'dev':\n return commandDev(sub);\n default:\n err(command ? `Unknown custom-chart command \"${command}\".` : 'custom-chart needs a command.');\n out(USAGE);\n return 1;\n }\n}\n\nconst invokedDirectly =\n typeof process.argv[1] === 'string' && /(^|[\\\\/])(cli\\.(js|mjs|ts)|databrain)$/.test(process.argv[1]);\nif (invokedDirectly) {\n runCli(process.argv.slice(2)).then(\n (code) => {\n process.exitCode = code;\n },\n (error: Error) => {\n err(`Unexpected error: ${error.message}`);\n process.exitCode = 1;\n },\n );\n}\n","import { createServer, type Server } from 'node:http';\nimport { buildProject, readProject } from '../custom-chart/bundle.js';\nimport type { CustomChartManifest, CustomChartOption } from '../custom-chart/types.js';\n\n/**\n * `databrain custom-chart dev`: a local page that hosts dist/index.js the way\n * the product does, in a sandboxed frame that only sees the sandbox protocol.\n * It is a tiny copy of the plugin relay, enough to see the chart draw, click\n * and resize with sample-data.json. Like the product host it injects\n * chart.css into the frame; the bundle itself carries no css. It is not the\n * product sandbox: no CSP, no relay frame, no limits. Never point it at real\n * data.\n */\nexport interface DevServerOptions {\n dir: string;\n port: number;\n}\n\nconst LIGHT_THEME = {\n fontFamily: 'Inter, system-ui, sans-serif',\n fontSize: 12,\n textColor: '#1f2937',\n textSecondaryColor: '#6b7280',\n background: '#ffffff',\n surfaceColor: '#f9fafb',\n borderColor: '#e5e7eb',\n borderRadius: 6,\n primaryColor: '#3366cc',\n palette: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#990099', '#0099c6', '#dd4477', '#66aa00'],\n};\n\nconst DARK_THEME = {\n ...LIGHT_THEME,\n textColor: '#f3f4f6',\n textSecondaryColor: '#9ca3af',\n background: '#111827',\n surfaceColor: '#1f2937',\n borderColor: '#374151',\n primaryColor: '#60a5fa',\n palette: ['#60a5fa', '#f87171', '#fbbf24', '#34d399', '#c084fc', '#22d3ee', '#f472b6', '#a3e635'],\n};\n\nfunction defaultOptions(manifest: CustomChartManifest): Record<string, string | number | boolean> {\n const options: Record<string, string | number | boolean> = {};\n for (const option of manifest.options as CustomChartOption[]) options[option.id] = option.default;\n return options;\n}\n\n/** Deterministic sample data from the manifest slots when sample-data.json is absent. */\nfunction generateSampleData(manifest: CustomChartManifest): { columns: unknown[]; rows: unknown[][] } {\n const labels = ['North', 'South', 'East', 'West', 'Central', 'Coast', 'Island', 'Valley'];\n const columns = manifest.slots.map((slot, index) => ({\n id: `${slot.id}_${index}`,\n label: slot.label,\n type: slot.types[0],\n filterable: slot.role === 'dimension',\n slotId: slot.id,\n }));\n const rows = labels.map((label, row) =>\n manifest.slots.map((slot, column) => {\n const type = slot.types[0];\n if (type === 'string') return `${label}${column > 0 ? ` ${column}` : ''}`;\n if (type === 'boolean') return (row + column) % 2 === 0;\n return Math.round(1000 - row * 97 + column * 53 + ((row * 31 + column * 17) % 90));\n }),\n );\n return { columns, rows };\n}\n\nconst PAGE = `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Custom chart dev</title>\n<style>\n :root { color-scheme: light dark; }\n body { margin: 0; font: 14px system-ui, sans-serif; display: grid; grid-template-rows: auto 1fr; height: 100vh; }\n header { display: flex; gap: 12px; align-items: center; padding: 10px 16px; border-bottom: 1px solid #8884; }\n header code { font-size: 12px; opacity: 0.8; }\n main { display: grid; grid-template-columns: 1fr 320px; min-height: 0; }\n #stage { padding: 16px; display: flex; align-items: flex-start; justify-content: center; overflow: auto; }\n #frame { border: 1px solid #8886; border-radius: 8px; background: transparent; }\n aside { border-left: 1px solid #8884; padding: 12px 16px; overflow: auto; }\n aside h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; margin: 0 0 8px; opacity: 0.7; }\n #log { font: 12px ui-monospace, monospace; white-space: pre-wrap; margin: 0; }\n select, button { font: inherit; }\n</style>\n</head>\n<body>\n<header>\n <strong id=\"title\">Custom chart</strong>\n <code id=\"key\"></code>\n <label>Theme <select id=\"theme\"><option value=\"light\">light</option><option value=\"dark\">dark</option></select></label>\n <label>Size <select id=\"size\"><option value=\"600x320\">600 x 320</option><option value=\"900x420\">900 x 420</option><option value=\"320x240\">320 x 240</option></select></label>\n <button id=\"reload\">Rebuild and reload</button>\n</header>\n<main>\n <div id=\"stage\"><iframe id=\"frame\" sandbox=\"allow-scripts\" width=\"600\" height=\"320\" title=\"Custom chart\"></iframe></div>\n <aside><h2>Events</h2><pre id=\"log\">Waiting for ready...</pre></aside>\n</main>\n<script>\n(async () => {\n const log = document.getElementById('log');\n const frame = document.getElementById('frame');\n const themeSelect = document.getElementById('theme');\n const sizeSelect = document.getElementById('size');\n const say = (line) => { log.textContent = (line + '\\\\n' + log.textContent).slice(0, 8000); };\n const [manifest, bundle, sample, themes, css] = await Promise.all([\n fetch('/manifest.json').then((r) => r.json()),\n fetch('/bundle.js').then((r) => r.ok ? r.text() : r.text().then((t) => { throw new Error(t); })).catch((e) => { say('BUILD ERROR\\\\n' + e.message); return null; }),\n fetch('/sample-data.json').then((r) => r.json()),\n fetch('/themes.json').then((r) => r.json()),\n fetch('/index.css').then((r) => r.text()),\n ]);\n document.getElementById('title').textContent = manifest.name;\n document.getElementById('key').textContent = 'custom:' + manifest.id;\n if (!bundle) return;\n let revision = 0;\n const data = () => ({\n columns: sample.columns, rows: sample.rows,\n options: Object.assign({}, sample.defaults, sample.options || {}),\n theme: themes[themeSelect.value], language: 'en'\n });\n const send = () => frame.contentWindow.postMessage({ protocol: 'dbn-chart-sandbox', version: 1, channel: 'dev', type: 'data', revision: ++revision, data: data() }, '*');\n addEventListener('message', (event) => {\n if (event.source !== frame.contentWindow) return;\n const m = event.data || {};\n if (m.type === 'ready') { say('ready'); send(); return; }\n if (m.type === 'resize' && Number.isFinite(m.height)) frame.height = Math.max(80, Math.min(1200, m.height));\n say(m.type + ' ' + JSON.stringify(m, (k, v) => (k === 'protocol' || k === 'version' || k === 'channel' || k === 'type') ? undefined : v));\n });\n const boot = '(' + (function () {\n var channel = 'dev'; var revision = -1; var latest = null; var listener = null;\n var send = function (payload) { parent.postMessage(Object.assign({ protocol: 'dbn-chart-sandbox', version: 1, channel: channel }, payload), '*'); };\n window.dbnSandbox = Object.freeze({\n onData: function (cb) { listener = cb; send({ type: 'ready' }); if (latest) cb(latest); },\n click: function (rowIndex, columnId) { send({ type: 'click', revision: revision, rowIndex: rowIndex, columnId: columnId }); },\n filter: function (columnId, values) { send({ type: 'filter', revision: revision, columnId: columnId, values: values }); },\n resize: function (height) { send({ type: 'resize', height: height }); }\n });\n addEventListener('error', function (e) { send({ type: 'error', code: 'runtime', message: String(e.message || e) }); });\n addEventListener('message', function (event) {\n var m = event.data; if (!m || m.type !== 'data' || m.revision <= revision) return;\n revision = m.revision; latest = m.data; if (listener) listener(latest);\n });\n }).toString() + ')();';\n const child = '<!doctype html><html><head><meta charset=\"utf-8\"><style>html,body{margin:0;width:100%;height:100%;overflow:hidden}body{font:14px system-ui}#chart{width:100%;height:100%}</style><style>' + css.replace(/<\\\\/style/gi, '') + '</style></head><body><div id=\"chart\"></div><script>' + boot + '<\\\\/script><script>' + bundle.replace(/<\\\\/script/gi, '<\\\\\\\\/script') + '<\\\\/script></body></html>';\n const mount = () => { frame.srcdoc = child; };\n themeSelect.addEventListener('change', () => { if (revision > 0) send(); });\n sizeSelect.addEventListener('change', () => { const [w, h] = sizeSelect.value.split('x'); frame.width = w; frame.height = h; if (revision > 0) send(); });\n document.getElementById('reload').addEventListener('click', () => location.reload());\n mount();\n})();\n</script>\n</body>\n</html>\n`;\n\nexport async function startDevServer(options: DevServerOptions): Promise<{ port: number; close: () => void; server: Server }> {\n const server = createServer(async (req, res) => {\n const url = new URL(req.url ?? '/', 'http://127.0.0.1');\n const json = (status: number, body: unknown) => {\n res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(JSON.stringify(body));\n };\n try {\n if (url.pathname === '/') {\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(PAGE);\n return;\n }\n if (url.pathname === '/bundle.js') {\n const result = await buildProject(options.dir, { write: false });\n if (!result.ok || !result.outputs) {\n res.writeHead(422, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(result.problems.map((p) => `${p.path || '/'} ${p.code} ${p.message}`).join('\\n'));\n return;\n }\n res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(result.outputs.bundleJs);\n return;\n }\n const project = readProject(options.dir);\n const manifest = project.manifest as CustomChartManifest | undefined;\n if (url.pathname === '/manifest.json') {\n json(manifest ? 200 : 422, manifest ?? { problems: project.problems });\n return;\n }\n if (url.pathname === '/sample-data.json') {\n if (!manifest) {\n json(422, { problems: project.problems });\n return;\n }\n const sample = (project.sampleData as { columns?: unknown[]; rows?: unknown[][]; options?: unknown } | undefined) ?? generateSampleData(manifest);\n json(200, { ...sample, defaults: defaultOptions(manifest) });\n return;\n }\n if (url.pathname === '/index.css') {\n // The host injects bundleCss into the frame; this page plays the host.\n res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' });\n res.end(project.css ?? '');\n return;\n }\n if (url.pathname === '/themes.json') {\n json(200, { light: LIGHT_THEME, dark: DARK_THEME });\n return;\n }\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('not found');\n } catch (err) {\n res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end((err as Error).message);\n }\n });\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, '127.0.0.1', () => resolve());\n });\n const address = server.address();\n const port = typeof address === 'object' && address ? address.port : options.port;\n return { port, server, close: () => server.close() };\n}\n"],"mappings":"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","names":["err","resolve"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";(()=>{var ie=Object.defineProperty;var ae=(e,t,n)=>t in e?ie(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var V=(e,t,n)=>ae(e,typeof t!="symbol"?t+"":t,n);var f=e=>e!==null&&typeof e=="object"&&Object.getPrototypeOf(e)===Object.prototype,O=(e,t)=>Object.keys(e).length===t.length&&t.every(n=>Object.hasOwn(e,n)),S=e=>typeof e=="string"&&/^[a-z][a-z0-9_-]{0,63}$/.test(e)&&!["constructor","prototype","__proto__"].includes(e),T=(e,t=256)=>typeof e=="string"&&e.length>0&&e.length<=t,_=e=>e===null||typeof e=="boolean"||typeof e=="number"&&Number.isFinite(e)||typeof e=="string"&&e.length<=4096,A=e=>new TextEncoder().encode(JSON.stringify(e)).byteLength;var de=32*1024,X=16,K=32,ce=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/,q=/^#[0-9a-fA-F]{6}$/,le=/^[a-z]{2}(-[A-Z]{2})?$/,fe=["string","number","boolean"],M=["schemaVersion","id","name","slots","options"],me=[...M,"version","description","translations"],L=["id","label","role","types","required"],ue=[...L,"description"],ge=["id","label","type","default"],Y={boolean:[],number:["min","max"],string:["maxLength"],select:["choices"],color:[]},pe={boolean:["group"],number:["group","step"],string:["group"],select:["group"],color:["group"]},j=e=>typeof e=="number"&&Number.isFinite(e),I=e=>typeof e=="string"&&e.length<=256,D=class{constructor(){V(this,"list",[])}add(t,n,o){this.list.push({code:t,path:n,message:o})}},C=(e,t,n,o,s,i,d=r=>`${o}.${r}`)=>{let r=new Set([...s,...i]),c=!0;for(let a of Object.keys(t))r.has(a)||(e.add(`${o}.unknownKey`,`${n}/${a}`,`Remove "${a}". It is not a known key here.`),c=!1);for(let a of s)Object.hasOwn(t,a)||(e.add(d(a),`${n}/${a}`,`Add the required key "${a}".`),c=!1);return c},be=(e,t,n,o)=>{if(!f(t)){e.add("manifest.slot.invalid",n,"A slot must be an object.");return}if(!C(e,t,n,"manifest.slot",L,o?ue.slice(L.length):[]))return;S(t.id)||e.add("manifest.slot.id",`${n}/id`,'Use a slot id that starts with a letter and has only lowercase letters, digits, "_" or "-" (max 64).'),T(t.label)||e.add("manifest.slot.label",`${n}/label`,"Give the slot a label of 1 to 256 characters.");let s=t.role;s!=="dimension"&&s!=="measure"&&e.add("manifest.slot.role",`${n}/role`,'Set role to "dimension" or "measure".'),typeof t.required!="boolean"&&e.add("manifest.slot.required",`${n}/required`,"Set required to true or false.");let i=t.types;Array.isArray(i)&&i.length>0&&i.length<=3&&new Set(i).size===i.length&&i.every(r=>fe.includes(r))?s==="measure"&&(i.length!==1||i[0]!=="number")&&e.add("manifest.slot.types",`${n}/types`,'A measure slot accepts only ["number"].'):e.add("manifest.slot.types",`${n}/types`,'List 1 to 3 unique types from "string", "number" and "boolean".'),Object.hasOwn(t,"description")&&!T(t.description,256)&&e.add("manifest.slot.description",`${n}/description`,"A slot description is 1 to 256 characters.")},ye=(e,t,n,o)=>{if(!f(t)){e.add("manifest.option.invalid",n,"An option must be an object.");return}let s=t.id;if(typeof s=="string"&&(s==="theme"||s.startsWith("theme."))){e.add("manifest.option.reserved",`${n}/id`,'The option id "theme" is reserved for the host theme. Choose another id.');return}let i=typeof t.type=="string"?t.type:"",d=Object.hasOwn(Y,i)?Y[i]:void 0;if(!d||i==="color"&&!o){e.add("manifest.option.type",`${n}/type`,o?'Set type to "boolean", "number", "string", "select" or "color".':'Set type to "boolean", "number", "string" or "select". "color" needs schemaVersion 2.');return}if(C(e,t,n,"manifest.option",[...ge,...d],o?pe[i]:[]))switch(S(s)||e.add("manifest.option.id",`${n}/id`,'Use an option id that starts with a letter and has only lowercase letters, digits, "_" or "-" (max 64).'),T(t.label)||e.add("manifest.option.label",`${n}/label`,"Give the option a label of 1 to 256 characters."),Object.hasOwn(t,"group")&&!T(t.group,64)&&e.add("manifest.option.group",`${n}/group`,"A group label is 1 to 64 characters."),i){case"boolean":typeof t.default!="boolean"&&e.add("manifest.option.default",`${n}/default`,"A boolean option needs a true or false default.");break;case"number":{if(!(j(t.min)&&j(t.max)&&t.min<=t.max)){e.add("manifest.option.range",`${n}/min`,"Set finite min and max with min <= max.");break}let c=t.min,a=t.max;(!j(t.default)||t.default<c||t.default>a)&&e.add("manifest.option.default",`${n}/default`,"The default must be a finite number between min and max."),Object.hasOwn(t,"step")&&(!j(t.step)||t.step<=0||t.step>a-c)&&e.add("manifest.option.step",`${n}/step`,"A step must be a positive number no larger than max - min.");break}case"string":{let r=t.maxLength;if(!j(r)||!Number.isInteger(r)||r<1||r>1024){e.add("manifest.option.maxLength",`${n}/maxLength`,"Set maxLength to an integer from 1 to 1024.");break}(typeof t.default!="string"||t.default.length>r)&&e.add("manifest.option.default",`${n}/default`,"The default must be a string no longer than maxLength.");break}case"select":{let r=t.choices;if(!(Array.isArray(r)&&r.length>0&&r.length<=32&&r.every(a=>f(a)&&Object.keys(a).length===2&&S(a.value)&&T(a.label))&&new Set(r.map(a=>a.value)).size===r.length)){e.add("manifest.option.choices",`${n}/choices`,"List 1 to 32 choices, each { value, label } with unique ids as values.");break}r.some(a=>a.value===t.default)||e.add("manifest.option.default",`${n}/default`,"The default must be the value of one of the choices.");break}case"color":(typeof t.default!="string"||!q.test(t.default))&&e.add("manifest.option.color",`${n}/default`,'A color default is a six-digit hex string such as "#3366cc".');break}},he=(e,t,n,o)=>{let s="/translations";if(!f(t)){e.add("manifest.translations.invalid",s,"translations must be an object keyed by language code.");return}for(let[i,d]of Object.entries(t)){let r=`${s}/${i}`;if(!le.test(i)){e.add("manifest.translations.language",r,'Use a language code such as "fr" or "pt-BR".');continue}if(!f(d)){e.add("manifest.translations.invalid",r,"A translation must be an object.");continue}if(C(e,d,r,"manifest.translations",[],["name","description","slots","options"])){for(let c of["name","description"])Object.hasOwn(d,c)&&!I(d[c])&&e.add("manifest.translations.text",`${r}/${c}`,"A translated string is at most 256 characters.");if(Object.hasOwn(d,"slots")){let c=d.slots;if(!f(c))e.add("manifest.translations.invalid",`${r}/slots`,"slots must be an object keyed by slot id.");else for(let[a,l]of Object.entries(c)){let g=`${r}/slots/${a}`;if(!n.some(m=>m.id===a)){e.add("manifest.translations.slot",g,`There is no slot "${a}" to translate.`);continue}if(!f(l)||!C(e,l,g,"manifest.translations",[],["label","description"])){f(l)||e.add("manifest.translations.invalid",g,"A slot translation must be an object.");continue}for(let m of["label","description"])Object.hasOwn(l,m)&&!I(l[m])&&e.add("manifest.translations.text",`${g}/${m}`,"A translated string is at most 256 characters.")}}if(Object.hasOwn(d,"options")){let c=d.options;if(!f(c))e.add("manifest.translations.invalid",`${r}/options`,"options must be an object keyed by option id.");else for(let[a,l]of Object.entries(c)){let g=`${r}/options/${a}`,m=o.find(p=>p.id===a);if(!m){e.add("manifest.translations.option",g,`There is no option "${a}" to translate.`);continue}if(!f(l)||!C(e,l,g,"manifest.translations",[],["label","group","choices"])){f(l)||e.add("manifest.translations.invalid",g,"An option translation must be an object.");continue}for(let p of["label","group"])Object.hasOwn(l,p)&&!I(l[p])&&e.add("manifest.translations.text",`${g}/${p}`,"A translated string is at most 256 characters.");if(Object.hasOwn(l,"choices")){let p=l.choices;if(!f(p)){e.add("manifest.translations.invalid",`${g}/choices`,"choices must be an object keyed by choice value.");continue}for(let[x,u]of Object.entries(p)){let y=`${g}/choices/${x}`;m.type!=="select"||!m.choices.some(b=>b.value===x)?e.add("manifest.translations.choice",y,`There is no choice "${x}" on option "${a}".`):I(u)||e.add("manifest.translations.text",y,"A translated string is at most 256 characters.")}}}}}}},xe=e=>{let t=new D;if(!f(e))return t.add("manifest.invalid","","The manifest must be a JSON object."),t.list;let n=e.schemaVersion;if(n!==1&&n!==2)return t.add("manifest.schemaVersion.unsupported","/schemaVersion","Set schemaVersion to 1 or 2."),t.list;let o=n===2;if(!C(t,e,"","manifest",M,o?me.slice(M.length):[],d=>`manifest.${d}.required`))return t.list;S(e.id)||t.add("manifest.id.invalid","/id",'Use an id that starts with a letter and has only lowercase letters, digits, "_" or "-" (max 64). The chart type becomes "custom:<id>".'),T(e.name)||t.add("manifest.name.invalid","/name","Give the chart a name of 1 to 256 characters."),Object.hasOwn(e,"version")&&(typeof e.version!="string"||!ce.test(e.version))&&t.add("manifest.version.invalid","/version",'version must be a semver string such as "1.2.0".'),Object.hasOwn(e,"description")&&!T(e.description,500)&&t.add("manifest.description.invalid","/description","description is 1 to 500 characters.");let s=e.slots;if(!Array.isArray(s))t.add("manifest.slots.invalid","/slots","slots must be an array.");else if(s.length===0)t.add("manifest.slots.empty","/slots","Declare at least one slot.");else if(s.length>X)t.add("manifest.slots.tooMany","/slots",`Declare at most ${X} slots.`);else{for(let[r,c]of s.entries())be(t,c,`/slots/${r}`,o);let d=new Set;s.forEach((r,c)=>{!f(r)||typeof r.id!="string"||(d.has(r.id)&&t.add("manifest.slot.duplicate",`/slots/${c}/id`,`Slot id "${r.id}" is used more than once.`),d.add(r.id))})}let i=e.options;if(!Array.isArray(i))t.add("manifest.options.invalid","/options","options must be an array (it may be empty).");else if(i.length>K)t.add("manifest.options.tooMany","/options",`Declare at most ${K} options.`);else{for(let[r,c]of i.entries())ye(t,c,`/options/${r}`,o);let d=new Set;i.forEach((r,c)=>{!f(r)||typeof r.id!="string"||(d.has(r.id)&&t.add("manifest.option.duplicate",`/options/${c}/id`,`Option id "${r.id}" is used more than once.`),d.add(r.id))})}return t.list.length===0&&Object.hasOwn(e,"translations")&&he(t,e.translations,s,i),t.list.length===0&&A(e)>de&&t.add("manifest.size","","The manifest must be 32 KiB or smaller as JSON."),t.list},N=e=>xe(e).length===0?e:null;var R=(e,t)=>{if(!N(e)||!f(t)||Object.keys(t).some(o=>!e.options.some(s=>s.id===o)))return null;let n={};for(let o of e.options){let s=Object.hasOwn(t,o.id)?t[o.id]:o.default,i=!1;switch(o.type){case"boolean":i=typeof s=="boolean";break;case"number":i=typeof s=="number"&&Number.isFinite(s)&&s>=o.min&&s<=o.max;break;case"string":i=typeof s=="string"&&s.length<=o.maxLength;break;case"select":i=o.choices.some(d=>d.value===s);break;case"color":i=typeof s=="string"&&q.test(s);break}if(!i)return null;n[o.id]=s}return n};var J=2*1024*1024,G=256*1024,B="allow-scripts",P="camera 'none'; microphone 'none'; geolocation 'none'; payment 'none'; usb 'none'; clipboard-read 'none'; clipboard-write 'none'; display-capture 'none'; fullscreen 'none'",Se=e=>{if(!N(e.manifest))throw new Error("Invalid chart manifest. Check schema version 1.");if(typeof e.javascript!="string"||e.javascript.trim().length===0||e.javascript.length>J||new TextEncoder().encode(e.javascript).byteLength>J)throw new Error("Chart bundle must contain JavaScript and be no larger than 2 MiB.");if(e.css!==void 0&&(typeof e.css!="string"||e.css.length>G||new TextEncoder().encode(e.css).byteLength>G))throw new Error("Chart stylesheet must be text no larger than 256 KiB.")},we=e=>`<style>${e.replace(/<\/(style)/gi,"<\\/$1")}</style>`,Oe=e=>["default-src 'none'","script-src 'unsafe-inline' data:","script-src-attr 'none'","style-src 'unsafe-inline'","connect-src 'none'","img-src data:","font-src 'none'","media-src 'none'","object-src 'none'","worker-src 'none'","manifest-src 'none'","base-uri 'none'","form-action 'none'",`frame-src ${e?"blob:":"'none'"}`].join("; "),Te=e=>{let t=new TextEncoder().encode(e),n="";for(let o of t)n+=String.fromCharCode(o);return btoa(n)},ke=e=>e.replace(/<\/(script)/gi,"<\\/$1").replace(/<!--/g,"<\\!--"),H=(e,t)=>`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${Oe(t)}"><meta name="referrer" content="no-referrer"><meta http-equiv="x-dns-prefetch-control" content="off"><title>Custom chart</title><style>html,body{margin:0;width:100%;height:100%;overflow:hidden}body{font:14px system-ui;color:CanvasText;background:Canvas}iframe{border:0;width:100%;height:100%}</style></head><body>${e}</body></html>`,Ae=e=>`(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
const channel = ${JSON.stringify(e)};
|
|
4
|
+
const send = (payload) => parent.postMessage({protocol:'dbn-chart-sandbox',version:1,channel,...payload}, '*');
|
|
5
|
+
let revision = -1;
|
|
6
|
+
let latest = null;
|
|
7
|
+
let listener = null;
|
|
8
|
+
Object.defineProperty(window, 'dbnSandbox', {value: Object.freeze({
|
|
9
|
+
onData(callback) { if (typeof callback !== 'function') return; listener = callback; send({type:'ready'}); if (latest) callback(latest); },
|
|
10
|
+
click(rowIndex, columnId) { send({type:'click',revision,rowIndex,columnId}); },
|
|
11
|
+
filter(columnId, values) { send({type:'filter',revision,columnId,values}); },
|
|
12
|
+
resize(height) { send({type:'resize',height}); }
|
|
13
|
+
}), writable:false, configurable:false});
|
|
14
|
+
addEventListener('error', () => send({type:'error',code:'runtime'}));
|
|
15
|
+
addEventListener('message', (event) => {
|
|
16
|
+
const m = event.data;
|
|
17
|
+
if (event.source !== parent || event.origin !== 'null' || !m || m.protocol !== 'dbn-chart-sandbox' ||
|
|
18
|
+
m.version !== 1 || m.channel !== channel || m.type !== 'data' || !Number.isSafeInteger(m.revision) ||
|
|
19
|
+
m.revision <= revision || !m.data || !Array.isArray(m.data.columns) || !Array.isArray(m.data.rows) ||
|
|
20
|
+
!m.data.options || typeof m.data.options !== 'object') return;
|
|
21
|
+
revision = m.revision; latest = m.data;
|
|
22
|
+
if (listener) listener(latest);
|
|
23
|
+
});
|
|
24
|
+
})();`,W=(e,t,n)=>{if(Se(e),!/^[a-zA-Z0-9-]{16,80}$/.test(t))throw new Error("Invalid sandbox channel.");let o=n==="null"?null:new URL(n);if(o&&(!["http:","https:"].includes(o.protocol)||o.origin!==n))throw new Error("Sandbox host must have an HTTP or opaque origin.");let s=H(`${e.css?we(e.css):""}<div id="chart" role="img" aria-label="Custom chart"></div><script>${Ae(t)}<\/script><script>${ke(e.javascript)}<\/script>`,!1),i=`(() => {
|
|
25
|
+
'use strict';
|
|
26
|
+
const channel = ${JSON.stringify(t)};
|
|
27
|
+
const hostOrigin = ${JSON.stringify(n)};
|
|
28
|
+
const hostTarget = hostOrigin === 'null' ? '*' : hostOrigin;
|
|
29
|
+
const childHtml = new TextDecoder().decode(Uint8Array.from(atob('${Te(s)}'), c => c.charCodeAt(0)));
|
|
30
|
+
const frame = document.createElement('iframe');
|
|
31
|
+
frame.setAttribute('sandbox', '${B}');
|
|
32
|
+
frame.setAttribute('allow', ${JSON.stringify(P)});
|
|
33
|
+
frame.referrerPolicy = 'no-referrer';
|
|
34
|
+
frame.title = 'Custom chart content';
|
|
35
|
+
const url = URL.createObjectURL(new Blob([childHtml], {type:'text/html'}));
|
|
36
|
+
let loads = 0;
|
|
37
|
+
let active = true;
|
|
38
|
+
let ready = false;
|
|
39
|
+
let count = 0;
|
|
40
|
+
let start = performance.now();
|
|
41
|
+
frame.addEventListener('load', () => {
|
|
42
|
+
URL.revokeObjectURL(url);
|
|
43
|
+
if (++loads > 1) {
|
|
44
|
+
active = false; frame.remove();
|
|
45
|
+
parent.postMessage({protocol:'dbn-chart-sandbox',version:1,channel,type:'error',code:'navigation'}, hostTarget);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
addEventListener('message', (event) => {
|
|
49
|
+
const m = event.data;
|
|
50
|
+
if (!active || !m || typeof m !== 'object' || m.protocol !== 'dbn-chart-sandbox' || m.version !== 1 || m.channel !== channel) return;
|
|
51
|
+
if (event.source === parent && event.origin === hostOrigin && m.type === 'data' && ready) {
|
|
52
|
+
frame.contentWindow.postMessage(m, '*'); return;
|
|
53
|
+
}
|
|
54
|
+
if (event.source !== frame.contentWindow || event.origin !== 'null' || !['ready','click','filter','resize','error'].includes(m.type)) return;
|
|
55
|
+
const now = performance.now();
|
|
56
|
+
if (now - start >= 1000) { start = now; count = 0; }
|
|
57
|
+
if (++count > 60) return;
|
|
58
|
+
if (m.type === 'ready') { if (ready) return; ready = true; }
|
|
59
|
+
parent.postMessage(m, hostTarget);
|
|
60
|
+
});
|
|
61
|
+
frame.src = url;
|
|
62
|
+
document.body.append(frame);
|
|
63
|
+
})();`;return H(`<script>${i}<\/script>`,!0)};var ee="dbn-chart-sandbox",$e=1,z=1024*1024,F=5e3,Ee=32,Z=["fontFamily","textColor","textSecondaryColor","background","surfaceColor","borderColor","primaryColor"],Q=["fontSize","borderRadius"],Ce=/^[a-z]{2}(-[A-Z]{2})?$/,je=e=>!f(e)||!O(e,[...Z,...Q,"palette"])?!1:Z.every(t=>typeof e[t]=="string"&&e[t].length<=256)&&Q.every(t=>typeof e[t]=="number"&&Number.isFinite(e[t]))&&Array.isArray(e.palette)&&e.palette.length<=Ee&&e.palette.every(t=>typeof t=="string"&&t.length<=256),_e=["theme","language"],Ie=e=>{let t=Object.keys(e),n=_e.filter(o=>Object.hasOwn(e,o)).length;return t.length===3+n&&["columns","rows","options"].every(o=>Object.hasOwn(e,o))},v=e=>{if(!f(e)||!Ie(e)||Object.hasOwn(e,"theme")&&!je(e.theme)||Object.hasOwn(e,"language")&&!(typeof e.language=="string"&&Ce.test(e.language))||!Array.isArray(e.columns)||e.columns.length<1||e.columns.length>64||!Array.isArray(e.rows)||e.rows.length>F||!f(e.options)||Object.keys(e.options).length>32)return!1;let t=e.columns;return!t.every(n=>f(n)&&(O(n,["id","label","type","filterable"])||O(n,["id","label","type","filterable","slotId"])&&S(n.slotId))&&S(n.id)&&T(n.label)&&["string","number","boolean"].includes(n.type)&&typeof n.filterable=="boolean")||new Set(t.map(({id:n})=>n)).size!==t.length||!e.rows.every(n=>Array.isArray(n)&&n.length===t.length&&n.every((o,s)=>_(o)&&(o===null||typeof o===t[s].type)))||!Object.entries(e.options).every(([n,o])=>S(n)&&o!==null&&_(o))?!1:A(e)<=z},te=(e,t)=>{if(!t.source||e.source!==t.source||e.origin!=="null")return null;let n=e.data;if(!f(n)||n.protocol!==ee||n.version!==1||n.channel!==t.channel)return null;let o=["protocol","version","channel","type"];if(n.type==="ready")return O(n,o)?n:null;if(n.type==="error")return O(n,[...o,"code"])&&(n.code==="navigation"||n.code==="runtime")?n:null;if(n.type==="resize")return O(n,[...o,"height"])&&typeof n.height=="number"&&Number.isInteger(n.height)&&n.height>=80&&n.height<=1200?n:null;if(n.revision!==t.revision)return null;let s=t.data.columns.findIndex(({id:i})=>i===n.columnId);return s<0?null:n.type==="click"?O(n,[...o,"revision","rowIndex","columnId"])&&typeof n.rowIndex=="number"&&Number.isInteger(n.rowIndex)&&n.rowIndex>=0&&n.rowIndex<t.data.rows.length?n:null:n.type==="filter"&&O(n,[...o,"revision","columnId","values"])&&t.data.columns[s].filterable&&Array.isArray(n.values)&&n.values.length<=100&&n.values.every(i=>_(i)&&t.data.rows.some(d=>d[s]===i))&&A(n)<=32*1024?n:null},ne=(e,t,n)=>({protocol:ee,version:$e,channel:e,type:"data",revision:t,data:n});var oe=4096,Re=(e,t)=>{let n=e.toLowerCase().replace(/[^a-z0-9_-]+/g,"_").replace(/^[^a-z]+|_+$/g,"").slice(0,48);return n?`${n}_${t}`:`col_${t}`},se=e=>{if(typeof e=="number")return Number.isFinite(e)?e:null;if(typeof e=="boolean")return e?1:0;if(typeof e=="string"&&e.trim()!==""){let t=Number(e);return Number.isFinite(t)?t:null}return null},ve=e=>{if(e==null)return null;if(e instanceof Date)return Number.isNaN(e.getTime())?null:e.toISOString();let t=typeof e=="string"?e:String(e);return t.length>oe?t.slice(0,oe):t},Me=e=>typeof e=="boolean"?e:e==="true"?!0:e==="false"?!1:e===1||e===0?e===1:null,Le=(e,t)=>{if(e.types.length===1)return e.types[0];let n=t.filter(o=>o!=null);return e.types.includes("boolean")&&n.length>0&&n.every(o=>typeof o=="boolean")?"boolean":e.types.includes("number")&&n.length>0&&n.every(o=>se(o)!==null)?"number":e.types.includes("string")?"string":e.types[0]},De=(e,t)=>{switch(e){case"number":return se(t);case"boolean":return Me(t);default:return ve(t)}},Ne=(e,t)=>{let n=R(e,t);if(n)return n;let o={};for(let[s,i]of Object.entries(t))R(e,{[s]:i})&&(o[s]=i);return R(e,o)??{}},re=({manifest:e,config:t,rows:n,columns:o,theme:s,language:i})=>{let d=new Map;if(o)for(let u of o)d.set(u.key,u.label);if(d.size===0&&n[0])for(let u of Object.keys(n[0]))d.set(u,u);let r=new Set,c={},a=[],l=[];for(let u of e.slots){let y=(t.bindings[u.id]??[]).filter(b=>d.has(b));y.length===0&&u.required&&l.push(u);for(let b of y){let h=Re(b,a.length);for(;r.has(h)||!S(h);)h=`col_${a.length}_${r.size}`;r.add(h),c[h]=b;let $=n.slice(0,200).map(E=>E[b]);a.push({id:h,label:(d.get(b)||b).slice(0,256)||h,type:Le(u,$),filterable:u.role==="dimension",slotId:u.id,source:b})}}let g=n.length,m=Math.min(g,F),p=u=>({columns:a.map(({id:y,label:b,type:h,filterable:$,slotId:E})=>({id:y,label:b,type:h,filterable:$,slotId:E})),rows:n.slice(0,u).map(y=>a.map(b=>De(b.type,y[b.source]))),options:Ne(e,t.options),...s?{theme:s}:{},...i?{language:i}:{}}),x=p(m);for(;m>0&&A(x)>z;)m=Math.floor(m/2),x=p(m);return a.length===0?{data:x,truncated:!1,totalRows:g,sources:c,missingSlots:l}:(v(x)||(x={...p(0)},m=0),{data:x,truncated:m<g,totalRows:g,sources:c,missingSlots:l})};function Be(e,t,n,o,s){let i=!0,d=!1,r=0,c=0,a=crypto.randomUUID(),l=document.createElement("iframe");l.title=t.manifest.name,l.setAttribute("sandbox",B),l.setAttribute("allow",P),l.referrerPolicy="no-referrer",l.style.cssText="width:100%;height:320px;border:0;display:block";let g=()=>{let w=getComputedStyle(document.body),k=getComputedStyle(document.documentElement).colorScheme==="dark";return{fontFamily:w.fontFamily,fontSize:parseFloat(w.fontSize)||14,textColor:w.color,textSecondaryColor:w.color,background:w.backgroundColor,surfaceColor:w.backgroundColor,borderColor:k?"#454652":"#dedee6",borderRadius:8,primaryColor:"#7c6cf0",palette:["#7c6cf0","#35b9a7","#e6a548","#df7197","#539bdd","#a985db"]}},m=()=>re({manifest:t.manifest,config:n,rows:o,theme:g()}),p=m();if(p.missingSlots.length||!v(p.data))throw Error("Bind each required custom chart slot to a result column. The rows are below.");let x=W({manifest:t.manifest,javascript:t.bundleJs,css:t.bundleCss},a,globalThis.origin),u=URL.createObjectURL(new Blob([x],{type:"text/html"})),y=()=>{!i||!d||(p=m(),l.contentWindow?.postMessage(ne(a,++r,p.data),"*"))},b=()=>{i=!1,clearTimeout(E),U.disconnect(),removeEventListener("message",$),URL.revokeObjectURL(u),l.remove()},h=w=>{b(),s(w)},$=w=>{if(!i)return;let k=te(w,{source:l.contentWindow,channel:a,revision:r,data:p.data});if(k)if(k.type==="ready"){if(d)return;d=!0,clearTimeout(E),s(""),y()}else k.type==="error"?h("The custom chart could not run safely. The rows are below."):k.type==="resize"&&(l.style.height=`${k.height}px`)},E=setTimeout(()=>h("This host could not open the custom chart sandbox. The rows are below."),8e3),U=new ResizeObserver(y);return l.addEventListener("load",()=>{URL.revokeObjectURL(u),++c>1&&h("The custom chart navigated away. The rows are below.")}),addEventListener("message",$),l.src=u,e.append(l),U.observe(e),{dispose:b,update:y}}Object.assign(globalThis,{DbnCustomChartRuntime:{mount:Be}});})();
|
package/dist/index.js
CHANGED
|
@@ -3,17 +3,18 @@ import {
|
|
|
3
3
|
DEFAULT_POSTHOG_API_KEY,
|
|
4
4
|
DEFAULT_POSTHOG_HOST,
|
|
5
5
|
loadConfig
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-V3UW3B6U.js";
|
|
7
7
|
import {
|
|
8
8
|
registerAllPrompts,
|
|
9
9
|
registerAllResources,
|
|
10
10
|
registerAllTools
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-OLOC2MTA.js";
|
|
12
12
|
import {
|
|
13
13
|
DatabrainClient,
|
|
14
14
|
MCP_SERVER_VERSION,
|
|
15
15
|
logger
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-347U6GXR.js";
|
|
17
|
+
import "./chunk-Q5EYDKSA.js";
|
|
17
18
|
|
|
18
19
|
// src/index.ts
|
|
19
20
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -61,7 +62,7 @@ Production safety: OAuth may not be available yet. In service-token mode, writes
|
|
|
61
62
|
|
|
62
63
|
Semantic models: search_semantic_model, get_semantic_entity, propose_semantic_change, apply_semantic_change, and eval_semantic_model use the configured service credential and datamartId from list_datamarts. For a v2 model, use these tools instead of update_semantic_layer. Search and inspect first. Propose a change, review its errors and plan, then evaluate its returned source. Apply that exact source with its expected_content_hash only after the user supplies confirm="APPLY_TO_PRODUCTION" and dry_run=false. Never supply confirmation yourself. Apply changes the live model for every consumer. A conflict requires a new proposal and review. dry_run defaults to true and never writes.
|
|
63
64
|
|
|
64
|
-
Custom chart types (chart type custom:<key>): custom_chart_scaffold returns a
|
|
65
|
+
Custom chart types (chart type custom:<key>): custom_chart_scaffold returns a draft and project files. In hosted agents, custom_chart_edit sends the description and draft to Chart studio Bryn when installed; if unavailable, edit draft.code yourself. Pass chartTs, manifest, bundleCss and iconSvg for server-side compilation without files or imports. custom_chart_preview shows validated draft code on bounded real sample rows without saving. custom_chart_validate checks a manifest and bundle without persisting, custom_chart_upload creates the chart or a preview version only with mode="apply" and confirm="APPLY_TO_PRODUCTION" (dry_run shows the plan first), and publish_custom_chart makes a version live for every builder only with the same confirmation because every dashboard using that key re-renders. For local projects only, build with \`databrain custom-chart build\`. A custom metric uses chartType="custom:<key>" and customChart={key,bindings,options}; its MCP App tile loads only the company published bundle in the app/embed sandbox. Hosts without MCP Apps show rows and a clear fallback.
|
|
65
66
|
|
|
66
67
|
Infrastructure creation is intentionally narrow in MCP: datasource and workspace creation still happen outside this workflow; sync_datasource and dry-run/apply create_datamart are available. The semantic layer is populated and maintained via MCP tools (get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status). When the semantic layer is empty, use these tools \u2014 do NOT send the user to the UI for semantic metadata.
|
|
67
68
|
|
|
@@ -117,7 +118,7 @@ function banner(msg) {
|
|
|
117
118
|
async function main() {
|
|
118
119
|
try {
|
|
119
120
|
if (process.argv.includes("--http")) {
|
|
120
|
-
const { startHosted } = await import("./main-
|
|
121
|
+
const { startHosted } = await import("./main-IUV24DMP.js");
|
|
121
122
|
await startHosted();
|
|
122
123
|
return;
|
|
123
124
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/server.ts","../src/utils/analytics.ts"],"sourcesContent":["import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { loadConfig } from './utils/config.js';\nimport { createServer } from './server.js';\nimport { setupAnalytics } from './utils/analytics.js';\nimport { logger } from './utils/logger.js';\nimport { MCP_SERVER_VERSION } from './version.js';\n\nfunction banner(msg: string): void {\n process.stderr.write(`[databrain] ${msg}\\n`);\n}\n\nasync function main(): Promise<void> {\n try {\n if (process.argv.includes('--http')) {\n const { startHosted } = await import('./hosted/main.js');\n await startHosted();\n return;\n }\n const config = loadConfig();\n const server = createServer(config);\n const posthog = setupAnalytics(server, config);\n const transport = new StdioServerTransport();\n\n let shuttingDown = false;\n const flushAnalytics = async (): Promise<void> => {\n if (shuttingDown || !posthog) return;\n shuttingDown = true;\n await posthog.shutdown();\n };\n // StdioServerTransport never observes stdin ending, so the MCP client\n // disconnecting drains the event loop and exits the process without ever\n // calling transport/server onclose. `beforeExit` is what actually fires\n // in that path; posthog-node's flush timers are unref'd so they don't\n // keep the process alive on their own.\n process.on('beforeExit', () => void flushAnalytics());\n process.on('SIGINT', () => void flushAnalytics().then(() => process.exit(0)));\n process.on('SIGTERM', () => void flushAnalytics().then(() => process.exit(0)));\n\n await server.connect(transport);\n banner(`v${MCP_SERVER_VERSION} ready`);\n } catch (error) {\n // An Error serializes to {} in JSON, which left a self-hosted operator with no\n // reason. Config errors name the setting, never its value; the logger also\n // scrubs registered secrets.\n logger.error(`Failed to start server: ${error instanceof Error ? error.message : String(error)}`);\n process.exit(1);\n }\n}\n\nmain();\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { DatabrainClient } from './client/databrain-client.js';\nimport type { Config } from './utils/config.js';\nimport { registerAllTools } from './tools/registry.js';\nimport { registerAllResources } from './resources/registry.js';\nimport { registerAllPrompts } from './prompts/registry.js';\nimport { MCP_SERVER_VERSION } from './version.js';\n\nexport function createServer(config: Config): McpServer {\n const client = new DatabrainClient(config);\n const auth = client.getActiveAuth();\n let operatorBlurb: string;\n if (auth.source === 'oauth-client-credentials') {\n operatorBlurb =\n 'OPERATOR IDENTITY: OAuth client-credentials mode is configured for org-level calls. Tokens identify a service application, not a human operator. The backend must validate the token and map its issuer/subject to an authorized principal. Configuration alone does not prove backend acceptance. Do NOT ask the user for credentials or tokens.\\n\\n';\n } else if (auth.source === 'service-token') {\n operatorBlurb =\n 'OPERATOR IDENTITY: service-token mode. Calls are authenticated at org level — actions in the audit log are tagged actor=system, not a specific user. Per-user identity binding will be added when the OAuth-based login flow ships. Do NOT ask the user for credentials or tokens.\\n\\n';\n } else if (auth.source === 'api-token') {\n operatorBlurb =\n 'OPERATOR IDENTITY: API-token-only mode. A per-data-app API token is configured, but DATABRAIN_SERVICE_TOKEN is not. Embedding operations can use the configured API token, but org-level setup tools require DATABRAIN_SERVICE_TOKEN in the MCP config.\\n\\n';\n } else {\n operatorBlurb =\n 'OPERATOR IDENTITY: no usable token is configured. The MCP server normally refuses this config at startup. Ask the user to add DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN to the MCP config before proceeding.\\n\\n';\n }\n\n const serviceTokenStatus = config.oauth\n ? 'OAuth client credentials are configured for service-level operations. A configured DATABRAIN_SERVICE_TOKEN is used only if token acquisition fails.'\n : config.serviceToken\n ? 'This MCP server has DATABRAIN_SERVICE_TOKEN configured; never ask the user for it.'\n : 'DATABRAIN_SERVICE_TOKEN is not configured. Ask the user to add it to the MCP config only if org-level setup is needed.';\n const apiTokenStatus = client.hasApiToken()\n ? 'This MCP server has a per-data-app API token configured.'\n : config.oauth\n ? 'Created/rotated via create_api_token or rotate_api_token using configured OAuth service credentials, once the backend supports them.'\n : 'Created/rotated via create_api_token or rotate_api_token during setup when DATABRAIN_SERVICE_TOKEN is available.';\n\n const server = new McpServer(\n {\n name: 'databrain',\n version: MCP_SERVER_VERSION,\n description:\n 'Databrain MCP Server — build customer-facing embedded analytics AND run internal analytics on your own data. Supports embed setup, semantic-layer maintenance, natural-language querying, SQL→metric creation (embedded production metrics and unpublished internal workspace metrics), theming, filters, permissions, and guest token generation.',\n },\n {\n instructions:\n operatorBlurb +\n 'Databrain uses two token types — never confuse them:\\n' +\n `• SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, sync_datasource, list_datamarts, create_datamart, list_workspaces, list_data_apps, list_tenants, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status, custom_chart_validate (server check), custom_chart_upload, list_custom_charts, publish_custom_chart. ${serviceTokenStatus}\\n` +\n `• API TOKEN (per data app): scoped to one data app, used for embedding operations (list_embeds, get_embed_details, create_embed, update_embed, generate_guest_token), data-app scoped dashboard listing, widget CRUD/metric creation through guest-token APIs, and list_scheduled_reports. ${apiTokenStatus}\\n\\n` +\n 'FIRST classify the request before calling tools — this avoids wasted turns:\\n' +\n '• (A) EMBEDDING: putting Databrain dashboards/charts in front of YOUR customers (data apps, embeds, guest tokens, per-customer dashboard widgets). Needs an API token and usually a clientId.\\n' +\n '• (B) INTERNAL ANALYTICS: exploring your own data or migrating SQL into your own Databrain metrics for internal use. Workspace-scoped, uses the service token, NO clientId. Creating an unpublished workspace metric (create_workspace_metric) lives here — it is NOT embedding.\\n\\n' +\n '5 core use cases — match the user\\'s scenario to the right prompt:\\n' +\n '1. Embed existing dashboard → prompt: embed-existing-dashboard (list_data_apps → create_api_token → list_dashboards → create_embed → generate_guest_token → generate_embed_code)\\n' +\n '2. Blank dashboard per client → prompt: embed-blank-dashboard (list_data_apps → list_datamarts → create_embed with clientId per client → generate_guest_token → generate_embed_code)\\n' +\n '3. Explore & query data → prompt: query-data (list_datamarts → get_semantic_layer → if empty, auto-generate descriptions with update_semantic_layer or use populate-semantic-layer prompt → ask_question)\\n' +\n '4. SQL → embedded production metric on a customer dashboard (EMBEDDING, requires clientId) → prompt: sql-to-metric-migration (validate_metric_spec → create_or_update_metric_from_query dry_run → apply only with explicit production confirmation)\\n' +\n '5. SQL → unpublished workspace metric (INTERNAL ANALYTICS, no clientId, not attached to any dashboard) → prompt: sql-to-workspace-metric (discover_datasource_schema → create_workspace_metric dry_run → apply with explicit confirmation)\\n\\n' +\n 'Production safety: OAuth may not be available yet. In service-token mode, writes are actor=system. For create_datamart and create_or_update_metric_from_query, dry_run is the default. Apply requires confirm=\"APPLY_TO_PRODUCTION\" after the user reviews the plan. Widget validation and mutation tools must verify the resolved embed is tenant-scoped for the requested clientId; if not, they must refuse to touch it as a likely production/data-app dashboard. Do not apply from a vague request.\\n\\n' +\n 'Semantic models: search_semantic_model, get_semantic_entity, propose_semantic_change, apply_semantic_change, and eval_semantic_model use the configured service credential and datamartId from list_datamarts. For a v2 model, use these tools instead of update_semantic_layer. Search and inspect first. Propose a change, review its errors and plan, then evaluate its returned source. Apply that exact source with its expected_content_hash only after the user supplies confirm=\"APPLY_TO_PRODUCTION\" and dry_run=false. Never supply confirmation yourself. Apply changes the live model for every consumer. A conflict requires a new proposal and review. dry_run defaults to true and never writes.\\n\\n' +\n 'Custom chart types (chart type custom:<key>): custom_chart_scaffold returns a chart project the agent writes to disk, custom_chart_validate checks a manifest and bundle without persisting, custom_chart_upload creates the chart or a preview version only with mode=\"apply\" and confirm=\"APPLY_TO_PRODUCTION\" (dry_run shows the plan first), and publish_custom_chart makes a version live for every builder only with the same confirmation because every dashboard using that key re-renders. Build the bundle with `databrain custom-chart build` (the CLI in this package), never by hand.\\n\\n' +\n 'Infrastructure creation is intentionally narrow in MCP: datasource and workspace creation still happen outside this workflow; sync_datasource and dry-run/apply create_datamart are available. The semantic layer is populated and maintained via MCP tools (get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status). When the semantic layer is empty, use these tools — do NOT send the user to the UI for semantic metadata.\\n\\n' +\n 'After embed setup, customize with: brand-my-embed prompt for theming. Use update_embed (body passthrough) for filters, localization, and permissions.\\n' +\n 'Use generate_guest_token for frontend auth and generate_embed_code for framework snippets.\\n' +\n 'For advanced embed configuration (UI toggles, filters, i18n), use update_embed with a body passthrough.\\n' +\n 'To diagnose embed issues, use get_embed_details and reason about the response.\\n' +\n 'Do NOT ask the user for any tokens — manage them via MCP tools.\\n\\n' +\n 'IMPORTANT — accessSettings defaults:\\n' +\n 'When calling create_embed, only pass accessSettings: { datamartName: \"...\" } plus any flags the user explicitly wants enabled. For multi-tenant dashboard embeds, also pass clientId. The tool auto-fills all required isAllow* fields with secure defaults (all false). Do NOT guess or enumerate accessSettings fields.\\n\\n' +\n 'IMPORTANT — create_data_app does NOT accept workspaceName. Only pass { name: \"...\" }.\\n\\n' +\n 'IMPORTANT — Be interactive. Present results from each tool call and ask the user before proceeding to the next step. Never batch multiple mutating operations without user confirmation.\\n' +\n 'Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, filters, permissions, datasource, datamart, workspace, metric creation.',\n },\n );\n\n registerAllTools(server, client);\n registerAllResources(server);\n registerAllPrompts(server);\n\n return server;\n}\n","import { createHash } from 'node:crypto';\nimport { PostHog } from 'posthog-node';\nimport { instrument } from '@posthog/mcp';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Config } from './config.js';\nimport { DEFAULT_POSTHOG_API_KEY, DEFAULT_POSTHOG_HOST } from './config.js';\nimport { logger } from './logger.js';\n\n// Derives a stable per-install distinct_id from the active auth token without\n// ever sending the token itself to PostHog.\nfunction distinctIdFromToken(token: string | undefined): string | null {\n if (!token) return null;\n return createHash('sha256').update(token).digest('hex').slice(0, 32);\n}\n\n/**\n * Wires PostHog MCP analytics (tool calls, initialize, exceptions) into the\n * server. Always on — not user-configurable. Returns the PostHog client so\n * the caller can flush/shutdown it on exit, or null when no auth token is\n * configured yet (nothing to derive a distinct_id from).\n */\nexport function setupAnalytics(server: McpServer, config: Config): PostHog | null {\n const distinctId = distinctIdFromToken(config.serviceToken || config.apiToken);\n if (!distinctId) {\n return null;\n }\n\n const posthog = new PostHog(config.posthogApiKey || DEFAULT_POSTHOG_API_KEY, {\n host: config.posthogHost || DEFAULT_POSTHOG_HOST,\n });\n\n instrument(server, posthog, {\n logger: (msg) => logger.debug(`[posthog] ${msg}`),\n identify: { distinctId },\n });\n\n return posthog;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/server.ts","../src/utils/analytics.ts"],"sourcesContent":["import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { loadConfig } from './utils/config.js';\nimport { createServer } from './server.js';\nimport { setupAnalytics } from './utils/analytics.js';\nimport { logger } from './utils/logger.js';\nimport { MCP_SERVER_VERSION } from './version.js';\n\nfunction banner(msg: string): void {\n process.stderr.write(`[databrain] ${msg}\\n`);\n}\n\nasync function main(): Promise<void> {\n try {\n if (process.argv.includes('--http')) {\n const { startHosted } = await import('./hosted/main.js');\n await startHosted();\n return;\n }\n const config = loadConfig();\n const server = createServer(config);\n const posthog = setupAnalytics(server, config);\n const transport = new StdioServerTransport();\n\n let shuttingDown = false;\n const flushAnalytics = async (): Promise<void> => {\n if (shuttingDown || !posthog) return;\n shuttingDown = true;\n await posthog.shutdown();\n };\n // StdioServerTransport never observes stdin ending, so the MCP client\n // disconnecting drains the event loop and exits the process without ever\n // calling transport/server onclose. `beforeExit` is what actually fires\n // in that path; posthog-node's flush timers are unref'd so they don't\n // keep the process alive on their own.\n process.on('beforeExit', () => void flushAnalytics());\n process.on('SIGINT', () => void flushAnalytics().then(() => process.exit(0)));\n process.on('SIGTERM', () => void flushAnalytics().then(() => process.exit(0)));\n\n await server.connect(transport);\n banner(`v${MCP_SERVER_VERSION} ready`);\n } catch (error) {\n // An Error serializes to {} in JSON, which left a self-hosted operator with no\n // reason. Config errors name the setting, never its value; the logger also\n // scrubs registered secrets.\n logger.error(`Failed to start server: ${error instanceof Error ? error.message : String(error)}`);\n process.exit(1);\n }\n}\n\nmain();\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { DatabrainClient } from './client/databrain-client.js';\nimport type { Config } from './utils/config.js';\nimport { registerAllTools } from './tools/registry.js';\nimport { registerAllResources } from './resources/registry.js';\nimport { registerAllPrompts } from './prompts/registry.js';\nimport { MCP_SERVER_VERSION } from './version.js';\n\nexport function createServer(config: Config): McpServer {\n const client = new DatabrainClient(config);\n const auth = client.getActiveAuth();\n let operatorBlurb: string;\n if (auth.source === 'oauth-client-credentials') {\n operatorBlurb =\n 'OPERATOR IDENTITY: OAuth client-credentials mode is configured for org-level calls. Tokens identify a service application, not a human operator. The backend must validate the token and map its issuer/subject to an authorized principal. Configuration alone does not prove backend acceptance. Do NOT ask the user for credentials or tokens.\\n\\n';\n } else if (auth.source === 'service-token') {\n operatorBlurb =\n 'OPERATOR IDENTITY: service-token mode. Calls are authenticated at org level — actions in the audit log are tagged actor=system, not a specific user. Per-user identity binding will be added when the OAuth-based login flow ships. Do NOT ask the user for credentials or tokens.\\n\\n';\n } else if (auth.source === 'api-token') {\n operatorBlurb =\n 'OPERATOR IDENTITY: API-token-only mode. A per-data-app API token is configured, but DATABRAIN_SERVICE_TOKEN is not. Embedding operations can use the configured API token, but org-level setup tools require DATABRAIN_SERVICE_TOKEN in the MCP config.\\n\\n';\n } else {\n operatorBlurb =\n 'OPERATOR IDENTITY: no usable token is configured. The MCP server normally refuses this config at startup. Ask the user to add DATABRAIN_SERVICE_TOKEN or DATABRAIN_API_TOKEN to the MCP config before proceeding.\\n\\n';\n }\n\n const serviceTokenStatus = config.oauth\n ? 'OAuth client credentials are configured for service-level operations. A configured DATABRAIN_SERVICE_TOKEN is used only if token acquisition fails.'\n : config.serviceToken\n ? 'This MCP server has DATABRAIN_SERVICE_TOKEN configured; never ask the user for it.'\n : 'DATABRAIN_SERVICE_TOKEN is not configured. Ask the user to add it to the MCP config only if org-level setup is needed.';\n const apiTokenStatus = client.hasApiToken()\n ? 'This MCP server has a per-data-app API token configured.'\n : config.oauth\n ? 'Created/rotated via create_api_token or rotate_api_token using configured OAuth service credentials, once the backend supports them.'\n : 'Created/rotated via create_api_token or rotate_api_token during setup when DATABRAIN_SERVICE_TOKEN is available.';\n\n const server = new McpServer(\n {\n name: 'databrain',\n version: MCP_SERVER_VERSION,\n description:\n 'Databrain MCP Server — build customer-facing embedded analytics AND run internal analytics on your own data. Supports embed setup, semantic-layer maintenance, natural-language querying, SQL→metric creation (embedded production metrics and unpublished internal workspace metrics), theming, filters, permissions, and guest token generation.',\n },\n {\n instructions:\n operatorBlurb +\n 'Databrain uses two token types — never confuse them:\\n' +\n `• SERVICE TOKEN (DATABRAIN_SERVICE_TOKEN): org-level, pre-configured in MCP server config. Used for: list_datasources, discover_datasource_schema, sync_datasource, list_datamarts, create_datamart, list_workspaces, list_data_apps, list_tenants, create_data_app, list_api_tokens, create_api_token, rotate_api_token, list_dashboards (workspace scope), ask_question, get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status, custom_chart_validate (server check), custom_chart_upload, list_custom_charts, publish_custom_chart. ${serviceTokenStatus}\\n` +\n `• API TOKEN (per data app): scoped to one data app, used for embedding operations (list_embeds, get_embed_details, create_embed, update_embed, generate_guest_token), data-app scoped dashboard listing, widget CRUD/metric creation through guest-token APIs, and list_scheduled_reports. ${apiTokenStatus}\\n\\n` +\n 'FIRST classify the request before calling tools — this avoids wasted turns:\\n' +\n '• (A) EMBEDDING: putting Databrain dashboards/charts in front of YOUR customers (data apps, embeds, guest tokens, per-customer dashboard widgets). Needs an API token and usually a clientId.\\n' +\n '• (B) INTERNAL ANALYTICS: exploring your own data or migrating SQL into your own Databrain metrics for internal use. Workspace-scoped, uses the service token, NO clientId. Creating an unpublished workspace metric (create_workspace_metric) lives here — it is NOT embedding.\\n\\n' +\n '5 core use cases — match the user\\'s scenario to the right prompt:\\n' +\n '1. Embed existing dashboard → prompt: embed-existing-dashboard (list_data_apps → create_api_token → list_dashboards → create_embed → generate_guest_token → generate_embed_code)\\n' +\n '2. Blank dashboard per client → prompt: embed-blank-dashboard (list_data_apps → list_datamarts → create_embed with clientId per client → generate_guest_token → generate_embed_code)\\n' +\n '3. Explore & query data → prompt: query-data (list_datamarts → get_semantic_layer → if empty, auto-generate descriptions with update_semantic_layer or use populate-semantic-layer prompt → ask_question)\\n' +\n '4. SQL → embedded production metric on a customer dashboard (EMBEDDING, requires clientId) → prompt: sql-to-metric-migration (validate_metric_spec → create_or_update_metric_from_query dry_run → apply only with explicit production confirmation)\\n' +\n '5. SQL → unpublished workspace metric (INTERNAL ANALYTICS, no clientId, not attached to any dashboard) → prompt: sql-to-workspace-metric (discover_datasource_schema → create_workspace_metric dry_run → apply with explicit confirmation)\\n\\n' +\n 'Production safety: OAuth may not be available yet. In service-token mode, writes are actor=system. For create_datamart and create_or_update_metric_from_query, dry_run is the default. Apply requires confirm=\"APPLY_TO_PRODUCTION\" after the user reviews the plan. Widget validation and mutation tools must verify the resolved embed is tenant-scoped for the requested clientId; if not, they must refuse to touch it as a likely production/data-app dashboard. Do not apply from a vague request.\\n\\n' +\n 'Semantic models: search_semantic_model, get_semantic_entity, propose_semantic_change, apply_semantic_change, and eval_semantic_model use the configured service credential and datamartId from list_datamarts. For a v2 model, use these tools instead of update_semantic_layer. Search and inspect first. Propose a change, review its errors and plan, then evaluate its returned source. Apply that exact source with its expected_content_hash only after the user supplies confirm=\"APPLY_TO_PRODUCTION\" and dry_run=false. Never supply confirmation yourself. Apply changes the live model for every consumer. A conflict requires a new proposal and review. dry_run defaults to true and never writes.\\n\\n' +\n 'Custom chart types (chart type custom:<key>): custom_chart_scaffold returns a draft and project files. In hosted agents, custom_chart_edit sends the description and draft to Chart studio Bryn when installed; if unavailable, edit draft.code yourself. Pass chartTs, manifest, bundleCss and iconSvg for server-side compilation without files or imports. custom_chart_preview shows validated draft code on bounded real sample rows without saving. custom_chart_validate checks a manifest and bundle without persisting, custom_chart_upload creates the chart or a preview version only with mode=\"apply\" and confirm=\"APPLY_TO_PRODUCTION\" (dry_run shows the plan first), and publish_custom_chart makes a version live for every builder only with the same confirmation because every dashboard using that key re-renders. For local projects only, build with `databrain custom-chart build`. A custom metric uses chartType=\"custom:<key>\" and customChart={key,bindings,options}; its MCP App tile loads only the company published bundle in the app/embed sandbox. Hosts without MCP Apps show rows and a clear fallback.\\n\\n' +\n 'Infrastructure creation is intentionally narrow in MCP: datasource and workspace creation still happen outside this workflow; sync_datasource and dry-run/apply create_datamart are available. The semantic layer is populated and maintained via MCP tools (get_semantic_layer, update_semantic_layer, start_semantic_layer_generation, get_semantic_layer_generation_status). When the semantic layer is empty, use these tools — do NOT send the user to the UI for semantic metadata.\\n\\n' +\n 'After embed setup, customize with: brand-my-embed prompt for theming. Use update_embed (body passthrough) for filters, localization, and permissions.\\n' +\n 'Use generate_guest_token for frontend auth and generate_embed_code for framework snippets.\\n' +\n 'For advanced embed configuration (UI toggles, filters, i18n), use update_embed with a body passthrough.\\n' +\n 'To diagnose embed issues, use get_embed_details and reason about the response.\\n' +\n 'Do NOT ask the user for any tokens — manage them via MCP tools.\\n\\n' +\n 'IMPORTANT — accessSettings defaults:\\n' +\n 'When calling create_embed, only pass accessSettings: { datamartName: \"...\" } plus any flags the user explicitly wants enabled. For multi-tenant dashboard embeds, also pass clientId. The tool auto-fills all required isAllow* fields with secure defaults (all false). Do NOT guess or enumerate accessSettings fields.\\n\\n' +\n 'IMPORTANT — create_data_app does NOT accept workspaceName. Only pass { name: \"...\" }.\\n\\n' +\n 'IMPORTANT — Be interactive. Present results from each tool call and ask the user before proceeding to the next step. Never batch multiple mutating operations without user confirmation.\\n' +\n 'Trigger keywords: embed, dashboard, analytics, Databrain, guest token, theme, filters, permissions, datasource, datamart, workspace, metric creation.',\n },\n );\n\n registerAllTools(server, client);\n registerAllResources(server);\n registerAllPrompts(server);\n\n return server;\n}\n","import { createHash } from 'node:crypto';\nimport { PostHog } from 'posthog-node';\nimport { instrument } from '@posthog/mcp';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { Config } from './config.js';\nimport { DEFAULT_POSTHOG_API_KEY, DEFAULT_POSTHOG_HOST } from './config.js';\nimport { logger } from './logger.js';\n\n// Derives a stable per-install distinct_id from the active auth token without\n// ever sending the token itself to PostHog.\nfunction distinctIdFromToken(token: string | undefined): string | null {\n if (!token) return null;\n return createHash('sha256').update(token).digest('hex').slice(0, 32);\n}\n\n/**\n * Wires PostHog MCP analytics (tool calls, initialize, exceptions) into the\n * server. Always on — not user-configurable. Returns the PostHog client so\n * the caller can flush/shutdown it on exit, or null when no auth token is\n * configured yet (nothing to derive a distinct_id from).\n */\nexport function setupAnalytics(server: McpServer, config: Config): PostHog | null {\n const distinctId = distinctIdFromToken(config.serviceToken || config.apiToken);\n if (!distinctId) {\n return null;\n }\n\n const posthog = new PostHog(config.posthogApiKey || DEFAULT_POSTHOG_API_KEY, {\n host: config.posthogHost || DEFAULT_POSTHOG_HOST,\n });\n\n instrument(server, posthog, {\n logger: (msg) => logger.debug(`[posthog] ${msg}`),\n identify: { distinctId },\n });\n\n return posthog;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
registerChartApps
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ZTHGF27R.js";
|
|
5
5
|
import {
|
|
6
6
|
PREVIEW_PATH,
|
|
7
7
|
WRITE_CREATE,
|
|
@@ -9,17 +9,19 @@ import {
|
|
|
9
9
|
registerAllPrompts,
|
|
10
10
|
registerAllResources,
|
|
11
11
|
registerAllTools
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-OLOC2MTA.js";
|
|
13
13
|
import {
|
|
14
14
|
DatabrainApiError,
|
|
15
15
|
DatabrainClient,
|
|
16
16
|
MCP_SERVER_VERSION,
|
|
17
17
|
parseErrorBody,
|
|
18
|
+
parseStudioStream,
|
|
18
19
|
stringify,
|
|
19
20
|
toolContext,
|
|
20
21
|
toolError,
|
|
21
22
|
toolSuccess
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-347U6GXR.js";
|
|
24
|
+
import "./chunk-Q5EYDKSA.js";
|
|
23
25
|
import {
|
|
24
26
|
FilePatStore,
|
|
25
27
|
HOSTED_SCOPES,
|
|
@@ -156,7 +158,7 @@ var TOOL_SCOPES = {
|
|
|
156
158
|
create_dashboard: ["mcp:dashboards:write"],
|
|
157
159
|
get_dashboard: ["mcp:dashboards:read"],
|
|
158
160
|
dashboard_snapshot: ["mcp:dashboards:read"],
|
|
159
|
-
generate_dashboard_tiles: ["mcp:dashboards:write", "mcp:query"],
|
|
161
|
+
generate_dashboard_tiles: ["mcp:dashboards:write", "mcp:query", "mcp:embeds:write"],
|
|
160
162
|
ask_question: ["mcp:query"],
|
|
161
163
|
get_semantic_layer: ["mcp:datamarts:read"],
|
|
162
164
|
update_semantic_layer: ["mcp:datamarts:write"],
|
|
@@ -168,14 +170,18 @@ var TOOL_SCOPES = {
|
|
|
168
170
|
apply_semantic_change: ["mcp:datamarts:write"],
|
|
169
171
|
eval_semantic_model: ["mcp:datamarts:read", "mcp:query"],
|
|
170
172
|
list_widgets: ["mcp:dashboards:read"],
|
|
171
|
-
validate_metric_spec: ["mcp:dashboards:read", "mcp:query"],
|
|
172
|
-
create_or_update_metric_from_query: ["mcp:dashboards:write", "mcp:query"],
|
|
173
|
+
validate_metric_spec: ["mcp:dashboards:read", "mcp:query", "mcp:embeds:write"],
|
|
174
|
+
create_or_update_metric_from_query: ["mcp:dashboards:write", "mcp:query", "mcp:embeds:write"],
|
|
173
175
|
create_workspace_metric: ["mcp:dashboards:write", "mcp:query"],
|
|
174
|
-
add_widget: ["mcp:dashboards:write"],
|
|
175
|
-
update_widget: ["mcp:dashboards:write"],
|
|
176
|
-
remove_widget: ["mcp:dashboards:write"],
|
|
177
|
-
generate_widget: ["mcp:dashboards:write", "mcp:query"],
|
|
176
|
+
add_widget: ["mcp:dashboards:write", "mcp:embeds:write"],
|
|
177
|
+
update_widget: ["mcp:dashboards:write", "mcp:embeds:write"],
|
|
178
|
+
remove_widget: ["mcp:dashboards:write", "mcp:embeds:write"],
|
|
179
|
+
generate_widget: ["mcp:dashboards:write", "mcp:query", "mcp:embeds:write"],
|
|
178
180
|
list_scheduled_reports: ["mcp:dashboards:read"],
|
|
181
|
+
custom_chart_edit: ["mcp:custom-charts:write"],
|
|
182
|
+
custom_chart_preview: ["mcp:custom-charts:read"],
|
|
183
|
+
load_custom_chart_bundle: ["mcp:custom-charts:read"],
|
|
184
|
+
load_custom_chart_runtime: ["mcp:custom-charts:read"],
|
|
179
185
|
custom_chart_scaffold: [],
|
|
180
186
|
custom_chart_validate: ["mcp:custom-charts:read"],
|
|
181
187
|
custom_chart_upload: ["mcp:custom-charts:write"],
|
|
@@ -185,11 +191,12 @@ var TOOL_SCOPES = {
|
|
|
185
191
|
// dashboard save; listed so a hosted composition is not refused for want of a policy.
|
|
186
192
|
preview_chart: ["mcp:query"],
|
|
187
193
|
save_chart_as_dashboard: ["mcp:dashboards:write", "mcp:query"],
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
// Hosted platform reads do not mint embed credentials. Guest rendering retains its backend boundary.
|
|
195
|
+
render_metric_tile: ["mcp:dashboards:read", "mcp:query"],
|
|
196
|
+
render_chart: ["mcp:dashboards:read", "mcp:query"],
|
|
197
|
+
refresh_widget: ["mcp:dashboards:read", "mcp:query"],
|
|
198
|
+
drill_down: ["mcp:dashboards:read", "mcp:query"],
|
|
199
|
+
change_date_range: ["mcp:dashboards:read", "mcp:query"]
|
|
193
200
|
};
|
|
194
201
|
function requiredScopes(name) {
|
|
195
202
|
if (!Object.hasOwn(TOOL_SCOPES, name)) throw new HostedAuthError(403, "tool_not_authorized");
|
|
@@ -203,7 +210,9 @@ var TOOL_ARGUMENT_VALUES = {
|
|
|
203
210
|
list_dashboards: { scope: ["auto", "data_app", "global"] }
|
|
204
211
|
};
|
|
205
212
|
var TOOL_OPAQUE_ARGUMENTS = {
|
|
206
|
-
preview_chart: ["rows"]
|
|
213
|
+
preview_chart: ["rows"],
|
|
214
|
+
custom_chart_preview: ["rows", "manifest"],
|
|
215
|
+
custom_chart_edit: ["draft"]
|
|
207
216
|
};
|
|
208
217
|
function assertToolArguments(name, value, principal, delegated = false) {
|
|
209
218
|
const allowed = Object.hasOwn(TOOL_ARGUMENT_VALUES, name) ? TOOL_ARGUMENT_VALUES[name] : {};
|
|
@@ -229,6 +238,9 @@ var WORKSPACE_FREE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
229
238
|
"list_workspaces",
|
|
230
239
|
"create_workspace",
|
|
231
240
|
"generate_embed_code",
|
|
241
|
+
"custom_chart_preview",
|
|
242
|
+
"load_custom_chart_bundle",
|
|
243
|
+
"load_custom_chart_runtime",
|
|
232
244
|
"custom_chart_scaffold",
|
|
233
245
|
"custom_chart_validate",
|
|
234
246
|
"custom_chart_upload",
|
|
@@ -363,6 +375,9 @@ var HostedClient = class extends DatabrainClient {
|
|
|
363
375
|
// A generated API credential must never replace the authorizing hosted principal.
|
|
364
376
|
setRuntimeApiToken(_token) {
|
|
365
377
|
}
|
|
378
|
+
isHosted() {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
366
381
|
async request(options) {
|
|
367
382
|
try {
|
|
368
383
|
const name = toolContext.getStore()?.tool;
|
|
@@ -371,7 +386,7 @@ var HostedClient = class extends DatabrainClient {
|
|
|
371
386
|
const named = callWorkspace.getStore()?.target;
|
|
372
387
|
const target = named ?? (selectWorkspace(options.body, this.principal, ["workspaceName", "workspaceId"]) ?? selectWorkspace(options.query, this.principal, ["workspaceName", "workspaceId"]));
|
|
373
388
|
const delegated = this.delegation !== void 0;
|
|
374
|
-
const opaque = options.method === "POST" && options.path === PREVIEW_PATH ? ["rows"] : void 0;
|
|
389
|
+
const opaque = options.method === "POST" && options.path === PREVIEW_PATH ? ["rows"] : options.method === "POST" && /^\/api\/v2\/custom-charts\/[a-z][a-z0-9_-]*\/bryn\/turn$/.test(options.path) ? ["draft"] : void 0;
|
|
375
390
|
assertBoundArguments(options.body, this.principal, { target, delegated, opaque });
|
|
376
391
|
assertBoundArguments(options.query, this.principal, { target, delegated });
|
|
377
392
|
let raw = await this.adapter.executeRequest(
|
|
@@ -445,7 +460,7 @@ function createHostedToolServer(principal, adapter, delegation) {
|
|
|
445
460
|
const bound = structuredClone(principal);
|
|
446
461
|
const delegated = delegation !== void 0;
|
|
447
462
|
const server = new McpServer({ name: "databrain", version: MCP_SERVER_VERSION }, {
|
|
448
|
-
instructions: "Hosted Databrain: each request is authenticated as the signed-in user, with that user's own role in each workspace. Use whoami to see the verified identity, scopes and the workspaces this sign-in granted. When more than one workspace is granted, name the workspace on each call (the workspace or workspaceName input). Never ask for a service token or put credentials in tool arguments. Workspace and tenant access come from the bearer principal. Existing dry-run and production-confirmation rules still apply.
|
|
463
|
+
instructions: "Hosted Databrain: each request is authenticated as the signed-in user, with that user's own role in each workspace. Use whoami to see the verified identity, scopes and the workspaces this sign-in granted. When more than one workspace is granted, name the workspace on each call (the workspace or workspaceName input). Never ask for a service token or put credentials in tool arguments. Workspace and tenant access come from the bearer principal. Existing dry-run and production-confirmation rules still apply. For existing saved charts, call list_widgets with dashboardId or dashboardName and no clientId; no embed, data app or datamart is needed. Use render_metric_tile to draw a metricRef directly. Use ask_question for new data questions. Chart controls and save_chart_as_dashboard use the same workspace and role; saving requires the existing production confirmation. Hosts without MCP Apps can read the text answer."
|
|
449
464
|
});
|
|
450
465
|
const register = server.tool.bind(server);
|
|
451
466
|
server.tool = ((...args) => {
|
|
@@ -467,6 +482,7 @@ function createHostedToolServer(principal, adapter, delegation) {
|
|
|
467
482
|
const target = assertToolArguments(name, values[0], bound, delegated);
|
|
468
483
|
if (name === "whoami") return whoami(bound);
|
|
469
484
|
const input = values[0] && typeof values[0] === "object" ? values[0] : void 0;
|
|
485
|
+
if (input?.projectDir !== void 0) return toolError("projectDir is not available on the hosted server. Pass chartTs, manifest, bundleCss and iconSvg to compile in memory.");
|
|
470
486
|
if (!target && !WORKSPACE_FREE_TOOLS.has(name) && !WORKSPACE_SPANNING_TOOLS.has(name) && !namesWorkspace(input)) {
|
|
471
487
|
throw new WorkspaceSelectionError(
|
|
472
488
|
400,
|
|
@@ -678,7 +694,7 @@ var USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION} (hosted)`;
|
|
|
678
694
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
679
695
|
var FORWARDED_HEADERS = /* @__PURE__ */ new Set(["idempotency-key"]);
|
|
680
696
|
var WORKSPACE_HEADER = "X-Databrain-Workspace";
|
|
681
|
-
var GRANT_DENIALS = /* @__PURE__ */ new Set(["ROLE_NOT_ALLOWED", "WORKSPACE_CREATE_NOT_ALLOWED"]);
|
|
697
|
+
var GRANT_DENIALS = /* @__PURE__ */ new Set(["ROLE_NOT_ALLOWED", "WORKSPACE_CREATE_NOT_ALLOWED", "forbidden_permission", "insufficient_scope"]);
|
|
682
698
|
function errorDetail(status, text, bearer) {
|
|
683
699
|
let parsed;
|
|
684
700
|
try {
|
|
@@ -731,7 +747,10 @@ function createProviderAdapter(config) {
|
|
|
731
747
|
redirect: "error",
|
|
732
748
|
signal: AbortSignal.timeout(1e4)
|
|
733
749
|
});
|
|
734
|
-
if (!response.ok)
|
|
750
|
+
if (!response.ok) {
|
|
751
|
+
if (response.status === 429 || response.status >= 500) throw new HostedAuthError(503, "temporarily_unavailable");
|
|
752
|
+
throw new HostedAuthError(401, "invalid_token");
|
|
753
|
+
}
|
|
735
754
|
const { issuer: _issuer, ...principal } = responseSchema.parse(await response.json());
|
|
736
755
|
if (principal.resource !== resource || principal.credentialType !== "oauth") throw new HostedAuthError(401, "invalid_token");
|
|
737
756
|
return principal;
|
|
@@ -793,14 +812,18 @@ function createProviderAdapter(config) {
|
|
|
793
812
|
if (!response.ok) {
|
|
794
813
|
const forbidden = response.status === 403 ? errorDetail(403, text, bearer) : void 0;
|
|
795
814
|
if (forbidden?.code && GRANT_DENIALS.has(forbidden.code)) {
|
|
796
|
-
throw new HostedAuthError(
|
|
815
|
+
throw new HostedAuthError(
|
|
816
|
+
403,
|
|
817
|
+
"delegated_request_denied",
|
|
818
|
+
forbidden.code === "insufficient_scope" && options.path === "/api/v2/guest-token/create" ? { ...forbidden, message: "Guest/tenant operations require mcp:embeds:write. Ask an admin for a role that allows it, then connect again." } : forbidden
|
|
819
|
+
);
|
|
797
820
|
}
|
|
798
|
-
const status = response.status === 401 ? 401 : response.status >= 500 ? 503 : response.status === 403 || response.status === 404 ? 404 : 400;
|
|
821
|
+
const status = response.status === 403 && options.responseFormat === "studio-sse" ? 403 : response.status === 401 ? 401 : response.status >= 500 ? 503 : response.status === 403 || response.status === 404 ? 404 : 400;
|
|
799
822
|
const detail = status === 401 || status === 503 || response.status === 403 ? void 0 : errorDetail(response.status, text, bearer);
|
|
800
823
|
throw new HostedAuthError(status, "delegated_request_denied", detail);
|
|
801
824
|
}
|
|
802
825
|
try {
|
|
803
|
-
return text ? JSON.parse(text) : null;
|
|
826
|
+
return options.responseFormat === "studio-sse" ? parseStudioStream(text) : text ? JSON.parse(text) : null;
|
|
804
827
|
} catch {
|
|
805
828
|
throw new HostedAuthError(503, "delegated_response_invalid");
|
|
806
829
|
}
|
|
@@ -854,4 +877,4 @@ async function startHosted() {
|
|
|
854
877
|
export {
|
|
855
878
|
startHosted
|
|
856
879
|
};
|
|
857
|
-
//# sourceMappingURL=main-
|
|
880
|
+
//# sourceMappingURL=main-IUV24DMP.js.map
|