@databrainhq/mcp-server 0.2.17 → 0.3.0-rc.1

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.
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hosted/types.ts
4
+ var HOSTED_SCOPES = [
5
+ "mcp:dashboards:read",
6
+ "mcp:dashboards:write",
7
+ "mcp:datamarts:read",
8
+ "mcp:datamarts:write",
9
+ "mcp:embeds:write",
10
+ "mcp:query"
11
+ ];
12
+ var HostedAuthError = class extends Error {
13
+ constructor(status, code) {
14
+ super(code);
15
+ this.status = status;
16
+ this.code = code;
17
+ }
18
+ };
19
+
20
+ // src/hosted/auth.ts
21
+ import { createHash, timingSafeEqual } from "crypto";
22
+ import { open } from "fs/promises";
23
+ import { constants } from "fs";
24
+ import { z } from "zod";
25
+ var identity = z.string().min(1).max(512).regex(/^[^\x00-\x1f\x7f]+$/);
26
+ var principalSchema = z.object({
27
+ actorKind: z.literal("user"),
28
+ credentialType: z.enum(["oauth", "pat"]),
29
+ userId: identity,
30
+ companyId: identity,
31
+ workspaceId: identity,
32
+ workspaceName: identity.optional(),
33
+ clientId: identity,
34
+ resource: z.string().url(),
35
+ scopes: z.array(z.enum(HOSTED_SCOPES)),
36
+ expiresAt: z.number().finite().positive().optional(),
37
+ tenantIds: z.array(identity).optional()
38
+ }).strict();
39
+ var recordSchema = z.object({
40
+ hash: z.string().regex(/^[a-f0-9]{64}$/),
41
+ revokedAt: z.number().finite().nullable(),
42
+ principal: principalSchema
43
+ }).strict();
44
+ function hashCredential(token) {
45
+ return createHash("sha256").update(token).digest("hex");
46
+ }
47
+ var FilePatStore = class {
48
+ constructor(path) {
49
+ this.path = path;
50
+ }
51
+ async findByHash(hash) {
52
+ const handle = await open(this.path, constants.O_RDONLY | constants.O_NOFOLLOW);
53
+ try {
54
+ const stat = await handle.stat();
55
+ if (!stat.isFile() || (stat.mode & 63) !== 0 || stat.size > 1048576 || process.getuid && stat.uid !== process.getuid()) throw new Error("Invalid credential store");
56
+ const records = z.array(recordSchema).max(1e3).parse(JSON.parse(await handle.readFile("utf8")));
57
+ const matches = records.filter((record) => timingSafeEqual(Buffer.from(record.hash, "hex"), Buffer.from(hash, "hex")));
58
+ if (matches.length > 1) throw new Error("Ambiguous credential store");
59
+ return matches[0];
60
+ } finally {
61
+ await handle.close();
62
+ }
63
+ }
64
+ };
65
+ function validatePrincipal(value, resource, kind) {
66
+ const parsed = principalSchema.safeParse(value);
67
+ if (!parsed.success || parsed.data.resource !== resource || parsed.data.credentialType !== kind || parsed.data.expiresAt !== void 0 && parsed.data.expiresAt <= Date.now() / 1e3 || kind === "pat" && parsed.success && parsed.data.expiresAt === void 0) throw new HostedAuthError(401, "invalid_token");
68
+ return parsed.data;
69
+ }
70
+ function createBearerVerifier(adapter, resource, pats) {
71
+ return async (token) => {
72
+ try {
73
+ if (!/^[A-Za-z0-9._~+\/-]+=*$/.test(token) || token.length > 8192) throw new HostedAuthError(401, "invalid_token");
74
+ if (!token.startsWith("dbp_")) {
75
+ return validatePrincipal(await adapter.verifyOAuth(token, resource), resource, "oauth");
76
+ }
77
+ if (!/^dbp_[A-Za-z0-9_-]{32,}$/.test(token)) throw new HostedAuthError(401, "invalid_token");
78
+ const record = await pats?.findByHash(hashCredential(token));
79
+ if (!record || record.revokedAt !== null || record.hash !== hashCredential(token)) throw new HostedAuthError(401, "invalid_token");
80
+ const stored = validatePrincipal(record.principal, resource, "pat");
81
+ const resolved = validatePrincipal(await adapter.resolvePatGrant(structuredClone(stored)), resource, "pat");
82
+ for (const key of ["userId", "companyId", "workspaceId", "clientId", "resource"]) {
83
+ if (resolved[key] !== stored[key]) throw new HostedAuthError(401, "invalid_token");
84
+ }
85
+ if (resolved.expiresAt > stored.expiresAt || resolved.scopes.some((scope) => !stored.scopes.includes(scope)) || resolved.tenantIds?.some((id) => !stored.tenantIds?.includes(id))) throw new HostedAuthError(401, "invalid_token");
86
+ return resolved;
87
+ } catch (error) {
88
+ if (error instanceof HostedAuthError) throw error;
89
+ throw new HostedAuthError(401, "invalid_token");
90
+ }
91
+ };
92
+ }
93
+
94
+ export {
95
+ HOSTED_SCOPES,
96
+ HostedAuthError,
97
+ principalSchema,
98
+ hashCredential,
99
+ FilePatStore,
100
+ createBearerVerifier
101
+ };
102
+ //# sourceMappingURL=chunk-JCXUZ3QZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hosted/types.ts","../src/hosted/auth.ts"],"sourcesContent":["import type { RequestOptions } from '../client/types.js';\n\nexport const HOSTED_SCOPES = [\n 'mcp:dashboards:read', 'mcp:dashboards:write',\n 'mcp:datamarts:read', 'mcp:datamarts:write', 'mcp:embeds:write', 'mcp:query',\n] as const;\nexport type HostedScope = typeof HOSTED_SCOPES[number];\n\n/** Only the trusted auth adapter may produce this context. Never populate it from MCP input. */\nexport interface HostedPrincipal {\n actorKind: 'user';\n credentialType: 'oauth' | 'pat';\n userId: string;\n companyId: string;\n workspaceId: string;\n workspaceName?: string;\n /** OAuth application ID, NOT the tenant identifier called clientId by legacy tools. */\n clientId: string;\n resource: string;\n scopes: HostedScope[];\n /** Required for stored PATs. OAuth may delegate expiry to fresh provider validation. */\n expiresAt?: number;\n /** Explicit tenant allow-list resolved by the backend; absent means no tenant access. */\n tenantIds?: string[];\n}\n\nexport interface PatRecord {\n hash: string;\n revokedAt: number | null;\n principal: HostedPrincipal;\n}\n\nexport interface PatStore {\n findByHash(hash: string): Promise<PatRecord | undefined>;\n}\n\n/** Production integration must recheck membership, policy and revocation on every call. */\nexport interface HostedAdapter {\n verifyOAuth(token: string, resource: string): Promise<HostedPrincipal>;\n resolvePatGrant(principal: HostedPrincipal): Promise<HostedPrincipal>;\n /**\n * Execute as this user/workspace with backend resource and tenant checks and auditing.\n * Receives NO incoming bearer or server service credential. Credential exchange, if\n * needed, belongs in the trusted adapter. Do not impersonate a static service token.\n * Return the raw API result: the shared client performs its usual data unwrapping.\n */\n executeRequest(options: RequestOptions, principal: HostedPrincipal): Promise<unknown>;\n}\n\nexport interface HostedConfig {\n publicUrl: string;\n issuer: string;\n port: number;\n testOnly: boolean;\n}\n\nexport class HostedAuthError extends Error {\n constructor(public readonly status: 401 | 403 | 503, public readonly code: string) {\n super(code);\n }\n}\n","import { createHash, timingSafeEqual } from 'node:crypto';\nimport { open } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport { z } from 'zod';\nimport { HOSTED_SCOPES, HostedAuthError } from './types.js';\nimport type { HostedAdapter, HostedPrincipal, PatRecord, PatStore } from './types.js';\n\nconst identity = z.string().min(1).max(512).regex(/^[^\\x00-\\x1f\\x7f]+$/);\nexport const principalSchema = z.object({\n actorKind: z.literal('user'), credentialType: z.enum(['oauth', 'pat']),\n userId: identity, companyId: identity, workspaceId: identity, workspaceName: identity.optional(),\n clientId: identity, resource: z.string().url(), scopes: z.array(z.enum(HOSTED_SCOPES)),\n expiresAt: z.number().finite().positive().optional(), tenantIds: z.array(identity).optional(),\n}).strict();\nconst recordSchema = z.object({\n hash: z.string().regex(/^[a-f0-9]{64}$/), revokedAt: z.number().finite().nullable(),\n principal: principalSchema,\n}).strict();\n\nexport function hashCredential(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\n/** Reread on every request: an atomic file replacement revokes immediately, without restart. */\nexport class FilePatStore implements PatStore {\n constructor(private readonly path: string) {}\n async findByHash(hash: string): Promise<PatRecord | undefined> {\n const handle = await open(this.path, constants.O_RDONLY | constants.O_NOFOLLOW);\n try {\n const stat = await handle.stat();\n if (!stat.isFile() || (stat.mode & 0o077) !== 0 || stat.size > 1_048_576 ||\n (process.getuid && stat.uid !== process.getuid())) throw new Error('Invalid credential store');\n const records = z.array(recordSchema).max(1000).parse(JSON.parse(await handle.readFile('utf8')));\n const matches = records.filter(record => timingSafeEqual(Buffer.from(record.hash, 'hex'), Buffer.from(hash, 'hex')));\n // Duplicate records are ambiguous, including an active/revoked pair.\n if (matches.length > 1) throw new Error('Ambiguous credential store');\n return matches[0];\n } finally { await handle.close(); }\n }\n}\n\nfunction validatePrincipal(value: unknown, resource: string, kind: 'oauth' | 'pat'): HostedPrincipal {\n const parsed = principalSchema.safeParse(value);\n if (!parsed.success || parsed.data.resource !== resource || parsed.data.credentialType !== kind ||\n (parsed.data.expiresAt !== undefined && parsed.data.expiresAt <= Date.now() / 1000) ||\n (kind === 'pat' && parsed.success && parsed.data.expiresAt === undefined)) throw new HostedAuthError(401, 'invalid_token');\n return parsed.data;\n}\n\nexport function createBearerVerifier(adapter: HostedAdapter, resource: string, pats?: PatStore) {\n return async (token: string): Promise<HostedPrincipal> => {\n try {\n if (!/^[A-Za-z0-9._~+\\/-]+=*$/.test(token) || token.length > 8192) throw new HostedAuthError(401, 'invalid_token');\n if (!token.startsWith('dbp_')) {\n return validatePrincipal(await adapter.verifyOAuth(token, resource), resource, 'oauth');\n }\n if (!/^dbp_[A-Za-z0-9_-]{32,}$/.test(token)) throw new HostedAuthError(401, 'invalid_token');\n const record = await pats?.findByHash(hashCredential(token));\n if (!record || record.revokedAt !== null || record.hash !== hashCredential(token)) throw new HostedAuthError(401, 'invalid_token');\n const stored = validatePrincipal(record.principal, resource, 'pat');\n const resolved = validatePrincipal(await adapter.resolvePatGrant(structuredClone(stored)), resource, 'pat');\n // Membership resolution may narrow a stored grant, never move or expand it.\n for (const key of ['userId', 'companyId', 'workspaceId', 'clientId', 'resource'] as const) {\n if (resolved[key] !== stored[key]) throw new HostedAuthError(401, 'invalid_token');\n }\n if (resolved.expiresAt! > stored.expiresAt! || resolved.scopes.some(scope => !stored.scopes.includes(scope)) ||\n resolved.tenantIds?.some(id => !stored.tenantIds?.includes(id))) throw new HostedAuthError(401, 'invalid_token');\n return resolved;\n } catch (error) {\n if (error instanceof HostedAuthError) throw error;\n // No raw verifier, filesystem or remote errors can disclose credential values.\n throw new HostedAuthError(401, 'invalid_token');\n }\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ APP_MIME,
4
+ CHART_URI,
5
+ DatabrainClient,
6
+ KPI_URI,
7
+ MCP_SERVER_VERSION,
8
+ READ_ONLY,
9
+ chartResult,
10
+ createServiceBundle,
11
+ enableChartApps,
12
+ pageSchema,
13
+ referenceSchema,
14
+ registerAllPrompts,
15
+ registerAllResources,
16
+ registerAllTools,
17
+ toolContext,
18
+ viewSchema
19
+ } from "./chunk-7DS7OLWP.js";
20
+
21
+ // src/apps/server.ts
22
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23
+
24
+ // src/apps/registry.ts
25
+ import { z } from "zod";
26
+
27
+ // src/apps/backend.ts
28
+ var RENDER_PATH = "/api/v2/dashboard/mcp-chart";
29
+ var UncachedChartClient = class extends DatabrainClient {
30
+ constructor(delegate) {
31
+ super({ apiUrl: delegate.getApiUrl(), demoDomain: delegate.getDemoDomain() });
32
+ this.delegate = delegate;
33
+ }
34
+ request(options) {
35
+ return this.delegate.request({ ...options, cacheKey: void 0, cacheTtlMs: void 0 });
36
+ }
37
+ };
38
+ function createChartBackend(client) {
39
+ const services = createServiceBundle(new UncachedChartClient(client));
40
+ return {
41
+ async execute(action, rawReference, rawView) {
42
+ const reference = referenceSchema.parse(rawReference);
43
+ const view = viewSchema.parse(rawView);
44
+ const app = await services.widgetService.resolveDataAppName(void 0, reference.embedId);
45
+ if (!app.ok) throw new Error("Chart scope unavailable");
46
+ const scope = await services.widgetService.ensureTenantDashboardScope({ ...reference, dataAppName: app.value });
47
+ if (!scope.ok) throw new Error("Chart scope denied");
48
+ const { token } = await services.guestTokenService.generateToken({
49
+ clientId: reference.clientId,
50
+ dataAppName: app.value,
51
+ allowedEmbeds: [reference.embedId],
52
+ permissions: {
53
+ isDisableMetricCreation: true,
54
+ isEnableManageMetrics: false,
55
+ isEnableMetricUpdation: false,
56
+ isEnableArchiveMetrics: false
57
+ }
58
+ });
59
+ if (!token) throw new Error("Guest scope unavailable");
60
+ const page = await client.request({
61
+ method: "POST",
62
+ path: RENDER_PATH,
63
+ auth: "none",
64
+ token,
65
+ body: {
66
+ action,
67
+ embedId: reference.embedId,
68
+ widgetId: reference.widgetId,
69
+ ...view,
70
+ pageSize: 50,
71
+ width: 600,
72
+ height: 320,
73
+ animation: false
74
+ },
75
+ skipDataUnwrap: true
76
+ });
77
+ return pageSchema.parse(page);
78
+ }
79
+ };
80
+ }
81
+
82
+ // src/apps/tile.ts
83
+ var TILE_SCRIPT = String.raw`(()=>{
84
+ const $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();
85
+ let seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null;
86
+ function send(message){if(!closed)parent.postMessage({jsonrpc:'2.0',...message},origin)}
87
+ function rpc(method,params){return new Promise((resolve,reject)=>{const id=++seq;
88
+ const timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'))},30000);
89
+ pending.set(id,{resolve,reject,timer});send({id,method,params})})}
90
+ function status(text,bad){const s=$('status');s.textContent=text;s.toggleAttribute('data-error',!!bad);if(bad)$('plot').dataset.state='off'}
91
+ function paint(){const img=$('chart-image');if(img&&rawSvg)img.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\/svg>\s*$/i,'<style>text{fill:'+getComputedStyle(document.body).color+'!important}</style></svg>'))}
92
+ function controls(){document.querySelectorAll('button,input,select').forEach(e=>{e.disabled=busy});
93
+ $('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref;$('plot').toggleAttribute('data-busy',busy);
94
+ $('expand').hidden=!context.availableDisplayModes?.includes('fullscreen');
95
+ root.toggleAttribute('data-deep',context.displayMode==='fullscreen'||$('expand').hidden)}
96
+ function host(change){context={...context,...change};if(change.theme==='dark'||change.theme==='light')root.style.colorScheme=change.theme;
97
+ const vars=change.styles?.variables||{};for(const [key,value] of Object.entries(vars)){
98
+ if(/^--(?:color-|font-|border-|shadow-)/.test(key)&&typeof value==='string'&&!/url\s*\(|@import/i.test(value))root.style.setProperty(key,value)}
99
+ const face=change.styles?.css?.fonts;if(typeof face==='string'&&face.length<4000&&!/@import|javascript:/i.test(face))$('hostfont').textContent=face;
100
+ const insets=change.safeAreaInsets;if(insets)for(const edge of ['top','right','bottom','left']){
101
+ const n=insets[edge];if(Number.isFinite(n))root.style.setProperty('--safe-'+edge,Math.max(0,Math.min(n,200))+'px')}
102
+ root.dataset.mode=context.displayMode==='fullscreen'?'full':'inline';
103
+ $('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();controls()}
104
+ function resize(){if(ready)send({method:'ui/notifications/size-changed',params:{width:document.body.scrollWidth,height:document.body.scrollHeight}})}
105
+ function table(rows,columns){$('rows').replaceChildren();$('data').hidden=!rows?.length;if(!rows?.length)return;
106
+ $('count').textContent=rows.length===1?'Show the 1 row behind this':'Show the '+rows.length+' rows behind this';
107
+ const t=document.createElement('table');
108
+ const header=document.createElement('tr');for(const name of columns){const th=document.createElement('th');th.scope='col';th.textContent=name;header.append(th)}
109
+ const head=document.createElement('thead');head.append(header);t.append(head);const body=document.createElement('tbody');
110
+ for(const row of rows.slice(0,50)){const tr=document.createElement('tr');for(const key of columns){const td=document.createElement('td');td.textContent=String(row[key]??'');tr.append(td)}body.append(tr)}t.append(body);$('rows').append(t)}
111
+ async function display(mode){try{const answer=await rpc('ui/request-display-mode',{mode});
112
+ host({displayMode:answer.mode||context.displayMode});if(root.dataset.mode==='full'&&!$('data').hidden)$('data').open=true;resize()}
113
+ catch{status('Expanded view is unavailable in this host.',1)}}
114
+ function wait(){busy=true;if(!$('plot').firstChild)$('plot').dataset.state='wait';status('Loading chart…');controls()}
115
+ async function call(name,extra={}){if(!ready||!ref||busy)return;wait();const rev=++revision;
116
+ try{const result=await rpc('tools/call',{name,arguments:{reference:ref,view:{...view,...extra},context:'User is interacting with the chart tile.'}});
117
+ if(rev===revision)render(result)}catch{if(rev===revision)status('Could not load the chart. Select Refresh to try again.',1)}finally{busy=false;controls()}}
118
+ function render(result){busy=false;controls();
119
+ if(result?.isError){status('Chart unavailable. Check your access to this dashboard, then select Refresh.',1);return}
120
+ const p=result?.structuredContent;if(!p||typeof p!=='object'){status('No chart data came back for this result.',1);return}
121
+ revision++;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();$('tip').textContent='';delete $('tip').dataset.value;
122
+ ref=p.reference||null;view=p.view||{};next=p.nextCursor||null;
123
+ $('title').textContent=p.title||'Analytics';$('description').textContent=p.description||'';
124
+ if(p.kind==='dashboard'){$('plot').dataset.state='off';
125
+ for(const item of p.widgets||[]){const b=document.createElement('button');b.textContent=item.name||'Untitled widget';b.onclick=()=>{ref=item.reference;view={};call('render_chart')};$('choices').append(b)}
126
+ status(p.widgets?.length?'Choose a chart to explore.':'This dashboard has no charts yet.');
127
+ }else if(p.kind==='kpi'){
128
+ const number=document.createElement('p');number.className='kpi';number.textContent=String(p.value??'—');$('plot').append(number);status('');
129
+ }else if(p.svg){
130
+ const box=document.createElement('div');box.className='chart';box.style.aspectRatio=p.width+'/'+p.height;
131
+ const img=document.createElement('img');img.id='chart-image';rawSvg=p.svg;img.alt=p.title||'Chart';img.onerror=()=>{status('Chart image unavailable. The data is in the table below.',1);$('data').open=$('data').toggleAttribute('data-only',true)};box.append(img);
132
+ for(const point of p.points||[]){const b=document.createElement('button');b.className='point';b.setAttribute('aria-label',point.label+': '+point.value);b.setAttribute('aria-pressed','false');
133
+ b.style.left=100*point.x/p.width+'%';b.style.top=100*point.y/p.height+'%';b.style.width=100*point.width/p.width+'%';b.style.height=100*point.height/p.height+'%';
134
+ const hover=()=>{$('tip').textContent=point.label+': '+point.value;$('tip').dataset.value='1'};b.onmouseenter=hover;b.onfocus=hover;
135
+ b.onclick=()=>{box.querySelectorAll('.point').forEach(e=>e.setAttribute('aria-pressed','false'));b.setAttribute('aria-pressed','true');hover();
136
+ $('choices').replaceChildren();if(point.drillId){const drill=document.createElement('button');drill.className='primary';drill.textContent='Explore selected data';drill.onclick=()=>call('drill_down',{drillId:point.drillId,cursor:undefined});$('choices').append(drill)}};box.append(b)}$('plot').append(box);
137
+ status(p.rows?.length?'':'This chart has no data for the current view.');if(p.points?.length)$('tip').textContent='Select part of the chart to read its value.';
138
+ }else{$('plot').dataset.state='off';status(p.rows?.length?'Chart preview is unavailable. The data is in the table below.':'No data for this view.',!p.rows?.length)}
139
+ paint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));
140
+ const only=!p.svg&&p.value===undefined;$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}
141
+ $('refresh').onclick=()=>call('refresh_widget',{cursor:undefined});$('next').onclick=()=>call('render_chart',{cursor:next});
142
+ $('expand').onclick=()=>display(context.displayMode==='fullscreen'?'inline':'fullscreen');
143
+ $('dates').onclick=()=>{const from=$('from').value,to=$('to').value;if(!from||!to||from>to){status('Choose a start date on or before the end date.',1);return}call('change_date_range',{dateRange:{from,to},cursor:undefined,drillId:undefined})};
144
+ function receive(event){if(event.source!==parent||closed)return;const m=event.data;
145
+ if(!m||Array.isArray(m)||m.jsonrpc!=='2.0'||(origin!=='*'&&event.origin!==origin))return;
146
+ if(!m.method&&pending.has(m.id)&&('result'in m||'error'in m)){if(origin==='*'&&event.origin!=='null')origin=event.origin;
147
+ const p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);m.error?p.reject(Error('host error')):p.resolve(m.result);return}
148
+ if(m.method==='ping'&&m.id!==undefined){send({id:m.id,result:{}});return}
149
+ if(m.method==='ui/resource-teardown'){send({id:m.id,result:{}});closed=true;revision++;for(const p of pending.values()){clearTimeout(p.timer);p.reject(Error('closed'))}pending.clear();observer.disconnect();removeEventListener('message',receive);return}
150
+ if(!ready||m.id!==undefined)return;
151
+ if(m.method==='ui/notifications/tool-result'){render(m.params)}
152
+ else if(m.method==='ui/notifications/tool-input'){wait()}
153
+ else if(m.method==='ui/notifications/host-context-changed'){host(m.params||{});resize()}
154
+ else if(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Chart request cancelled.');controls()}}
155
+ addEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();
156
+ rpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain chart tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})
157
+ .then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;send({method:'ui/notifications/initialized'});resize()})
158
+ .catch(()=>status('This chart needs a host that supports MCP Apps.',1));
159
+ })();`;
160
+ function tileHtml(kind = "chart") {
161
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
162
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'">
163
+ <title>Databrain ${kind} tile</title><style id="hostfont"></style><style>
164
+ /* Host tokens are the Claude MCP Apps style-variable names. Every one carries a fallback:
165
+ hosts may ship any subset, so nothing here may depend on a token existing. */
166
+ :root{color-scheme:light dark;
167
+ --dbn-bg:var(--color-background-primary,Canvas);--dbn-text:var(--color-text-primary,CanvasText);
168
+ --dbn-mute:var(--color-text-secondary,GrayText);--dbn-faint:var(--color-text-tertiary,GrayText);
169
+ --dbn-edge:var(--color-border-secondary,ButtonBorder);--dbn-line:var(--color-border-tertiary,var(--dbn-edge));
170
+ --dbn-accent:var(--color-text-info,LinkText);--dbn-ring:var(--color-ring-primary,var(--dbn-accent));
171
+ --dbn-bad:var(--color-text-danger,var(--dbn-text));--dbn-bad-edge:var(--color-border-danger,currentColor);
172
+ --dbn-quiet:color-mix(in srgb,currentColor 6%,transparent);
173
+ --bw:var(--border-width-regular,1px);--r-s:var(--border-radius-sm,6px);--r-m:var(--border-radius-md,8px);--r-l:var(--border-radius-lg,10px);
174
+ --med:var(--font-weight-medium,500);--semi:var(--font-weight-semibold,600);
175
+ /* One curve. Quick / standard / display-mode. Corporate: no overshoot. */
176
+ --e:cubic-bezier(.2,0,0,1);--q:120ms;--s:300ms;--l:400ms}
177
+ *{box-sizing:border-box}
178
+ body{margin:0;padding:var(--safe-top,0px) var(--safe-right,0px) var(--safe-bottom,0px) var(--safe-left,0px);background:var(--dbn-bg);color:var(--dbn-text);
179
+ font:var(--font-text-sm-size,14px)/var(--font-text-sm-line-height,1.4) var(--font-sans,system-ui)}
180
+ main{padding:20px;max-width:1200px;margin:auto;transition:padding var(--l) var(--e)}
181
+ header{display:flex;align-items:start;justify-content:space-between;gap:12px}header>div{min-width:0}
182
+ h1{font-size:var(--font-heading-md-size,18px);line-height:var(--font-heading-md-line-height,1.4);margin:0 0 6px;font-weight:var(--semi);letter-spacing:-.01em;overflow-wrap:anywhere;transition:font-size var(--l) var(--e)}
183
+ p{margin:6px 0;color:var(--dbn-mute);overflow-wrap:anywhere}#expand{flex:none}
184
+ button,input{font:inherit;color:inherit;background:var(--dbn-bg);border:var(--bw) solid var(--dbn-edge);border-radius:var(--r-s);padding:7px 10px}
185
+ button{cursor:pointer;transition:background-color var(--q) var(--e),border-color var(--q) var(--e)}
186
+ input{max-width:160px;min-width:0}button:hover:not(:disabled){background:var(--dbn-quiet)}button:disabled{color:var(--dbn-faint);cursor:default}
187
+ button:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--dbn-ring);outline-offset:2px}
188
+ #choices button.primary{border-color:var(--dbn-accent);font-weight:var(--med)}
189
+ #status:empty{margin:0}
190
+ #status[data-error]{color:var(--dbn-bad);font-weight:var(--med);border-left:2px solid var(--dbn-bad-edge);background:var(--color-background-danger,transparent);border-radius:0 var(--r-s) var(--r-s) 0;padding:8px 10px;margin:8px 0}
191
+ /* Reserved plot. The skeleton is a static bar silhouette matching the 15:8 backend render:
192
+ Claude's guidance asks for a skeleton that matches the final layout, and bans nothing here,
193
+ but shimmer is banned by our own motion budget - so this paints once and never repaints. */
194
+ #plot{position:relative;transition:opacity var(--q) var(--e)}#plot[data-busy]{opacity:.55}
195
+ #plot[data-state=wait]{aspect-ratio:15/8}
196
+ #plot[data-state=wait]::before,#plot[data-state=ready]::before{content:'';position:absolute;pointer-events:none;inset:16% 6% 13%;
197
+ border-bottom:var(--bw) solid var(--dbn-line);transition:opacity var(--s) var(--e);
198
+ background:linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 0 100%/11% 44% no-repeat,
199
+ linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 22% 100%/11% 71% no-repeat,
200
+ linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 44% 100%/11% 57% no-repeat,
201
+ linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 66% 100%/11% 86% no-repeat,
202
+ linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 88% 100%/11% 33% no-repeat}
203
+ #plot[data-state=ready]::before{opacity:0}
204
+ .chart{position:relative;width:100%;margin:12px 0}.chart img{width:100%;height:100%;display:block}
205
+ .point{position:absolute;padding:0;border:0;background:transparent;border-radius:var(--border-radius-xs,4px);
206
+ outline:2px solid transparent;outline-offset:1px;transition:background-color var(--q) var(--e),outline-color var(--q) var(--e)}
207
+ .point:hover{background:color-mix(in srgb,currentColor 12%,transparent)}
208
+ .point[aria-pressed=true]{outline-color:var(--dbn-accent);background:color-mix(in srgb,var(--dbn-accent) 18%,transparent)}
209
+ .kpi{font-size:clamp(34px,10vw,44px);line-height:1.1;font-weight:var(--semi);letter-spacing:-.025em;font-variant-numeric:tabular-nums;color:var(--dbn-text);margin:14px 0 6px;transition:font-size var(--l) var(--e)}
210
+ #tip{color:var(--dbn-mute);font-size:var(--font-text-xs-size,12px);overflow-wrap:anywhere;min-height:1.4em}
211
+ #tip[data-value]{color:var(--dbn-text);font-weight:var(--med);font-variant-numeric:tabular-nums}
212
+ #choices,footer,.dates{display:flex;gap:8px;flex-wrap:wrap;align-items:center}#choices{margin:10px 0 0}footer{margin-top:12px}.dates{margin:8px 0 2px}
213
+ label{display:flex;align-items:center;gap:6px;color:var(--dbn-mute)}
214
+ #range{margin-top:10px}#data{margin-top:2px}#data[hidden],#range[hidden]{display:none}
215
+ html:not([data-deep]) #range,html:not([data-deep]) #data:not([data-only]){display:none}
216
+ summary{cursor:pointer;color:var(--dbn-mute);font-size:var(--font-text-xs-size,12px);padding:5px 0;width:fit-content;transition:color var(--q) var(--e)}summary:hover{color:var(--dbn-text)}
217
+ #rows{overflow:auto;max-height:260px;border-radius:var(--r-s)}
218
+ table{border-collapse:collapse;width:100%;font-size:var(--font-text-xs-size,12px)}
219
+ th,td{text-align:left;padding:8px;border-bottom:var(--bw) solid var(--dbn-line);overflow-wrap:anywhere}
220
+ td:last-child{font-variant-numeric:tabular-nums}th{font-weight:var(--semi);position:sticky;top:0;background:var(--dbn-bg);color:var(--dbn-mute)}
221
+ /* Inline to fullscreen is a layout morph, not a new screen: main widens over 400ms on the
222
+ one curve and every dependent box follows it. Fullscreen is the detailed view, so the
223
+ table loses its inline height cap. */
224
+ /* The SVG is rendered at 600x320 and scaled by width, so an unbounded fullscreen column
225
+ makes the chart's own axis type larger than the tile's heading. Hold a measure. */
226
+ [data-mode=full] main{padding:28px 32px;max-width:900px}
227
+ [data-mode=full] h1{font-size:var(--font-heading-xl-size,24px);line-height:var(--font-heading-xl-line-height,1.25)}
228
+ [data-mode=full] .kpi{font-size:clamp(44px,7vw,72px)}
229
+ [data-mode=full] #rows{max-height:none}
230
+ /* Touch: the host gives vertical pan to the conversation, so a nested scroller is a trap. */
231
+ @media(pointer:coarse){button,input{min-height:44px}summary{padding:12px 0}#rows{max-height:none;overflow:visible}}
232
+ @media(max-width:480px){main{padding:14px}}
233
+ /* Entrance. Content first, chrome 60ms later: 300+60 = 360ms, inside the 400ms budget.
234
+ No stagger past two groups - a chat tile that cascades reads as lag, not luxury. */
235
+ @keyframes rise{from{opacity:0;transform:translateY(6px)}}
236
+ @keyframes land{from{opacity:0;transform:translateY(4px) scale(.995)}}
237
+ header,#status,#plot{animation:rise var(--s) var(--e) both}
238
+ #tip,#choices,footer,#range,#data{animation:rise var(--s) var(--e) 60ms both}
239
+ .chart,.kpi{animation:land var(--s) var(--e) both}
240
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}
241
+ </style></head><body><main id="tile"><header><div><h1 id="title">Analytics</h1><p id="description"></p></div><button id="expand" hidden>Expand</button></header>
242
+ <p id="status" role="status">Loading chart\u2026</p><div id="plot" data-state="wait"></div><div id="tip"></div><div id="choices"></div>
243
+ <footer><button id="refresh" hidden>Refresh</button><button id="next" hidden>Next page</button></footer>
244
+ <details id="range" hidden><summary>Change the date range</summary><div class="dates"><label>From <input id="from" type="date"></label><label>To <input id="to" type="date"></label><button id="dates" type="button">Apply dates</button></div></details>
245
+ <details id="data" hidden><summary id="count">Show the data behind this</summary><div id="rows" tabindex="0" role="region" aria-label="Chart data"></div></details></main><script>${TILE_SCRIPT}</script></body></html>`;
246
+ }
247
+
248
+ // src/apps/registry.ts
249
+ var actions = {
250
+ render_chart: "Render one authorized chart page as SVG with point metadata. Cursor pagination stays within the same guest scope.",
251
+ refresh_widget: "Refresh a chart in the current guest scope. Revalidates access before returning data.",
252
+ drill_down: "Explore a server-issued drill identifier within the current chart and guest scope.",
253
+ change_date_range: "Render a chart for an ordered date range within the current guest scope."
254
+ };
255
+ function registerChartApps(server, client, backend = createChartBackend(client)) {
256
+ for (const [name, description] of Object.entries(actions)) {
257
+ const action = name;
258
+ const tool = server.tool(name, description, {
259
+ reference: referenceSchema,
260
+ view: viewSchema.default({}),
261
+ context: z.string().max(200).optional().describe("Chart interaction context")
262
+ }, READ_ONLY, async ({ reference, view }) => {
263
+ try {
264
+ if (action === "drill_down" && !view.drillId || action === "change_date_range" && !view.dateRange) throw new Error("Missing view selector");
265
+ const page = pageSchema.parse(await toolContext.run(
266
+ { tool: name, category: "embedding", workflowIntent: "chart_app", inputMode: "app" },
267
+ () => backend.execute(action, reference, view)
268
+ ));
269
+ if (page.svg && (!/^\s*<svg\b/i.test(page.svg) || /<\s*(?:script|foreignObject|iframe|image|animate\w*|set)\b|<!DOCTYPE|<!ENTITY|\bon\w+\s*=|@(?:import|keyframes)|\banimation\s*:|(?:href\s*=\s*["'](?!#)|url\s*\(\s*["']?(?!#))/i.test(page.svg))) {
270
+ throw new Error("Unsafe SVG");
271
+ }
272
+ return chartResult(`${page.kind === "kpi" ? "KPI" : "Chart"} ready. ${page.rows.length} rows in this page.`, {
273
+ ...page,
274
+ reference,
275
+ view
276
+ });
277
+ } catch {
278
+ return { isError: true, content: [{
279
+ type: "text",
280
+ text: "Chart unavailable. Check dashboard access and the chart renderer integration, then retry."
281
+ }] };
282
+ }
283
+ });
284
+ tool.update({ _meta: { ui: { resourceUri: CHART_URI, visibility: ["app"] } } });
285
+ }
286
+ for (const [uri, kind] of [[CHART_URI, "chart"], [KPI_URI, "kpi"]]) {
287
+ server.resource(`Databrain ${kind} tile`, uri, { mimeType: APP_MIME, description: `Interactive ${kind} using host theme and scoped server tools` }, async () => ({
288
+ contents: [{ uri, mimeType: APP_MIME, text: tileHtml(kind), _meta: { ui: { prefersBorder: true } } }]
289
+ }));
290
+ }
291
+ }
292
+
293
+ // src/apps/server.ts
294
+ function createChartAppServer(config, backend) {
295
+ const client = new DatabrainClient(config);
296
+ const server = new McpServer({ name: "databrain-chart-apps", version: MCP_SERVER_VERSION }, {
297
+ instructions: "Use read-only chart previews to inspect analytics. Chart interactions inherit guest scope. Never request or expose credentials. Mutation tools retain their existing dry-run and confirmation requirements."
298
+ });
299
+ enableChartApps(server);
300
+ registerAllTools(server, client);
301
+ registerAllResources(server);
302
+ registerAllPrompts(server);
303
+ registerChartApps(server, client, backend);
304
+ return server;
305
+ }
306
+
307
+ export {
308
+ createChartAppServer
309
+ };
310
+ //# sourceMappingURL=chunk-NDVW3ZSJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apps/server.ts","../src/apps/registry.ts","../src/apps/backend.ts","../src/apps/tile.ts"],"sourcesContent":["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';\nimport { enableChartApps } from './feature.js';\nimport { registerChartApps } from './registry.js';\nimport type { ChartBackend } from './backend.js';\n\n/** Reviewable composition seam; hosted owners must retain their principal-bound client/policy. */\nexport function createChartAppServer(config: Config, backend?: ChartBackend): McpServer {\n const client = new DatabrainClient(config);\n const server = new McpServer({ name: 'databrain-chart-apps', version: MCP_SERVER_VERSION }, {\n instructions: 'Use read-only chart previews to inspect analytics. Chart interactions inherit guest scope. ' +\n 'Never request or expose credentials. Mutation tools retain their existing dry-run and confirmation requirements.',\n });\n enableChartApps(server);\n registerAllTools(server, client);\n registerAllResources(server);\n registerAllPrompts(server);\n registerChartApps(server, client, backend);\n return server;\n}\n","import { z } from 'zod';\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { DatabrainClient } from '../client/databrain-client.js';\nimport { READ_ONLY } from '../tools/annotations.js';\nimport { toolContext } from '../utils/tool-context.js';\nimport { createChartBackend, type ChartBackend, type ChartAction } from './backend.js';\nimport { APP_MIME, CHART_URI, KPI_URI, pageSchema, referenceSchema, viewSchema } from './contract.js';\nimport { chartResult } from './feature.js';\nimport { tileHtml } from './tile.js';\n\nconst actions: Record<ChartAction, string> = {\n render_chart: 'Render one authorized chart page as SVG with point metadata. Cursor pagination stays within the same guest scope.',\n refresh_widget: 'Refresh a chart in the current guest scope. Revalidates access before returning data.',\n drill_down: 'Explore a server-issued drill identifier within the current chart and guest scope.',\n change_date_range: 'Render a chart for an ordered date range within the current guest scope.',\n};\n\n/** Call after registerAllTools so existing registration policy/instrumentation remains active. */\nexport function registerChartApps(server: McpServer, client: DatabrainClient, backend: ChartBackend = createChartBackend(client)): void {\n for (const [name, description] of Object.entries(actions)) {\n const action = name as ChartAction;\n const tool = server.tool(name, description, {\n reference: referenceSchema, view: viewSchema.default({}),\n context: z.string().max(200).optional().describe('Chart interaction context'),\n }, READ_ONLY, async ({ reference, view }) => {\n try {\n if ((action === 'drill_down' && !view.drillId) || (action === 'change_date_range' && !view.dateRange)) throw new Error('Missing view selector');\n const page = pageSchema.parse(await toolContext.run({ tool: name, category: 'embedding', workflowIntent: 'chart_app', inputMode: 'app' },\n () => backend.execute(action, reference, view)));\n // SVG is rendered as an isolated image, never inserted into the HTML DOM.\n // Reject active/external/animated constructs as a further contract check.\n if (page.svg && (!/^\\s*<svg\\b/i.test(page.svg) ||\n /<\\s*(?:script|foreignObject|iframe|image|animate\\w*|set)\\b|<!DOCTYPE|<!ENTITY|\\bon\\w+\\s*=|@(?:import|keyframes)|\\banimation\\s*:|(?:href\\s*=\\s*[\"'](?!#)|url\\s*\\(\\s*[\"']?(?!#))/i.test(page.svg))) {\n throw new Error('Unsafe SVG');\n }\n return chartResult(`${page.kind === 'kpi' ? 'KPI' : 'Chart'} ready. ${page.rows.length} rows in this page.`, {\n ...page, reference, view,\n });\n } catch {\n return { isError: true, content: [{ type: 'text' as const,\n text: 'Chart unavailable. Check dashboard access and the chart renderer integration, then retry.' }] };\n }\n });\n tool.update({ _meta: { ui: { resourceUri: CHART_URI, visibility: ['app'] } } });\n }\n for (const [uri, kind] of [[CHART_URI, 'chart'], [KPI_URI, 'kpi']] as const) {\n server.resource(`Databrain ${kind} tile`, uri, { mimeType: APP_MIME, description: `Interactive ${kind} using host theme and scoped server tools` }, async () => ({\n contents: [{ uri, mimeType: APP_MIME, text: tileHtml(kind), _meta: { ui: { prefersBorder: true } } }],\n }));\n }\n}\n","import { DatabrainClient } from '../client/databrain-client.js';\nimport type { RequestOptions } from '../client/types.js';\nimport { createServiceBundle } from '../services/factory.js';\nimport { pageSchema, referenceSchema, viewSchema, type ChartPage, type ChartReference, type ChartView } from './contract.js';\n\nexport const RENDER_PATH = '/api/v2/dashboard/mcp-chart';\nexport type ChartAction = 'render_chart' | 'refresh_widget' | 'drill_down' | 'change_date_range';\nexport interface ChartBackend {\n execute(action: ChartAction, reference: ChartReference, view: ChartView): Promise<ChartPage>;\n}\n\nclass UncachedChartClient extends DatabrainClient {\n constructor(private readonly delegate: DatabrainClient) {\n super({ apiUrl: delegate.getApiUrl(), demoDomain: delegate.getDemoDomain() });\n }\n override request<T>(options: RequestOptions): Promise<T> {\n return this.delegate.request<T>({ ...options, cacheKey: undefined, cacheTtlMs: undefined });\n }\n}\n\n/** Proposed backend route; no local renderer, ambient service fallback, or UI credentials. */\nexport function createChartBackend(client: DatabrainClient): ChartBackend {\n // The general embed lookup caches for five minutes. Authorization must not.\n const services = createServiceBundle(new UncachedChartClient(client));\n return {\n async execute(action, rawReference, rawView) {\n const reference = referenceSchema.parse(rawReference);\n const view = viewSchema.parse(rawView);\n // Every page/refresh/drill repeats current tenant ownership validation.\n const app = await services.widgetService.resolveDataAppName(undefined, reference.embedId);\n if (!app.ok) throw new Error('Chart scope unavailable');\n const scope = await services.widgetService.ensureTenantDashboardScope({ ...reference, dataAppName: app.value });\n if (!scope.ok) throw new Error('Chart scope denied');\n const { token } = await services.guestTokenService.generateToken({\n clientId: reference.clientId, dataAppName: app.value, allowedEmbeds: [reference.embedId],\n permissions: { isDisableMetricCreation: true, isEnableManageMetrics: false,\n isEnableMetricUpdation: false, isEnableArchiveMetrics: false },\n });\n if (!token) throw new Error('Guest scope unavailable');\n const page = await client.request<unknown>({\n method: 'POST', path: RENDER_PATH, auth: 'none', token,\n body: { action, embedId: reference.embedId, widgetId: reference.widgetId, ...view,\n pageSize: 50, width: 600, height: 320, animation: false },\n skipDataUnwrap: true,\n });\n return pageSchema.parse(page);\n },\n };\n}\n","/** Deliberately no imports in the iframe. All requests go through the host bridge. */\nexport const TILE_SCRIPT = String.raw`(()=>{\nconst $=id=>document.getElementById(id),root=document.documentElement,pending=new Map();\nlet seq=0,origin='*',ready=false,closed=false,ref=null,view={},next=null,context={},busy=false,revision=0,rawSvg=null;\nfunction send(message){if(!closed)parent.postMessage({jsonrpc:'2.0',...message},origin)}\nfunction rpc(method,params){return new Promise((resolve,reject)=>{const id=++seq;\nconst timer=setTimeout(()=>{pending.delete(id);reject(Error('timeout'))},30000);\npending.set(id,{resolve,reject,timer});send({id,method,params})})}\nfunction status(text,bad){const s=$('status');s.textContent=text;s.toggleAttribute('data-error',!!bad);if(bad)$('plot').dataset.state='off'}\nfunction paint(){const img=$('chart-image');if(img&&rawSvg)img.src='data:image/svg+xml,'+encodeURIComponent(rawSvg.replace(/<\\/svg>\\s*$/i,'<style>text{fill:'+getComputedStyle(document.body).color+'!important}</style></svg>'))}\nfunction controls(){document.querySelectorAll('button,input,select').forEach(e=>{e.disabled=busy});\n$('refresh').hidden=!ref;$('next').hidden=!next;$('range').hidden=!ref;$('plot').toggleAttribute('data-busy',busy);\n$('expand').hidden=!context.availableDisplayModes?.includes('fullscreen');\nroot.toggleAttribute('data-deep',context.displayMode==='fullscreen'||$('expand').hidden)}\nfunction host(change){context={...context,...change};if(change.theme==='dark'||change.theme==='light')root.style.colorScheme=change.theme;\nconst vars=change.styles?.variables||{};for(const [key,value] of Object.entries(vars)){\nif(/^--(?:color-|font-|border-|shadow-)/.test(key)&&typeof value==='string'&&!/url\\s*\\(|@import/i.test(value))root.style.setProperty(key,value)}\nconst face=change.styles?.css?.fonts;if(typeof face==='string'&&face.length<4000&&!/@import|javascript:/i.test(face))$('hostfont').textContent=face;\nconst insets=change.safeAreaInsets;if(insets)for(const edge of ['top','right','bottom','left']){\nconst n=insets[edge];if(Number.isFinite(n))root.style.setProperty('--safe-'+edge,Math.max(0,Math.min(n,200))+'px')}\nroot.dataset.mode=context.displayMode==='fullscreen'?'full':'inline';\n$('expand').textContent=context.displayMode==='fullscreen'?'Back to conversation':'Expand';paint();controls()}\nfunction resize(){if(ready)send({method:'ui/notifications/size-changed',params:{width:document.body.scrollWidth,height:document.body.scrollHeight}})}\nfunction table(rows,columns){$('rows').replaceChildren();$('data').hidden=!rows?.length;if(!rows?.length)return;\n$('count').textContent=rows.length===1?'Show the 1 row behind this':'Show the '+rows.length+' rows behind this';\nconst t=document.createElement('table');\nconst header=document.createElement('tr');for(const name of columns){const th=document.createElement('th');th.scope='col';th.textContent=name;header.append(th)}\nconst head=document.createElement('thead');head.append(header);t.append(head);const body=document.createElement('tbody');\nfor(const row of rows.slice(0,50)){const tr=document.createElement('tr');for(const key of columns){const td=document.createElement('td');td.textContent=String(row[key]??'');tr.append(td)}body.append(tr)}t.append(body);$('rows').append(t)}\nasync function display(mode){try{const answer=await rpc('ui/request-display-mode',{mode});\nhost({displayMode:answer.mode||context.displayMode});if(root.dataset.mode==='full'&&!$('data').hidden)$('data').open=true;resize()}\ncatch{status('Expanded view is unavailable in this host.',1)}}\nfunction wait(){busy=true;if(!$('plot').firstChild)$('plot').dataset.state='wait';status('Loading chart…');controls()}\nasync function call(name,extra={}){if(!ready||!ref||busy)return;wait();const rev=++revision;\ntry{const result=await rpc('tools/call',{name,arguments:{reference:ref,view:{...view,...extra},context:'User is interacting with the chart tile.'}});\nif(rev===revision)render(result)}catch{if(rev===revision)status('Could not load the chart. Select Refresh to try again.',1)}finally{busy=false;controls()}}\nfunction render(result){busy=false;controls();\nif(result?.isError){status('Chart unavailable. Check your access to this dashboard, then select Refresh.',1);return}\nconst p=result?.structuredContent;if(!p||typeof p!=='object'){status('No chart data came back for this result.',1);return}\nrevision++;rawSvg=null;$('plot').replaceChildren();$('plot').dataset.state='ready';$('choices').replaceChildren();$('tip').textContent='';delete $('tip').dataset.value;\nref=p.reference||null;view=p.view||{};next=p.nextCursor||null;\n$('title').textContent=p.title||'Analytics';$('description').textContent=p.description||'';\nif(p.kind==='dashboard'){$('plot').dataset.state='off';\nfor(const item of p.widgets||[]){const b=document.createElement('button');b.textContent=item.name||'Untitled widget';b.onclick=()=>{ref=item.reference;view={};call('render_chart')};$('choices').append(b)}\nstatus(p.widgets?.length?'Choose a chart to explore.':'This dashboard has no charts yet.');\n}else if(p.kind==='kpi'){\nconst number=document.createElement('p');number.className='kpi';number.textContent=String(p.value??'—');$('plot').append(number);status('');\n}else if(p.svg){\nconst box=document.createElement('div');box.className='chart';box.style.aspectRatio=p.width+'/'+p.height;\nconst img=document.createElement('img');img.id='chart-image';rawSvg=p.svg;img.alt=p.title||'Chart';img.onerror=()=>{status('Chart image unavailable. The data is in the table below.',1);$('data').open=$('data').toggleAttribute('data-only',true)};box.append(img);\nfor(const point of p.points||[]){const b=document.createElement('button');b.className='point';b.setAttribute('aria-label',point.label+': '+point.value);b.setAttribute('aria-pressed','false');\nb.style.left=100*point.x/p.width+'%';b.style.top=100*point.y/p.height+'%';b.style.width=100*point.width/p.width+'%';b.style.height=100*point.height/p.height+'%';\nconst hover=()=>{$('tip').textContent=point.label+': '+point.value;$('tip').dataset.value='1'};b.onmouseenter=hover;b.onfocus=hover;\nb.onclick=()=>{box.querySelectorAll('.point').forEach(e=>e.setAttribute('aria-pressed','false'));b.setAttribute('aria-pressed','true');hover();\n$('choices').replaceChildren();if(point.drillId){const drill=document.createElement('button');drill.className='primary';drill.textContent='Explore selected data';drill.onclick=()=>call('drill_down',{drillId:point.drillId,cursor:undefined});$('choices').append(drill)}};box.append(b)}$('plot').append(box);\nstatus(p.rows?.length?'':'This chart has no data for the current view.');if(p.points?.length)$('tip').textContent='Select part of the chart to read its value.';\n}else{$('plot').dataset.state='off';status(p.rows?.length?'Chart preview is unavailable. The data is in the table below.':'No data for this view.',!p.rows?.length)}\npaint();table(p.rows||[],p.columns||Object.keys(p.rows?.[0]||{}));\nconst only=!p.svg&&p.value===undefined;$('data').open=only;$('data').toggleAttribute('data-only',only);controls();resize()}\n$('refresh').onclick=()=>call('refresh_widget',{cursor:undefined});$('next').onclick=()=>call('render_chart',{cursor:next});\n$('expand').onclick=()=>display(context.displayMode==='fullscreen'?'inline':'fullscreen');\n$('dates').onclick=()=>{const from=$('from').value,to=$('to').value;if(!from||!to||from>to){status('Choose a start date on or before the end date.',1);return}call('change_date_range',{dateRange:{from,to},cursor:undefined,drillId:undefined})};\nfunction receive(event){if(event.source!==parent||closed)return;const m=event.data;\nif(!m||Array.isArray(m)||m.jsonrpc!=='2.0'||(origin!=='*'&&event.origin!==origin))return;\nif(!m.method&&pending.has(m.id)&&('result'in m||'error'in m)){if(origin==='*'&&event.origin!=='null')origin=event.origin;\nconst p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);m.error?p.reject(Error('host error')):p.resolve(m.result);return}\nif(m.method==='ping'&&m.id!==undefined){send({id:m.id,result:{}});return}\nif(m.method==='ui/resource-teardown'){send({id:m.id,result:{}});closed=true;revision++;for(const p of pending.values()){clearTimeout(p.timer);p.reject(Error('closed'))}pending.clear();observer.disconnect();removeEventListener('message',receive);return}\nif(!ready||m.id!==undefined)return;\nif(m.method==='ui/notifications/tool-result'){render(m.params)}\nelse if(m.method==='ui/notifications/tool-input'){wait()}\nelse if(m.method==='ui/notifications/host-context-changed'){host(m.params||{});resize()}\nelse if(m.method==='ui/notifications/tool-cancelled'){revision++;busy=false;$('plot').dataset.state='off';status('Chart request cancelled.');controls()}}\naddEventListener('message',receive);const observer=new ResizeObserver(resize);observer.observe(document.body);controls();\nrpc('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'Databrain chart tile',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline','fullscreen']}})\n.then(r=>{if(r.protocolVersion!=='2026-01-26')throw Error('version');host(r.hostContext||{});ready=true;send({method:'ui/notifications/initialized'});resize()})\n.catch(()=>status('This chart needs a host that supports MCP Apps.',1));\n})();`;\n\nexport function tileHtml(kind: 'chart' | 'kpi' = 'chart'): string {\n return `<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'\">\n<title>Databrain ${kind} tile</title><style id=\"hostfont\"></style><style>\n/* Host tokens are the Claude MCP Apps style-variable names. Every one carries a fallback:\n hosts may ship any subset, so nothing here may depend on a token existing. */\n:root{color-scheme:light dark;\n--dbn-bg:var(--color-background-primary,Canvas);--dbn-text:var(--color-text-primary,CanvasText);\n--dbn-mute:var(--color-text-secondary,GrayText);--dbn-faint:var(--color-text-tertiary,GrayText);\n--dbn-edge:var(--color-border-secondary,ButtonBorder);--dbn-line:var(--color-border-tertiary,var(--dbn-edge));\n--dbn-accent:var(--color-text-info,LinkText);--dbn-ring:var(--color-ring-primary,var(--dbn-accent));\n--dbn-bad:var(--color-text-danger,var(--dbn-text));--dbn-bad-edge:var(--color-border-danger,currentColor);\n--dbn-quiet:color-mix(in srgb,currentColor 6%,transparent);\n--bw:var(--border-width-regular,1px);--r-s:var(--border-radius-sm,6px);--r-m:var(--border-radius-md,8px);--r-l:var(--border-radius-lg,10px);\n--med:var(--font-weight-medium,500);--semi:var(--font-weight-semibold,600);\n/* One curve. Quick / standard / display-mode. Corporate: no overshoot. */\n--e:cubic-bezier(.2,0,0,1);--q:120ms;--s:300ms;--l:400ms}\n*{box-sizing:border-box}\nbody{margin:0;padding:var(--safe-top,0px) var(--safe-right,0px) var(--safe-bottom,0px) var(--safe-left,0px);background:var(--dbn-bg);color:var(--dbn-text);\nfont:var(--font-text-sm-size,14px)/var(--font-text-sm-line-height,1.4) var(--font-sans,system-ui)}\nmain{padding:20px;max-width:1200px;margin:auto;transition:padding var(--l) var(--e)}\nheader{display:flex;align-items:start;justify-content:space-between;gap:12px}header>div{min-width:0}\nh1{font-size:var(--font-heading-md-size,18px);line-height:var(--font-heading-md-line-height,1.4);margin:0 0 6px;font-weight:var(--semi);letter-spacing:-.01em;overflow-wrap:anywhere;transition:font-size var(--l) var(--e)}\np{margin:6px 0;color:var(--dbn-mute);overflow-wrap:anywhere}#expand{flex:none}\nbutton,input{font:inherit;color:inherit;background:var(--dbn-bg);border:var(--bw) solid var(--dbn-edge);border-radius:var(--r-s);padding:7px 10px}\nbutton{cursor:pointer;transition:background-color var(--q) var(--e),border-color var(--q) var(--e)}\ninput{max-width:160px;min-width:0}button:hover:not(:disabled){background:var(--dbn-quiet)}button:disabled{color:var(--dbn-faint);cursor:default}\nbutton:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--dbn-ring);outline-offset:2px}\n#choices button.primary{border-color:var(--dbn-accent);font-weight:var(--med)}\n#status:empty{margin:0}\n#status[data-error]{color:var(--dbn-bad);font-weight:var(--med);border-left:2px solid var(--dbn-bad-edge);background:var(--color-background-danger,transparent);border-radius:0 var(--r-s) var(--r-s) 0;padding:8px 10px;margin:8px 0}\n/* Reserved plot. The skeleton is a static bar silhouette matching the 15:8 backend render:\n Claude's guidance asks for a skeleton that matches the final layout, and bans nothing here,\n but shimmer is banned by our own motion budget - so this paints once and never repaints. */\n#plot{position:relative;transition:opacity var(--q) var(--e)}#plot[data-busy]{opacity:.55}\n#plot[data-state=wait]{aspect-ratio:15/8}\n#plot[data-state=wait]::before,#plot[data-state=ready]::before{content:'';position:absolute;pointer-events:none;inset:16% 6% 13%;\nborder-bottom:var(--bw) solid var(--dbn-line);transition:opacity var(--s) var(--e);\nbackground:linear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 0 100%/11% 44% no-repeat,\nlinear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 22% 100%/11% 71% no-repeat,\nlinear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 44% 100%/11% 57% no-repeat,\nlinear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 66% 100%/11% 86% no-repeat,\nlinear-gradient(var(--dbn-quiet),var(--dbn-quiet)) 88% 100%/11% 33% no-repeat}\n#plot[data-state=ready]::before{opacity:0}\n.chart{position:relative;width:100%;margin:12px 0}.chart img{width:100%;height:100%;display:block}\n.point{position:absolute;padding:0;border:0;background:transparent;border-radius:var(--border-radius-xs,4px);\noutline:2px solid transparent;outline-offset:1px;transition:background-color var(--q) var(--e),outline-color var(--q) var(--e)}\n.point:hover{background:color-mix(in srgb,currentColor 12%,transparent)}\n.point[aria-pressed=true]{outline-color:var(--dbn-accent);background:color-mix(in srgb,var(--dbn-accent) 18%,transparent)}\n.kpi{font-size:clamp(34px,10vw,44px);line-height:1.1;font-weight:var(--semi);letter-spacing:-.025em;font-variant-numeric:tabular-nums;color:var(--dbn-text);margin:14px 0 6px;transition:font-size var(--l) var(--e)}\n#tip{color:var(--dbn-mute);font-size:var(--font-text-xs-size,12px);overflow-wrap:anywhere;min-height:1.4em}\n#tip[data-value]{color:var(--dbn-text);font-weight:var(--med);font-variant-numeric:tabular-nums}\n#choices,footer,.dates{display:flex;gap:8px;flex-wrap:wrap;align-items:center}#choices{margin:10px 0 0}footer{margin-top:12px}.dates{margin:8px 0 2px}\nlabel{display:flex;align-items:center;gap:6px;color:var(--dbn-mute)}\n#range{margin-top:10px}#data{margin-top:2px}#data[hidden],#range[hidden]{display:none}\nhtml:not([data-deep]) #range,html:not([data-deep]) #data:not([data-only]){display:none}\nsummary{cursor:pointer;color:var(--dbn-mute);font-size:var(--font-text-xs-size,12px);padding:5px 0;width:fit-content;transition:color var(--q) var(--e)}summary:hover{color:var(--dbn-text)}\n#rows{overflow:auto;max-height:260px;border-radius:var(--r-s)}\ntable{border-collapse:collapse;width:100%;font-size:var(--font-text-xs-size,12px)}\nth,td{text-align:left;padding:8px;border-bottom:var(--bw) solid var(--dbn-line);overflow-wrap:anywhere}\ntd:last-child{font-variant-numeric:tabular-nums}th{font-weight:var(--semi);position:sticky;top:0;background:var(--dbn-bg);color:var(--dbn-mute)}\n/* Inline to fullscreen is a layout morph, not a new screen: main widens over 400ms on the\n one curve and every dependent box follows it. Fullscreen is the detailed view, so the\n table loses its inline height cap. */\n/* The SVG is rendered at 600x320 and scaled by width, so an unbounded fullscreen column\n makes the chart's own axis type larger than the tile's heading. Hold a measure. */\n[data-mode=full] main{padding:28px 32px;max-width:900px}\n[data-mode=full] h1{font-size:var(--font-heading-xl-size,24px);line-height:var(--font-heading-xl-line-height,1.25)}\n[data-mode=full] .kpi{font-size:clamp(44px,7vw,72px)}\n[data-mode=full] #rows{max-height:none}\n/* Touch: the host gives vertical pan to the conversation, so a nested scroller is a trap. */\n@media(pointer:coarse){button,input{min-height:44px}summary{padding:12px 0}#rows{max-height:none;overflow:visible}}\n@media(max-width:480px){main{padding:14px}}\n/* Entrance. Content first, chrome 60ms later: 300+60 = 360ms, inside the 400ms budget.\n No stagger past two groups - a chat tile that cascades reads as lag, not luxury. */\n@keyframes rise{from{opacity:0;transform:translateY(6px)}}\n@keyframes land{from{opacity:0;transform:translateY(4px) scale(.995)}}\nheader,#status,#plot{animation:rise var(--s) var(--e) both}\n#tip,#choices,footer,#range,#data{animation:rise var(--s) var(--e) 60ms both}\n.chart,.kpi{animation:land var(--s) var(--e) both}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n</style></head><body><main id=\"tile\"><header><div><h1 id=\"title\">Analytics</h1><p id=\"description\"></p></div><button id=\"expand\" hidden>Expand</button></header>\n<p id=\"status\" role=\"status\">Loading chart…</p><div id=\"plot\" data-state=\"wait\"></div><div id=\"tip\"></div><div id=\"choices\"></div>\n<footer><button id=\"refresh\" hidden>Refresh</button><button id=\"next\" hidden>Next page</button></footer>\n<details id=\"range\" hidden><summary>Change the date range</summary><div class=\"dates\"><label>From <input id=\"from\" type=\"date\"></label><label>To <input id=\"to\" type=\"date\"></label><button id=\"dates\" type=\"button\">Apply dates</button></div></details>\n<details id=\"data\" hidden><summary id=\"count\">Show the data behind this</summary><div id=\"rows\" tabindex=\"0\" role=\"region\" aria-label=\"Chart data\"></div></details></main><script>${TILE_SCRIPT}</script></body></html>`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ oauthConfigSchema,
4
+ oauthHttpsUrl
5
+ } from "./chunk-7DS7OLWP.js";
6
+
7
+ // src/utils/config.ts
8
+ import { z, ZodError } from "zod";
9
+ var DEFAULT_POSTHOG_API_KEY = "phc_YgR4BO3sbtQAUvTfRUEN5xBzzPrJfrEG9STJEy3mNO2";
10
+ var DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
11
+ var configSchema = z.object({
12
+ serviceToken: z.string().min(1).optional(),
13
+ apiToken: z.string().min(1).optional(),
14
+ oauth: oauthConfigSchema.optional(),
15
+ apiUrl: z.string().url().default("https://api.usedatabrain.com"),
16
+ demoDomain: z.string().url().default("https://demo.usedatabrain.com"),
17
+ posthogApiKey: z.string().min(1).optional(),
18
+ posthogHost: z.string().url().optional()
19
+ }).refine((c) => c.serviceToken || c.apiToken || c.oauth, {
20
+ message: "Set DATABRAIN_SERVICE_TOKEN, DATABRAIN_API_TOKEN, or complete DATABRAIN_OAUTH_* client credentials"
21
+ }).refine((c) => !c.oauth || oauthHttpsUrl.safeParse(c.apiUrl).success, {
22
+ message: "DATABRAIN_API_URL must use HTTPS without userinfo or fragments when OAuth is configured"
23
+ });
24
+ function formatConfigError(err) {
25
+ const issues = err.issues.map((i) => i.path[0] === "oauth" ? `Invalid or missing OAuth setting: ${i.path.join(".")}` : i.message);
26
+ const lines = ["[databrain] Configuration error:"];
27
+ for (const issue of issues) {
28
+ lines.push(` - ${issue}`);
29
+ }
30
+ lines.push("");
31
+ lines.push(" Set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) \u2014 org-level token for setup and management.");
32
+ lines.push(' Then add to your MCP config: { "env": { "DATABRAIN_SERVICE_TOKEN": "..." } }');
33
+ lines.push(" Or configure DATABRAIN_OAUTH_ISSUER, DATABRAIN_OAUTH_CLIENT_ID and DATABRAIN_OAUTH_CLIENT_SECRET for service credentials. See specs/oauth-client-credentials.md.");
34
+ lines.push("");
35
+ lines.push(" Self-hosted? Also set DATABRAIN_API_URL to your instance's API origin (e.g. https://api.your-databrain.example.com). Tokens minted on cloud do not work against a self-hosted instance and vice versa.");
36
+ return lines.join("\n");
37
+ }
38
+ function loadConfig() {
39
+ try {
40
+ const oauthEnv = {
41
+ issuer: process.env.DATABRAIN_OAUTH_ISSUER || void 0,
42
+ clientId: process.env.DATABRAIN_OAUTH_CLIENT_ID || void 0,
43
+ clientSecret: process.env.DATABRAIN_OAUTH_CLIENT_SECRET || void 0,
44
+ resource: process.env.DATABRAIN_OAUTH_RESOURCE || void 0,
45
+ scope: process.env.DATABRAIN_OAUTH_SCOPE || void 0,
46
+ authMethod: process.env.DATABRAIN_OAUTH_AUTH_METHOD || void 0
47
+ };
48
+ const oauthConfigured = Object.values(oauthEnv).some((value) => value !== void 0);
49
+ return configSchema.parse({
50
+ serviceToken: process.env.DATABRAIN_SERVICE_TOKEN || void 0,
51
+ apiToken: process.env.DATABRAIN_API_TOKEN || void 0,
52
+ oauth: oauthConfigured ? {
53
+ ...oauthEnv,
54
+ resource: oauthEnv.resource || process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com"
55
+ } : void 0,
56
+ apiUrl: process.env.DATABRAIN_API_URL || "https://api.usedatabrain.com",
57
+ demoDomain: process.env.DATABRAIN_DEMO_DOMAIN || "https://demo.usedatabrain.com",
58
+ posthogApiKey: process.env.POSTHOG_PROJECT_API_KEY || DEFAULT_POSTHOG_API_KEY,
59
+ posthogHost: process.env.POSTHOG_HOST || DEFAULT_POSTHOG_HOST
60
+ });
61
+ } catch (err) {
62
+ if (err instanceof ZodError) {
63
+ process.stderr.write(formatConfigError(err) + "\n");
64
+ throw new Error("Invalid Databrain configuration. Check the named environment settings.");
65
+ }
66
+ throw err;
67
+ }
68
+ }
69
+
70
+ export {
71
+ DEFAULT_POSTHOG_API_KEY,
72
+ DEFAULT_POSTHOG_HOST,
73
+ loadConfig
74
+ };
75
+ //# sourceMappingURL=chunk-RF3HTNO7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/config.ts"],"sourcesContent":["import { z, ZodError } from 'zod';\nimport { oauthConfigSchema, oauthHttpsUrl } from '../client/oauth-client-credentials.js';\n\n// PostHog project token for Databrain's own MCP usage analytics. This is a\n// public write-only key (like a web analytics site ID), safe to ship in the\n// package — it lets the CLI send telemetry out of the box, matching how\n// DATABRAIN_API_URL already ships with a working default.\nexport const DEFAULT_POSTHOG_API_KEY = 'phc_YgR4BO3sbtQAUvTfRUEN5xBzzPrJfrEG9STJEy3mNO2';\nexport const DEFAULT_POSTHOG_HOST = 'https://us.i.posthog.com';\n\nconst configSchema = z\n .object({\n serviceToken: z.string().min(1).optional(),\n apiToken: z.string().min(1).optional(),\n oauth: oauthConfigSchema.optional(),\n apiUrl: z.string().url().default('https://api.usedatabrain.com'),\n demoDomain: z.string().url().default('https://demo.usedatabrain.com'),\n posthogApiKey: z.string().min(1).optional(),\n posthogHost: z.string().url().optional(),\n })\n .refine((c) => c.serviceToken || c.apiToken || c.oauth, {\n message:\n 'Set DATABRAIN_SERVICE_TOKEN, DATABRAIN_API_TOKEN, or complete DATABRAIN_OAUTH_* client credentials',\n })\n .refine((c) => !c.oauth || oauthHttpsUrl.safeParse(c.apiUrl).success, {\n message: 'DATABRAIN_API_URL must use HTTPS without userinfo or fragments when OAuth is configured',\n });\n\nexport type Config = z.infer<typeof configSchema>;\n\nfunction formatConfigError(err: ZodError): string {\n const issues = err.issues.map((i) => i.path[0] === 'oauth'\n ? `Invalid or missing OAuth setting: ${i.path.join('.')}` : i.message);\n const lines = ['[databrain] Configuration error:'];\n for (const issue of issues) {\n lines.push(` - ${issue}`);\n }\n lines.push('');\n lines.push(' Set DATABRAIN_SERVICE_TOKEN (Settings > Service Tokens) — org-level token for setup and management.');\n lines.push(' Then add to your MCP config: { \"env\": { \"DATABRAIN_SERVICE_TOKEN\": \"...\" } }');\n lines.push(' Or configure DATABRAIN_OAUTH_ISSUER, DATABRAIN_OAUTH_CLIENT_ID and DATABRAIN_OAUTH_CLIENT_SECRET for service credentials. See specs/oauth-client-credentials.md.');\n lines.push('');\n lines.push(' Self-hosted? Also set DATABRAIN_API_URL to your instance\\'s API origin (e.g. https://api.your-databrain.example.com). Tokens minted on cloud do not work against a self-hosted instance and vice versa.');\n return lines.join('\\n');\n}\n\nexport function loadConfig(): Config {\n try {\n const oauthEnv = {\n issuer: process.env.DATABRAIN_OAUTH_ISSUER || undefined,\n clientId: process.env.DATABRAIN_OAUTH_CLIENT_ID || undefined,\n clientSecret: process.env.DATABRAIN_OAUTH_CLIENT_SECRET || undefined,\n resource: process.env.DATABRAIN_OAUTH_RESOURCE || undefined,\n scope: process.env.DATABRAIN_OAUTH_SCOPE || undefined,\n authMethod: process.env.DATABRAIN_OAUTH_AUTH_METHOD || undefined,\n };\n const oauthConfigured = Object.values(oauthEnv).some(value => value !== undefined);\n return configSchema.parse({\n serviceToken: process.env.DATABRAIN_SERVICE_TOKEN || undefined,\n apiToken: process.env.DATABRAIN_API_TOKEN || undefined,\n oauth: oauthConfigured ? {\n ...oauthEnv,\n resource: oauthEnv.resource || process.env.DATABRAIN_API_URL || 'https://api.usedatabrain.com',\n } : undefined,\n apiUrl:\n process.env.DATABRAIN_API_URL || 'https://api.usedatabrain.com',\n demoDomain:\n process.env.DATABRAIN_DEMO_DOMAIN || 'https://demo.usedatabrain.com',\n posthogApiKey: process.env.POSTHOG_PROJECT_API_KEY || DEFAULT_POSTHOG_API_KEY,\n posthogHost: process.env.POSTHOG_HOST || DEFAULT_POSTHOG_HOST,\n });\n } catch (err) {\n if (err instanceof ZodError) {\n process.stderr.write(formatConfigError(err) + '\\n');\n // Zod enum failures can contain the supplied value; index.ts serializes\n // startup errors. Do not expose any OAuth config values to that logger.\n throw new Error('Invalid Databrain configuration. Check the named environment settings.');\n }\n throw err;\n }\n}\n"],"mappings":";;;;;;;AAAA,SAAS,GAAG,gBAAgB;AAOrB,IAAM,0BAA0B;AAChC,IAAM,uBAAuB;AAEpC,IAAM,eAAe,EAClB,OAAO;AAAA,EACN,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACzC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACrC,OAAO,kBAAkB,SAAS;AAAA,EAClC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,8BAA8B;AAAA,EAC/D,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,+BAA+B;AAAA,EACpE,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EAC1C,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AACzC,CAAC,EACA,OAAO,CAAC,MAAM,EAAE,gBAAgB,EAAE,YAAY,EAAE,OAAO;AAAA,EACtD,SACE;AACJ,CAAC,EACA,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,cAAc,UAAU,EAAE,MAAM,EAAE,SAAS;AAAA,EACpE,SAAS;AACX,CAAC;AAIH,SAAS,kBAAkB,KAAuB;AAChD,QAAM,SAAS,IAAI,OAAO,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,UAC/C,qCAAqC,EAAE,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO;AACvE,QAAM,QAAQ,CAAC,kCAAkC;AACjD,aAAW,SAAS,QAAQ;AAC1B,UAAM,KAAK,OAAO,KAAK,EAAE;AAAA,EAC3B;AACA,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,4GAAuG;AAClH,QAAM,KAAK,gFAAgF;AAC3F,QAAM,KAAK,oKAAoK;AAC/K,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,0MAA2M;AACtN,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,aAAqB;AACnC,MAAI;AACF,UAAM,WAAW;AAAA,MACf,QAAQ,QAAQ,IAAI,0BAA0B;AAAA,MAC9C,UAAU,QAAQ,IAAI,6BAA6B;AAAA,MACnD,cAAc,QAAQ,IAAI,iCAAiC;AAAA,MAC3D,UAAU,QAAQ,IAAI,4BAA4B;AAAA,MAClD,OAAO,QAAQ,IAAI,yBAAyB;AAAA,MAC5C,YAAY,QAAQ,IAAI,+BAA+B;AAAA,IACzD;AACA,UAAM,kBAAkB,OAAO,OAAO,QAAQ,EAAE,KAAK,WAAS,UAAU,MAAS;AACjF,WAAO,aAAa,MAAM;AAAA,MACxB,cAAc,QAAQ,IAAI,2BAA2B;AAAA,MACrD,UAAU,QAAQ,IAAI,uBAAuB;AAAA,MAC7C,OAAO,kBAAkB;AAAA,QACvB,GAAG;AAAA,QACH,UAAU,SAAS,YAAY,QAAQ,IAAI,qBAAqB;AAAA,MAClE,IAAI;AAAA,MACJ,QACE,QAAQ,IAAI,qBAAqB;AAAA,MACnC,YACE,QAAQ,IAAI,yBAAyB;AAAA,MACvC,eAAe,QAAQ,IAAI,2BAA2B;AAAA,MACtD,aAAa,QAAQ,IAAI,gBAAgB;AAAA,IAC3C,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,eAAe,UAAU;AAC3B,cAAQ,OAAO,MAAM,kBAAkB,GAAG,IAAI,IAAI;AAGlD,YAAM,IAAI,MAAM,wEAAwE;AAAA,IAC1F;AACA,UAAM;AAAA,EACR;AACF;","names":[]}