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

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.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +21 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/.mcp.json +11 -0
  4. package/README.md +171 -7
  5. package/dist/apps/server.d.ts +323 -0
  6. package/dist/apps/server.js +11 -0
  7. package/dist/apps/server.js.map +1 -0
  8. package/dist/apps/stdio.d.ts +2 -0
  9. package/dist/apps/stdio.js +16 -0
  10. package/dist/apps/stdio.js.map +1 -0
  11. package/dist/chunk-5IGP6A6T.js +496 -0
  12. package/dist/chunk-5IGP6A6T.js.map +1 -0
  13. package/dist/chunk-CUPL7CAN.js +75 -0
  14. package/dist/chunk-CUPL7CAN.js.map +1 -0
  15. package/dist/chunk-ERTAKB2H.js +11093 -0
  16. package/dist/chunk-ERTAKB2H.js.map +1 -0
  17. package/dist/chunk-N7X762VU.js +34 -0
  18. package/dist/chunk-N7X762VU.js.map +1 -0
  19. package/dist/chunk-OCBTNAAO.js +1946 -0
  20. package/dist/chunk-OCBTNAAO.js.map +1 -0
  21. package/dist/chunk-Z3ZHN4UW.js +130 -0
  22. package/dist/chunk-Z3ZHN4UW.js.map +1 -0
  23. package/dist/cli.d.ts +15 -0
  24. package/dist/cli.js +495 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/custom-chart/runtime-shim.js +146 -0
  27. package/dist/custom-chart/template/AGENTS.md +374 -0
  28. package/dist/custom-chart/template/README.md +374 -0
  29. package/dist/custom-chart/template/chart.css +28 -0
  30. package/dist/custom-chart/template/chart.ts +192 -0
  31. package/dist/custom-chart/template/custom-chart.d.ts +78 -0
  32. package/dist/custom-chart/template/icon.svg +1 -0
  33. package/dist/custom-chart/template/manifest.json +23 -0
  34. package/dist/custom-chart/template/package.json +14 -0
  35. package/dist/custom-chart/template/sample-data.json +18 -0
  36. package/dist/custom-chart/template/tsconfig.json +12 -0
  37. package/dist/index.js +65 -9787
  38. package/dist/index.js.map +1 -1
  39. package/dist/main-KZRHWE3K.js +857 -0
  40. package/dist/main-KZRHWE3K.js.map +1 -0
  41. package/dist/test-only-K7QTSVO4.js +38 -0
  42. package/dist/test-only-K7QTSVO4.js.map +1 -0
  43. package/package.json +17 -5
  44. package/skills/author-dashboard/SKILL.md +30 -0
  45. package/skills/embed-dashboard/SKILL.md +23 -0
  46. package/skills/style-dashboard/SKILL.md +58 -0
@@ -0,0 +1,1946 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/errors.ts
4
+ var DatabrainApiError = class extends Error {
5
+ constructor(status, code, message, details, serverHint, retryable) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ this.details = details;
10
+ this.serverHint = serverHint;
11
+ this.retryable = retryable;
12
+ this.name = "DatabrainApiError";
13
+ }
14
+ toToolResponse() {
15
+ const parts = [`Error: ${this.message}`];
16
+ const detailDump = this.formatDetails();
17
+ if (detailDump) parts.push(detailDump);
18
+ const hint = this.serverHint ? `Hint: ${this.serverHint}` : this.hint();
19
+ if (hint) parts.push(hint);
20
+ return {
21
+ content: [{ type: "text", text: parts.join("\n\n") }],
22
+ isError: true
23
+ };
24
+ }
25
+ /**
26
+ * Surface API-returned details for 4xx errors so the LLM can recover or
27
+ * report the real reason (validation messages, field names, etc.). Skips
28
+ * fields already collapsed into `message`/`code`, and skips 5xx where the
29
+ * payload usually isn't actionable.
30
+ */
31
+ formatDetails() {
32
+ if (this.status < 400 || this.status >= 500) return "";
33
+ if (!this.details || typeof this.details !== "object") return "";
34
+ const skipKeys = /* @__PURE__ */ new Set(["message", "detail", "error", "code", "data", "hint", "retryable"]);
35
+ const extra = {};
36
+ for (const [k, v] of Object.entries(this.details)) {
37
+ if (skipKeys.has(k)) continue;
38
+ if (v === null || v === void 0) continue;
39
+ extra[k] = v;
40
+ }
41
+ if (Object.keys(extra).length === 0) return "";
42
+ try {
43
+ return `API details: ${JSON.stringify(extra)}`;
44
+ } catch {
45
+ return "";
46
+ }
47
+ }
48
+ hint() {
49
+ if (this.code === "OAUTH_SIGN_IN_REQUIRED") {
50
+ return "Hint: Snapshots draw as a signed-in Databrain user. Connect this AI app with a Databrain sign-in (the hosted connector, or /mcp in Claude Code); a service token cannot take them.";
51
+ }
52
+ if (this.code === "RENDER_IN_PROGRESS") {
53
+ return "Hint: Your other snapshot is still rendering. Wait a few seconds, then call the tool again.";
54
+ }
55
+ if (this.code === "SNAPSHOT_TIMEOUT" || this.code === "SNAPSHOT_TOO_LARGE") {
56
+ return "Hint: Ask for one chart with metricId, or a smaller width.";
57
+ }
58
+ if (this.code === "UNAUTHORIZED_ORIGIN") {
59
+ return "Hint: The embed origin is not on the allowed domains list. Add the domain to the allowed domains in the Databrain settings; the token itself is fine.";
60
+ }
61
+ if (this.status === 401 || this.status === 403) {
62
+ if (this.code === "OAUTH_API_ERROR") {
63
+ return "Hint: Check that the backend accepts the configured OAuth issuer and resource, and that the service principal has permission for this operation. Static fallback applies only to token acquisition failures.";
64
+ }
65
+ if (this.code === "INVALID_SERVICE_TOKEN" || this.code === "MISSING_SERVICE_TOKEN" || this.message.toLowerCase().includes("service token")) {
66
+ return "Hint: The service token (DATABRAIN_SERVICE_TOKEN) is invalid, expired, or missing. It must be set in the MCP server config (not at runtime). Check Settings > Service Tokens in the Databrain UI.";
67
+ }
68
+ return "Hint: The per-data-app API token is invalid or expired. Use `create_api_token` to generate a new one. Do not ask the user \u2014 manage tokens via MCP tools.";
69
+ }
70
+ if (this.status === 400) {
71
+ return "Hint: The request was invalid. Check parameter values match expected types and formats. Use the corresponding list tool to verify IDs and names.";
72
+ }
73
+ if (this.status === 404) {
74
+ return "Hint: The requested resource was not found. Verify the ID or name is correct using the corresponding list tool.";
75
+ }
76
+ if (this.status === 409) {
77
+ return "Hint: A resource with this name already exists. Use a different name or list existing resources first.";
78
+ }
79
+ if (this.status === 422) {
80
+ return "Hint: The request parameters are invalid. Check the required fields and their formats.";
81
+ }
82
+ if (this.status === 429) {
83
+ return "Hint: API rate limit reached. Wait a moment and try again.";
84
+ }
85
+ if (this.status >= 500) {
86
+ return "Hint: The Databrain API is experiencing issues. Try again in a few moments.";
87
+ }
88
+ return "";
89
+ }
90
+ };
91
+ function toolError(message) {
92
+ return {
93
+ content: [{ type: "text", text: message }],
94
+ isError: true
95
+ };
96
+ }
97
+ function stringify(value) {
98
+ if (typeof value === "string") return value;
99
+ try {
100
+ return JSON.stringify(value, null, 2);
101
+ } catch {
102
+ return String(value);
103
+ }
104
+ }
105
+ function toolSuccess(text) {
106
+ return {
107
+ content: [{ type: "text", text }]
108
+ };
109
+ }
110
+
111
+ // src/utils/logger.ts
112
+ var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
113
+ var currentLevel = process.env.DATABRAIN_LOG_LEVEL || "warn";
114
+ var secrets = /* @__PURE__ */ new Set();
115
+ function registerSecret(value) {
116
+ if (typeof value === "string" && value.length >= 8) secrets.add(value);
117
+ }
118
+ function redactSecrets(text) {
119
+ let out = text;
120
+ for (const secret of secrets) out = out.split(secret).join("[redacted]");
121
+ return out;
122
+ }
123
+ function log(level, message, data) {
124
+ if (LOG_LEVELS[level] < LOG_LEVELS[currentLevel]) return;
125
+ const line = `[databrain-mcp] [${level.toUpperCase()}] ${message}`;
126
+ const text = data !== void 0 ? `${line} ${JSON.stringify(data)}` : line;
127
+ process.stderr.write(`${redactSecrets(text)}
128
+ `);
129
+ }
130
+ var logger = {
131
+ debug: (msg, data) => log("debug", msg, data),
132
+ info: (msg, data) => log("info", msg, data),
133
+ warn: (msg, data) => log("warn", msg, data),
134
+ error: (msg, data) => log("error", msg, data)
135
+ };
136
+
137
+ // package.json
138
+ var package_default = {
139
+ name: "@databrainhq/mcp-server",
140
+ mcpName: "com.usedatabrain/mcp-server",
141
+ version: "0.3.0-rc.10",
142
+ description: "MCP server for Databrain embedded analytics - connect databases, create datamarts, configure embeds, and generate frontend code",
143
+ type: "module",
144
+ bin: {
145
+ "mcp-server": "dist/index.js",
146
+ "databrain-mcp-server": "dist/index.js",
147
+ "databrain-mcp-chart-apps": "dist/apps/stdio.js",
148
+ databrain: "dist/cli.js"
149
+ },
150
+ main: "dist/index.js",
151
+ types: "dist/index.d.ts",
152
+ files: [
153
+ "dist",
154
+ "skills",
155
+ ".claude-plugin",
156
+ ".mcp.json"
157
+ ],
158
+ scripts: {
159
+ build: "tsup && node scripts/copy-custom-chart-assets.mjs",
160
+ dev: "tsx src/index.ts",
161
+ test: "vitest run",
162
+ "test:watch": "vitest",
163
+ "test:live": "vitest run tests/integration/prompt-flows.test.ts",
164
+ lint: "tsc --noEmit",
165
+ prepublishOnly: "npm run build"
166
+ },
167
+ keywords: [
168
+ "mcp",
169
+ "databrain",
170
+ "embedded-analytics",
171
+ "ai",
172
+ "model-context-protocol"
173
+ ],
174
+ license: "MIT",
175
+ dependencies: {
176
+ "@modelcontextprotocol/sdk": "^1.32.1",
177
+ "@posthog/mcp": "^0.10.7",
178
+ esbuild: "0.27.3",
179
+ fflate: "0.8.3",
180
+ "posthog-node": "^5.21.2",
181
+ yaml: "^2.9.1",
182
+ zod: "^3.24.0"
183
+ },
184
+ devDependencies: {
185
+ "@types/node": "^25.3.5",
186
+ tsup: "^8.4.0",
187
+ tsx: "^4.19.0",
188
+ typescript: "^5.7.0",
189
+ vitest: "^3.0.0"
190
+ },
191
+ publishConfig: {
192
+ access: "public"
193
+ },
194
+ repository: {
195
+ type: "git",
196
+ url: "git+https://github.com/databrainhq/mcp-server.git"
197
+ },
198
+ engines: {
199
+ node: ">=18.0.0"
200
+ }
201
+ };
202
+
203
+ // src/version.ts
204
+ var MCP_SERVER_VERSION = package_default.version;
205
+
206
+ // src/utils/cache.ts
207
+ var TtlCache = class {
208
+ store = /* @__PURE__ */ new Map();
209
+ get(key) {
210
+ const entry = this.store.get(key);
211
+ if (!entry) return void 0;
212
+ if (Date.now() > entry.expiresAt) {
213
+ this.store.delete(key);
214
+ return void 0;
215
+ }
216
+ return entry.value;
217
+ }
218
+ set(key, value, ttlMs) {
219
+ this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
220
+ }
221
+ invalidateByPrefix(prefix) {
222
+ for (const key of this.store.keys()) {
223
+ if (key.startsWith(prefix)) {
224
+ this.store.delete(key);
225
+ }
226
+ }
227
+ }
228
+ clear() {
229
+ this.store.clear();
230
+ }
231
+ };
232
+
233
+ // src/client/error-body.ts
234
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
235
+ var str = (v) => typeof v === "string" && v.trim() ? v : void 0;
236
+ var fromEnvelope = (v) => {
237
+ if (!isRecord(v)) return void 0;
238
+ const out = {
239
+ code: str(v.code),
240
+ message: str(v.message) ?? str(v.detail),
241
+ hint: str(v.hint),
242
+ retryable: typeof v.retryable === "boolean" ? v.retryable : void 0,
243
+ details: v.details
244
+ };
245
+ return out.code || out.message || out.hint ? out : void 0;
246
+ };
247
+ function parseErrorBody(body) {
248
+ if (!isRecord(body)) return {};
249
+ const nested = fromEnvelope(isRecord(body.data) ? body.data.error : void 0) ?? fromEnvelope(body.error) ?? fromEnvelope(body);
250
+ if (nested) {
251
+ return { ...nested, message: nested.message ?? str(body.error) };
252
+ }
253
+ return { message: str(body.error) };
254
+ }
255
+
256
+ // src/utils/tool-context.ts
257
+ import { AsyncLocalStorage } from "async_hooks";
258
+ var toolContext = new AsyncLocalStorage();
259
+
260
+ // src/client/oauth-client-credentials.ts
261
+ import { z } from "zod";
262
+ var oauthHttpsUrl = z.string().refine((value) => {
263
+ try {
264
+ const url = new URL(value);
265
+ return url.protocol === "https:" && !url.username && !url.password && !url.hash;
266
+ } catch {
267
+ return false;
268
+ }
269
+ }, "OAuth URLs must use HTTPS without userinfo or fragments");
270
+ var oauthConfigSchema = z.object({
271
+ issuer: oauthHttpsUrl.refine((value) => {
272
+ try {
273
+ return !new URL(value).search;
274
+ } catch {
275
+ return false;
276
+ }
277
+ }, "OAuth issuer must not contain a query"),
278
+ clientId: z.string().min(1),
279
+ clientSecret: z.string().min(1),
280
+ resource: oauthHttpsUrl,
281
+ scope: z.string().min(1).optional(),
282
+ authMethod: z.enum(["client_secret_basic", "client_secret_post"]).default("client_secret_basic")
283
+ });
284
+ var metadataSchema = z.object({
285
+ issuer: z.string(),
286
+ token_endpoint: oauthHttpsUrl,
287
+ token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
288
+ grant_types_supported: z.array(z.string()).optional()
289
+ });
290
+ var tokenSchema = z.object({
291
+ access_token: z.string().regex(/^[A-Za-z0-9\-._~+/]+=*$/),
292
+ token_type: z.string().refine((value) => value.toLowerCase() === "bearer"),
293
+ expires_in: z.number().finite().positive().optional()
294
+ });
295
+ var TOKEN_TIMEOUT_MS = 1e4;
296
+ var FAILURE_COOLDOWN_MS = 3e4;
297
+ var MAX_REFRESH_SKEW_MS = 6e4;
298
+ function authError() {
299
+ return new Error("OAuth token acquisition failed. Check the configured issuer, client credentials, resource and scopes.");
300
+ }
301
+ var OAuthClientCredentials = class {
302
+ config;
303
+ tokenEndpoint;
304
+ cached;
305
+ pending;
306
+ retryAt = 0;
307
+ constructor(config) {
308
+ const parsed = oauthConfigSchema.safeParse(config);
309
+ if (!parsed.success) throw authError();
310
+ this.config = Object.freeze(parsed.data);
311
+ }
312
+ getStatus() {
313
+ if (this.cached && Date.now() < this.cached.renewAt) return "ready";
314
+ if (this.retryAt > 0) return "unavailable";
315
+ return "configured";
316
+ }
317
+ async getToken() {
318
+ if (this.cached && Date.now() < this.cached.renewAt) return this.cached.value;
319
+ if (this.pending) return this.pending;
320
+ if (Date.now() < this.retryAt) throw authError();
321
+ this.cached = void 0;
322
+ this.pending = this.acquire().catch(() => {
323
+ this.retryAt = Date.now() + FAILURE_COOLDOWN_MS;
324
+ throw authError();
325
+ }).finally(() => {
326
+ this.pending = void 0;
327
+ });
328
+ return this.pending;
329
+ }
330
+ async discoverTokenEndpoint(signal) {
331
+ if (this.tokenEndpoint) return this.tokenEndpoint;
332
+ const issuer = new URL(this.config.issuer);
333
+ const path = issuer.pathname.replace(/\/$/, "");
334
+ const candidates = [.../* @__PURE__ */ new Set([
335
+ `${issuer.origin}/.well-known/oauth-authorization-server${path}`,
336
+ `${issuer.origin}/.well-known/openid-configuration${path}`,
337
+ `${issuer.origin}${path}/.well-known/openid-configuration`
338
+ ])];
339
+ for (const url of candidates) {
340
+ const response = await fetch(url, {
341
+ headers: { Accept: "application/json" },
342
+ redirect: "error",
343
+ signal
344
+ });
345
+ if (response.status === 404 || response.status === 405) continue;
346
+ if (!response.ok) throw authError();
347
+ const metadata = metadataSchema.parse(await response.json());
348
+ if (metadata.issuer !== this.config.issuer) throw authError();
349
+ const methods = metadata.token_endpoint_auth_methods_supported ?? ["client_secret_basic"];
350
+ if (!methods.includes(this.config.authMethod)) throw authError();
351
+ if (metadata.grant_types_supported && !metadata.grant_types_supported.includes("client_credentials")) {
352
+ throw authError();
353
+ }
354
+ this.tokenEndpoint = metadata.token_endpoint;
355
+ return this.tokenEndpoint;
356
+ }
357
+ throw authError();
358
+ }
359
+ async acquire() {
360
+ const signal = AbortSignal.timeout(TOKEN_TIMEOUT_MS);
361
+ const endpoint = await this.discoverTokenEndpoint(signal);
362
+ const body = new URLSearchParams({
363
+ grant_type: "client_credentials",
364
+ resource: this.config.resource
365
+ });
366
+ if (this.config.scope) body.set("scope", this.config.scope);
367
+ const headers = {
368
+ Accept: "application/json",
369
+ "Content-Type": "application/x-www-form-urlencoded"
370
+ };
371
+ if (this.config.authMethod === "client_secret_post") {
372
+ body.set("client_id", this.config.clientId);
373
+ body.set("client_secret", this.config.clientSecret);
374
+ } else {
375
+ const encode = (value) => new URLSearchParams({ value }).toString().slice(6);
376
+ headers.Authorization = `Basic ${Buffer.from(`${encode(this.config.clientId)}:${encode(this.config.clientSecret)}`).toString("base64")}`;
377
+ }
378
+ const startedAt = Date.now();
379
+ const response = await fetch(endpoint, {
380
+ method: "POST",
381
+ headers,
382
+ body: body.toString(),
383
+ redirect: "error",
384
+ signal
385
+ });
386
+ if (!response.ok) throw authError();
387
+ const token = tokenSchema.parse(await response.json());
388
+ if (token.expires_in !== void 0) {
389
+ const lifetimeMs = token.expires_in * 1e3;
390
+ const expiresAt = startedAt + lifetimeMs;
391
+ if (!Number.isFinite(expiresAt) || Date.now() >= expiresAt) throw authError();
392
+ this.cached = {
393
+ value: token.access_token,
394
+ renewAt: expiresAt - Math.min(MAX_REFRESH_SKEW_MS, lifetimeMs * 0.1)
395
+ };
396
+ }
397
+ this.retryAt = 0;
398
+ return token.access_token;
399
+ }
400
+ };
401
+
402
+ // src/client/databrain-client.ts
403
+ var MCP_USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION}`;
404
+ var MAX_RETRIES = 3;
405
+ var REQUEST_TIMEOUT_MS = 3e4;
406
+ var NON_TRANSIENT_PATTERNS = [
407
+ "out of context",
408
+ "Definition not found",
409
+ "not a data question",
410
+ "Query generation failed",
411
+ "not found",
412
+ "not authenticated",
413
+ "Invalid Service Token",
414
+ "service token"
415
+ ];
416
+ var DatabrainClient = class {
417
+ constructor(config) {
418
+ this.config = config;
419
+ registerSecret(config.serviceToken);
420
+ registerSecret(config.apiToken);
421
+ if (config.oauth) {
422
+ if (!oauthHttpsUrl.safeParse(config.apiUrl).success) {
423
+ throw new Error("OAuth requires an HTTPS Databrain API URL without userinfo or fragments.");
424
+ }
425
+ this.oauth = new OAuthClientCredentials(config.oauth);
426
+ this.config = Object.freeze({ ...config });
427
+ }
428
+ }
429
+ cache = new TtlCache();
430
+ runtimeApiToken;
431
+ oauth;
432
+ oauthFallback = false;
433
+ hasApiToken() {
434
+ return !!(this.runtimeApiToken || this.config.apiToken);
435
+ }
436
+ setRuntimeApiToken(token) {
437
+ registerSecret(token);
438
+ this.runtimeApiToken = token;
439
+ logger.info("Runtime API token set for this session");
440
+ }
441
+ getServiceToken() {
442
+ return this.config.serviceToken;
443
+ }
444
+ getApiUrl() {
445
+ return this.config.apiUrl;
446
+ }
447
+ getDemoDomain() {
448
+ return this.config.demoDomain;
449
+ }
450
+ /**
451
+ * Describes which credential is currently used as the bearer for outgoing
452
+ * API calls. Used by `whoami` so the LLM can give the user a truthful answer.
453
+ */
454
+ getActiveAuth() {
455
+ if (this.oauth) {
456
+ if (this.oauthFallback) return { source: "service-token", oauthFallback: true };
457
+ return { source: "oauth-client-credentials", status: this.oauth.getStatus() };
458
+ }
459
+ if (this.config.serviceToken) return { source: "service-token" };
460
+ if (this.runtimeApiToken || this.config.apiToken) return { source: "api-token" };
461
+ return { source: "none" };
462
+ }
463
+ async getToken(auth) {
464
+ if (auth === "none") return void 0;
465
+ if (auth === "service") {
466
+ if (this.oauth) {
467
+ try {
468
+ const token = await this.oauth.getToken();
469
+ this.oauthFallback = false;
470
+ return token;
471
+ } catch {
472
+ if (!this.config.serviceToken) {
473
+ throw new DatabrainApiError(
474
+ 0,
475
+ "OAUTH_TOKEN_ERROR",
476
+ "OAuth token acquisition failed. Check the configured issuer, client credentials, resource and scopes."
477
+ );
478
+ }
479
+ if (!this.oauthFallback) logger.warn("OAuth unavailable; using the configured static service token.");
480
+ this.oauthFallback = true;
481
+ }
482
+ }
483
+ return this.config.serviceToken;
484
+ }
485
+ return this.runtimeApiToken || this.config.apiToken;
486
+ }
487
+ async request(options) {
488
+ return this.requestInternal(options);
489
+ }
490
+ async requestInternal(options) {
491
+ const usesOAuth = !!this.oauth && options.auth === "service" && !options.token;
492
+ const cacheAllowed = !usesOAuth;
493
+ if (options.invalidateCache) {
494
+ for (const prefix of options.invalidateCache) {
495
+ this.cache.invalidateByPrefix(prefix);
496
+ }
497
+ }
498
+ if (cacheAllowed && options.cacheKey && options.cacheTtlMs) {
499
+ const cached = this.cache.get(options.cacheKey);
500
+ if (cached !== void 0) {
501
+ logger.debug(`Cache hit: ${options.cacheKey}`);
502
+ return cached;
503
+ }
504
+ }
505
+ let token = options.token ?? await this.getToken(options.auth);
506
+ if (options.auth !== "none" && !options.token && !token) {
507
+ if (options.auth === "service") {
508
+ throw new DatabrainApiError(
509
+ 401,
510
+ "MISSING_SERVICE_TOKEN",
511
+ "DATABRAIN_SERVICE_TOKEN is required for this operation (org-level token for discovery and data app management). It must be set in the MCP server configuration \u2014 do not ask the user for it at runtime."
512
+ );
513
+ }
514
+ throw new DatabrainApiError(
515
+ 401,
516
+ "MISSING_API_TOKEN",
517
+ "A per-data-app API token is required for this embedding operation but none is configured. Call `create_api_token` to generate one, or `set_api_token` if you already have the value. Do not ask the user for this token \u2014 create it via the MCP tools."
518
+ );
519
+ }
520
+ let url = `${this.config.apiUrl}${options.path}`;
521
+ if (options.query) {
522
+ const params = new URLSearchParams(options.query);
523
+ url += `?${params.toString()}`;
524
+ }
525
+ let lastError;
526
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
527
+ try {
528
+ if (usesOAuth && attempt > 1) token = await this.getToken(options.auth);
529
+ const headers = {
530
+ "Content-Type": "application/json",
531
+ Accept: "application/json",
532
+ "User-Agent": MCP_USER_AGENT,
533
+ "X-Databrain-Client": MCP_USER_AGENT,
534
+ ...options.headers
535
+ };
536
+ const ctx = toolContext.getStore();
537
+ if (ctx) {
538
+ headers["X-Databrain-MCP-Tool"] = ctx.tool;
539
+ headers["X-Databrain-MCP-Tool-Category"] = ctx.category;
540
+ headers["X-Databrain-MCP-Workflow-Intent"] = ctx.workflowIntent;
541
+ headers["X-Databrain-MCP-Input-Mode"] = ctx.inputMode;
542
+ }
543
+ if (token) {
544
+ if (options.auth === "none") {
545
+ headers["x-plugin-token"] = token;
546
+ headers["Origin"] = "https://mcp.usedatabrain.com";
547
+ } else {
548
+ headers["Authorization"] = `Bearer ${token}`;
549
+ }
550
+ }
551
+ const response = await fetch(url, {
552
+ method: options.method,
553
+ headers,
554
+ body: options.body ? JSON.stringify(options.body) : void 0,
555
+ signal: AbortSignal.timeout(options.timeoutMs || REQUEST_TIMEOUT_MS),
556
+ ...usesOAuth ? { redirect: "error" } : {}
557
+ });
558
+ if (!response.ok) {
559
+ if (usesOAuth) {
560
+ throw new DatabrainApiError(
561
+ response.status,
562
+ "OAUTH_API_ERROR",
563
+ `Databrain API request failed with status ${response.status} using configured service credentials.`
564
+ );
565
+ }
566
+ const errorBody = await response.text();
567
+ logger.warn(`[API Error] ${options.method} ${options.path} status=${response.status} body=${errorBody.slice(0, 500)}`);
568
+ let parsed = {};
569
+ try {
570
+ parsed = JSON.parse(errorBody);
571
+ } catch {
572
+ }
573
+ const envelope = parseErrorBody(parsed);
574
+ const errorMsg = envelope.message || `API request failed with status ${response.status}`;
575
+ const error = new DatabrainApiError(
576
+ response.status,
577
+ envelope.code || `HTTP_${response.status}`,
578
+ errorMsg,
579
+ parsed,
580
+ envelope.hint,
581
+ envelope.retryable
582
+ );
583
+ const isTransient = envelope.retryable !== void 0 ? envelope.retryable && (response.status === 429 || response.status >= 500) : response.status === 429 || response.status >= 500 && !NON_TRANSIENT_PATTERNS.some((p) => errorMsg.includes(p));
584
+ if (isTransient && attempt < MAX_RETRIES) {
585
+ const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 8e3);
586
+ logger.warn(`Retrying in ${delay}ms (attempt ${attempt}/${MAX_RETRIES})`, {
587
+ status: response.status,
588
+ path: options.path
589
+ });
590
+ await new Promise((r) => setTimeout(r, delay));
591
+ lastError = error;
592
+ continue;
593
+ }
594
+ throw error;
595
+ }
596
+ const text = await response.text();
597
+ let data = text ? JSON.parse(text) : {};
598
+ if (!options.skipDataUnwrap && data && typeof data === "object" && !Array.isArray(data) && "data" in data) {
599
+ const inner = data.data;
600
+ if (inner !== null && inner !== void 0) {
601
+ data = inner;
602
+ }
603
+ }
604
+ if (cacheAllowed && options.cacheKey && options.cacheTtlMs) {
605
+ this.cache.set(options.cacheKey, data, options.cacheTtlMs);
606
+ }
607
+ return data;
608
+ } catch (err) {
609
+ if (err instanceof DatabrainApiError) throw err;
610
+ if (attempt < MAX_RETRIES && err?.name === "TimeoutError" && !options.noRetryOnTimeout) {
611
+ const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 8e3);
612
+ logger.warn(`Request timed out, retrying in ${delay}ms`, { path: options.path });
613
+ await new Promise((r) => setTimeout(r, delay));
614
+ lastError = err;
615
+ continue;
616
+ }
617
+ throw new DatabrainApiError(
618
+ 0,
619
+ "NETWORK_ERROR",
620
+ usesOAuth ? "Failed to connect to Databrain API using configured service credentials." : `Failed to connect to Databrain API: ${err.message}`
621
+ );
622
+ }
623
+ }
624
+ throw lastError || new DatabrainApiError(0, "UNKNOWN", "Request failed after retries");
625
+ }
626
+ };
627
+
628
+ // src/custom-chart/assets.ts
629
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
630
+ import { dirname, join, resolve } from "path";
631
+ import { fileURLToPath } from "url";
632
+ var cachedDir;
633
+ function customChartAssetDir() {
634
+ if (cachedDir) return cachedDir;
635
+ const here = dirname(fileURLToPath(import.meta.url));
636
+ const candidates = [
637
+ here,
638
+ join(here, "custom-chart"),
639
+ join(here, "..", "custom-chart"),
640
+ join(here, "..", "..", "custom-chart"),
641
+ join(here, "..", "src", "custom-chart")
642
+ ];
643
+ for (const candidate of candidates) {
644
+ if (existsSync(join(candidate, "runtime-shim.js")) && existsSync(join(candidate, "template", "manifest.json"))) {
645
+ cachedDir = resolve(candidate);
646
+ return cachedDir;
647
+ }
648
+ }
649
+ throw new Error(
650
+ `Custom chart assets not found near ${here}. Expected runtime-shim.js and template/ (run npm run build).`
651
+ );
652
+ }
653
+ function readRuntimeShim() {
654
+ return readFileSync(join(customChartAssetDir(), "runtime-shim.js"), "utf8");
655
+ }
656
+ function templateDir() {
657
+ return join(customChartAssetDir(), "template");
658
+ }
659
+ function readTemplateFiles() {
660
+ const dir = templateDir();
661
+ const files = {};
662
+ for (const name of readdirSync(dir).sort()) {
663
+ const path = join(dir, name);
664
+ if (statSync(path).isFile()) files[name] = readFileSync(path, "utf8");
665
+ }
666
+ return files;
667
+ }
668
+
669
+ // src/custom-chart/manifest.ts
670
+ import { z as z2 } from "zod";
671
+
672
+ // src/custom-chart/types.ts
673
+ var MAX_MANIFEST_BYTES = 32 * 1024;
674
+ var MAX_BUNDLE_JS_BYTES = 2 * 1024 * 1024;
675
+ var MAX_BUNDLE_CSS_BYTES = 256 * 1024;
676
+ var MAX_ICON_BYTES = 16 * 1024;
677
+ var MAX_SLOTS = 16;
678
+ var MAX_OPTIONS = 32;
679
+
680
+ // src/custom-chart/manifest.ts
681
+ var ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
682
+ var FORBIDDEN_IDS = ["constructor", "prototype", "__proto__"];
683
+ var LANGUAGE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;
684
+ var SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
685
+ var COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
686
+ var FIELD_TYPES = ["string", "number", "boolean"];
687
+ var MAX_TEXT = 256;
688
+ var MAX_DESCRIPTION = 500;
689
+ var MAX_GROUP = 64;
690
+ var MAX_CHOICES = 32;
691
+ var MAX_STRING_OPTION_LENGTH = 1024;
692
+ var V1_TOP_KEYS = ["schemaVersion", "id", "name", "slots", "options"];
693
+ var V2_TOP_EXTRA_KEYS = ["version", "description", "translations"];
694
+ var V1_SLOT_KEYS = ["id", "label", "role", "types", "required"];
695
+ var V2_SLOT_EXTRA_KEYS = ["description"];
696
+ var OPTION_BASE_KEYS = ["id", "label", "type", "default"];
697
+ var OPTION_TYPE_KEYS = {
698
+ boolean: [],
699
+ number: ["min", "max"],
700
+ string: ["maxLength"],
701
+ select: ["choices"],
702
+ color: []
703
+ };
704
+ var V2_OPTION_EXTRA_KEYS = {
705
+ boolean: ["group"],
706
+ number: ["group", "step"],
707
+ string: ["group"],
708
+ select: ["group"],
709
+ color: ["group"]
710
+ };
711
+ var TRANSLATION_KEYS = ["name", "description", "slots", "options"];
712
+ var SLOT_TRANSLATION_KEYS = ["label", "description"];
713
+ var OPTION_TRANSLATION_KEYS = ["label", "group", "choices"];
714
+ var isRecord2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
715
+ var isId = (value) => typeof value === "string" && ID_PATTERN.test(value) && !FORBIDDEN_IDS.includes(value);
716
+ var isText = (value, max = MAX_TEXT) => typeof value === "string" && value.length > 0 && value.length <= max;
717
+ var isTranslationText = (value) => typeof value === "string" && value.length <= MAX_TEXT;
718
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
719
+ var jsonBytes = (value) => Buffer.byteLength(JSON.stringify(value), "utf8");
720
+ var Collector = class {
721
+ problems = [];
722
+ add(code, path, message) {
723
+ this.problems.push({ code, path, message });
724
+ }
725
+ /**
726
+ * Unknown keys report `<scope>.unknownKey`, one per key; a missing required
727
+ * key reports `missingCode(key)`. Returns false when anything was reported,
728
+ * so the caller can stop looking at a shape it cannot trust.
729
+ */
730
+ checkKeys(value, path, scope, required, optional, missingCode = (key) => `${scope}.${key}`) {
731
+ const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
732
+ let ok = true;
733
+ for (const key of Object.keys(value)) {
734
+ if (!allowed.has(key)) {
735
+ this.add(`${scope}.unknownKey`, `${path}/${key}`, `Remove "${key}". It is not a known key here.`);
736
+ ok = false;
737
+ }
738
+ }
739
+ for (const key of required) {
740
+ if (!Object.hasOwn(value, key)) {
741
+ this.add(missingCode(key), `${path}/${key}`, `Add the required key "${key}".`);
742
+ ok = false;
743
+ }
744
+ }
745
+ return ok;
746
+ }
747
+ };
748
+ function validateSlot(c, value, path, v2) {
749
+ if (!isRecord2(value)) {
750
+ c.add("manifest.slot.invalid", path, "A slot must be an object.");
751
+ return;
752
+ }
753
+ if (!c.checkKeys(value, path, "manifest.slot", V1_SLOT_KEYS, v2 ? V2_SLOT_EXTRA_KEYS : [])) return;
754
+ if (!isId(value.id)) {
755
+ c.add("manifest.slot.id", `${path}/id`, "A slot id is lowercase, starts with a letter, and uses only a-z, 0-9, _ and -, up to 64 characters.");
756
+ }
757
+ if (!isText(value.label)) {
758
+ c.add("manifest.slot.label", `${path}/label`, `A slot label is 1 to ${MAX_TEXT} characters.`);
759
+ }
760
+ const role = value.role;
761
+ if (role !== "dimension" && role !== "measure") {
762
+ c.add("manifest.slot.role", `${path}/role`, 'A slot role is "dimension" or "measure".');
763
+ }
764
+ if (typeof value.required !== "boolean") {
765
+ c.add("manifest.slot.required", `${path}/required`, "required is true or false.");
766
+ }
767
+ const types = value.types;
768
+ const typesOk = Array.isArray(types) && types.length > 0 && types.length <= 3 && new Set(types).size === types.length && types.every((type) => FIELD_TYPES.includes(type));
769
+ if (!typesOk) {
770
+ c.add("manifest.slot.types", `${path}/types`, 'types is a list of 1 to 3 distinct values from "string", "number" and "boolean".');
771
+ } else if (role === "measure" && (types.length !== 1 || types[0] !== "number")) {
772
+ c.add("manifest.slot.types", `${path}/types`, 'A measure slot accepts only ["number"].');
773
+ }
774
+ if (Object.hasOwn(value, "description") && !isText(value.description)) {
775
+ c.add("manifest.slot.description", `${path}/description`, `A slot description is 1 to ${MAX_TEXT} characters.`);
776
+ }
777
+ }
778
+ function validateOption(c, value, path, v2) {
779
+ if (!isRecord2(value)) {
780
+ c.add("manifest.option.invalid", path, "An option must be an object.");
781
+ return;
782
+ }
783
+ const id = value.id;
784
+ if (typeof id === "string" && (id === "theme" || id.startsWith("theme."))) {
785
+ c.add("manifest.option.reserved", `${path}/id`, 'The option id "theme" (and any id starting with "theme.") is reserved for the host theme.');
786
+ return;
787
+ }
788
+ const type = typeof value.type === "string" ? value.type : "";
789
+ const typeKeys = Object.hasOwn(OPTION_TYPE_KEYS, type) ? OPTION_TYPE_KEYS[type] : void 0;
790
+ if (!typeKeys || type === "color" && !v2) {
791
+ c.add(
792
+ "manifest.option.type",
793
+ `${path}/type`,
794
+ v2 ? 'An option type is one of "boolean", "number", "string", "select", "color".' : 'An option type is one of "boolean", "number", "string", "select" (color needs schemaVersion 2).'
795
+ );
796
+ return;
797
+ }
798
+ if (!c.checkKeys(value, path, "manifest.option", [...OPTION_BASE_KEYS, ...typeKeys], v2 ? V2_OPTION_EXTRA_KEYS[type] : [])) return;
799
+ if (!isId(id)) {
800
+ c.add("manifest.option.id", `${path}/id`, "An option id is lowercase, starts with a letter, and uses only a-z, 0-9, _ and -, up to 64 characters.");
801
+ }
802
+ if (!isText(value.label)) {
803
+ c.add("manifest.option.label", `${path}/label`, `An option label is 1 to ${MAX_TEXT} characters.`);
804
+ }
805
+ if (Object.hasOwn(value, "group") && !isText(value.group, MAX_GROUP)) {
806
+ c.add("manifest.option.group", `${path}/group`, `An option group is 1 to ${MAX_GROUP} characters.`);
807
+ }
808
+ switch (type) {
809
+ case "boolean":
810
+ if (typeof value.default !== "boolean") {
811
+ c.add("manifest.option.default", `${path}/default`, "A boolean default is true or false.");
812
+ }
813
+ break;
814
+ case "number": {
815
+ const rangeOk = isFiniteNumber(value.min) && isFiniteNumber(value.max) && value.min <= value.max;
816
+ if (!rangeOk) {
817
+ c.add("manifest.option.range", `${path}/min`, "Set finite min and max with min <= max.");
818
+ break;
819
+ }
820
+ const min = value.min;
821
+ const max = value.max;
822
+ if (!isFiniteNumber(value.default) || value.default < min || value.default > max) {
823
+ c.add("manifest.option.default", `${path}/default`, "A number default is a finite number between min and max.");
824
+ }
825
+ if (Object.hasOwn(value, "step") && (!isFiniteNumber(value.step) || value.step <= 0 || value.step > max - min)) {
826
+ c.add("manifest.option.step", `${path}/step`, "step is a positive number no larger than max - min.");
827
+ }
828
+ break;
829
+ }
830
+ case "string": {
831
+ const maxLength = value.maxLength;
832
+ if (!isFiniteNumber(maxLength) || !Number.isInteger(maxLength) || maxLength < 1 || maxLength > MAX_STRING_OPTION_LENGTH) {
833
+ c.add("manifest.option.maxLength", `${path}/maxLength`, `A string option needs an integer "maxLength" from 1 to ${MAX_STRING_OPTION_LENGTH}.`);
834
+ break;
835
+ }
836
+ if (typeof value.default !== "string" || value.default.length > maxLength) {
837
+ c.add("manifest.option.default", `${path}/default`, "A string default is a string no longer than maxLength.");
838
+ }
839
+ break;
840
+ }
841
+ case "select": {
842
+ const choices = value.choices;
843
+ const choicesOk = Array.isArray(choices) && choices.length > 0 && choices.length <= MAX_CHOICES && choices.every(
844
+ (choice) => isRecord2(choice) && Object.keys(choice).length === 2 && isId(choice.value) && isText(choice.label)
845
+ ) && new Set(choices.map((choice) => choice.value)).size === choices.length;
846
+ if (!choicesOk) {
847
+ c.add("manifest.option.choices", `${path}/choices`, `choices is a list of 1 to ${MAX_CHOICES} distinct { value, label } pairs; a value follows the id rule.`);
848
+ break;
849
+ }
850
+ if (!choices.some((choice) => choice.value === value.default)) {
851
+ c.add("manifest.option.default", `${path}/default`, "A select default is the value of one of its choices.");
852
+ }
853
+ break;
854
+ }
855
+ case "color":
856
+ if (typeof value.default !== "string" || !COLOR_PATTERN.test(value.default)) {
857
+ c.add("manifest.option.color", `${path}/default`, 'A color default is a 6 digit hex colour like "#3366cc".');
858
+ }
859
+ break;
860
+ default:
861
+ break;
862
+ }
863
+ }
864
+ function validateTranslations(c, value, slots, options) {
865
+ const path = "/translations";
866
+ if (!isRecord2(value)) {
867
+ c.add("manifest.translations.invalid", path, "translations is an object keyed by language code.");
868
+ return;
869
+ }
870
+ for (const [language, entry] of Object.entries(value)) {
871
+ const langPath = `${path}/${language}`;
872
+ if (!LANGUAGE_PATTERN.test(language)) {
873
+ c.add("manifest.translations.language", langPath, `"${language}" is not a language code like "fr" or "pt-BR".`);
874
+ continue;
875
+ }
876
+ if (!isRecord2(entry)) {
877
+ c.add("manifest.translations.invalid", langPath, "A translation is an object with optional name, description, slots and options.");
878
+ continue;
879
+ }
880
+ if (!c.checkKeys(entry, langPath, "manifest.translations", [], TRANSLATION_KEYS)) continue;
881
+ for (const key of ["name", "description"]) {
882
+ if (Object.hasOwn(entry, key) && !isTranslationText(entry[key])) {
883
+ c.add("manifest.translations.text", `${langPath}/${key}`, `A translated string is at most ${MAX_TEXT} characters.`);
884
+ }
885
+ }
886
+ if (Object.hasOwn(entry, "slots")) {
887
+ const entrySlots = entry.slots;
888
+ if (!isRecord2(entrySlots)) {
889
+ c.add("manifest.translations.invalid", `${langPath}/slots`, "slots is an object keyed by slot id.");
890
+ } else {
891
+ for (const [slotId, slotEntry] of Object.entries(entrySlots)) {
892
+ const slotPath = `${langPath}/slots/${slotId}`;
893
+ if (!slots.some((slot) => slot.id === slotId)) {
894
+ c.add("manifest.translations.slot", slotPath, `"${slotId}" is not a slot id in this manifest.`);
895
+ continue;
896
+ }
897
+ if (!isRecord2(slotEntry)) {
898
+ c.add("manifest.translations.invalid", slotPath, "A slot translation is an object with optional label and description.");
899
+ continue;
900
+ }
901
+ if (!c.checkKeys(slotEntry, slotPath, "manifest.translations", [], SLOT_TRANSLATION_KEYS)) continue;
902
+ for (const key of ["label", "description"]) {
903
+ if (Object.hasOwn(slotEntry, key) && !isTranslationText(slotEntry[key])) {
904
+ c.add("manifest.translations.text", `${slotPath}/${key}`, `A translated string is at most ${MAX_TEXT} characters.`);
905
+ }
906
+ }
907
+ }
908
+ }
909
+ }
910
+ if (Object.hasOwn(entry, "options")) {
911
+ const entryOptions = entry.options;
912
+ if (!isRecord2(entryOptions)) {
913
+ c.add("manifest.translations.invalid", `${langPath}/options`, "options is an object keyed by option id.");
914
+ } else {
915
+ for (const [optionId, optionEntry] of Object.entries(entryOptions)) {
916
+ const optionPath = `${langPath}/options/${optionId}`;
917
+ const option = options.find((item) => item.id === optionId);
918
+ if (!option) {
919
+ c.add("manifest.translations.option", optionPath, `"${optionId}" is not an option id in this manifest.`);
920
+ continue;
921
+ }
922
+ if (!isRecord2(optionEntry)) {
923
+ c.add("manifest.translations.invalid", optionPath, "An option translation is an object with optional label, group and choices.");
924
+ continue;
925
+ }
926
+ if (!c.checkKeys(optionEntry, optionPath, "manifest.translations", [], OPTION_TRANSLATION_KEYS)) continue;
927
+ for (const key of ["label", "group"]) {
928
+ if (Object.hasOwn(optionEntry, key) && !isTranslationText(optionEntry[key])) {
929
+ c.add("manifest.translations.text", `${optionPath}/${key}`, `A translated string is at most ${MAX_TEXT} characters.`);
930
+ }
931
+ }
932
+ if (!Object.hasOwn(optionEntry, "choices")) continue;
933
+ const choices = optionEntry.choices;
934
+ if (!isRecord2(choices)) {
935
+ c.add("manifest.translations.invalid", `${optionPath}/choices`, "choices is an object keyed by choice value.");
936
+ continue;
937
+ }
938
+ for (const [choiceValue, label] of Object.entries(choices)) {
939
+ const choicePath = `${optionPath}/choices/${choiceValue}`;
940
+ if (option.type !== "select" || !(option.choices ?? []).some((choice) => choice.value === choiceValue)) {
941
+ c.add("manifest.translations.choice", choicePath, `"${choiceValue}" is not a choice of option "${optionId}".`);
942
+ } else if (!isTranslationText(label)) {
943
+ c.add("manifest.translations.text", choicePath, `A translated string is at most ${MAX_TEXT} characters.`);
944
+ }
945
+ }
946
+ }
947
+ }
948
+ }
949
+ }
950
+ }
951
+ function validateManifest(value) {
952
+ const c = new Collector();
953
+ if (!isRecord2(value)) {
954
+ c.add("manifest.invalid", "", "manifest.json must be a JSON object.");
955
+ return c.problems;
956
+ }
957
+ const schemaVersion = value.schemaVersion;
958
+ if (schemaVersion !== 1 && schemaVersion !== 2) {
959
+ c.add("manifest.schemaVersion.unsupported", "/schemaVersion", "schemaVersion must be 1 or 2.");
960
+ return c.problems;
961
+ }
962
+ const v2 = schemaVersion === 2;
963
+ if (!c.checkKeys(value, "", "manifest", V1_TOP_KEYS, v2 ? V2_TOP_EXTRA_KEYS : [], (key) => `manifest.${key}.required`)) {
964
+ return c.problems;
965
+ }
966
+ if (!isId(value.id)) {
967
+ c.add("manifest.id.invalid", "/id", 'id is lowercase, starts with a letter, and uses only a-z, 0-9, _ and -, up to 64 characters. It becomes the chart type "custom:<id>".');
968
+ }
969
+ if (!isText(value.name)) {
970
+ c.add("manifest.name.invalid", "/name", `name is 1 to ${MAX_TEXT} characters.`);
971
+ }
972
+ if (Object.hasOwn(value, "version") && (typeof value.version !== "string" || !SEMVER_PATTERN.test(value.version))) {
973
+ c.add("manifest.version.invalid", "/version", 'version is a semver string like "1.2.0".');
974
+ }
975
+ if (Object.hasOwn(value, "description") && !isText(value.description, MAX_DESCRIPTION)) {
976
+ c.add("manifest.description.invalid", "/description", `description is 1 to ${MAX_DESCRIPTION} characters.`);
977
+ }
978
+ const slots = value.slots;
979
+ if (!Array.isArray(slots)) {
980
+ c.add("manifest.slots.invalid", "/slots", "slots must be an array.");
981
+ } else if (slots.length === 0) {
982
+ c.add("manifest.slots.empty", "/slots", "Declare at least one slot.");
983
+ } else if (slots.length > MAX_SLOTS) {
984
+ c.add("manifest.slots.tooMany", "/slots", `Declare at most ${MAX_SLOTS} slots.`);
985
+ } else {
986
+ slots.forEach((slot, index) => validateSlot(c, slot, `/slots/${index}`, v2));
987
+ const seen = /* @__PURE__ */ new Set();
988
+ slots.forEach((slot, index) => {
989
+ if (!isRecord2(slot) || typeof slot.id !== "string") return;
990
+ if (seen.has(slot.id)) {
991
+ c.add("manifest.slot.duplicate", `/slots/${index}/id`, `Slot id "${slot.id}" is used more than once.`);
992
+ }
993
+ seen.add(slot.id);
994
+ });
995
+ }
996
+ const options = value.options;
997
+ if (!Array.isArray(options)) {
998
+ c.add("manifest.options.invalid", "/options", "options must be an array (it may be empty).");
999
+ } else if (options.length > MAX_OPTIONS) {
1000
+ c.add("manifest.options.tooMany", "/options", `Declare at most ${MAX_OPTIONS} options.`);
1001
+ } else {
1002
+ options.forEach((option, index) => validateOption(c, option, `/options/${index}`, v2));
1003
+ const seen = /* @__PURE__ */ new Set();
1004
+ options.forEach((option, index) => {
1005
+ if (!isRecord2(option) || typeof option.id !== "string") return;
1006
+ if (seen.has(option.id)) {
1007
+ c.add("manifest.option.duplicate", `/options/${index}/id`, `Option id "${option.id}" is used more than once.`);
1008
+ }
1009
+ seen.add(option.id);
1010
+ });
1011
+ }
1012
+ if (c.problems.length === 0 && Object.hasOwn(value, "translations")) {
1013
+ validateTranslations(
1014
+ c,
1015
+ value.translations,
1016
+ slots,
1017
+ options
1018
+ );
1019
+ }
1020
+ if (c.problems.length === 0 && jsonBytes(value) > MAX_MANIFEST_BYTES) {
1021
+ c.add("manifest.size", "", `manifest.json must be at most ${MAX_MANIFEST_BYTES} bytes as JSON.`);
1022
+ }
1023
+ return c.problems;
1024
+ }
1025
+ var slotSchema = z2.object({
1026
+ id: z2.string(),
1027
+ label: z2.string(),
1028
+ role: z2.enum(["dimension", "measure"]),
1029
+ types: z2.array(z2.enum(["string", "number", "boolean"])),
1030
+ required: z2.boolean(),
1031
+ description: z2.string().optional()
1032
+ });
1033
+ var optionSchema = z2.object({
1034
+ id: z2.string(),
1035
+ label: z2.string(),
1036
+ type: z2.enum(["boolean", "number", "string", "select", "color"]),
1037
+ default: z2.union([z2.string(), z2.number(), z2.boolean()]),
1038
+ min: z2.number().optional(),
1039
+ max: z2.number().optional(),
1040
+ step: z2.number().optional(),
1041
+ maxLength: z2.number().optional(),
1042
+ choices: z2.array(z2.object({ value: z2.string(), label: z2.string() })).optional(),
1043
+ group: z2.string().optional()
1044
+ });
1045
+ var manifestSchema = z2.object({
1046
+ schemaVersion: z2.union([z2.literal(1), z2.literal(2)]),
1047
+ id: z2.string(),
1048
+ name: z2.string(),
1049
+ version: z2.string().optional(),
1050
+ description: z2.string().optional(),
1051
+ slots: z2.array(slotSchema),
1052
+ options: z2.array(optionSchema),
1053
+ translations: z2.record(z2.unknown()).optional()
1054
+ });
1055
+ function sortKeys(value) {
1056
+ if (Array.isArray(value)) return value.map((item) => sortKeys(item));
1057
+ if (isRecord2(value)) {
1058
+ const out = {};
1059
+ for (const key of Object.keys(value).sort()) out[key] = sortKeys(value[key]);
1060
+ return out;
1061
+ }
1062
+ return value;
1063
+ }
1064
+
1065
+ // src/custom-chart/scaffold.ts
1066
+ var DEFAULT_SLOTS = [
1067
+ { id: "category", label: "Category", role: "dimension", types: ["string"], required: true, description: "One bar per value." },
1068
+ { id: "value", label: "Value", role: "measure", types: ["number"], required: true },
1069
+ { id: "target", label: "Target", role: "measure", types: ["number"], required: false }
1070
+ ];
1071
+ function scaffoldProject(input) {
1072
+ const template = readTemplateFiles();
1073
+ const templateManifest = JSON.parse(template["manifest.json"]);
1074
+ const problems = [];
1075
+ if (!isId(input.key)) {
1076
+ problems.push({
1077
+ code: "manifest.id.invalid",
1078
+ path: "/id",
1079
+ message: "key must match ^[a-z][a-z0-9_-]{0,63}$ (lowercase letters, digits, _ and -)."
1080
+ });
1081
+ }
1082
+ const name = typeof input.name === "string" ? input.name.trim() : "";
1083
+ if (name.length === 0 || name.length > 256) {
1084
+ problems.push({ code: "manifest.name.invalid", path: "/name", message: "name must be 1 to 256 characters." });
1085
+ }
1086
+ const manifest = {
1087
+ schemaVersion: 2,
1088
+ id: input.key,
1089
+ name,
1090
+ version: "1.0.0",
1091
+ ...input.description ? { description: input.description } : {},
1092
+ slots: input.slots && input.slots.length > 0 ? input.slots : templateManifest.slots ?? DEFAULT_SLOTS,
1093
+ options: input.options ?? templateManifest.options
1094
+ };
1095
+ problems.push(...validateManifest(manifest));
1096
+ const packageJson = JSON.parse(template["package.json"]);
1097
+ packageJson.name = input.key;
1098
+ const files = {};
1099
+ for (const [file, content] of Object.entries(template)) {
1100
+ if (file === "manifest.json") files[file] = JSON.stringify(manifest, null, 2) + "\n";
1101
+ else if (file === "package.json") files[file] = JSON.stringify(packageJson, null, 2) + "\n";
1102
+ else files[file] = content;
1103
+ }
1104
+ return {
1105
+ ok: problems.length === 0,
1106
+ problems,
1107
+ files,
1108
+ nextStep: "Write these files into a folder, edit chart.ts, then run `databrain custom-chart validate` and `databrain custom-chart build`. Upload with `databrain custom-chart upload` or the custom_chart_upload tool."
1109
+ };
1110
+ }
1111
+
1112
+ // src/custom-chart/bundle.ts
1113
+ import { createHash } from "crypto";
1114
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1115
+ import { join as join2, resolve as resolve2, sep } from "path";
1116
+ import { build as esbuild } from "esbuild";
1117
+
1118
+ // src/custom-chart/sanitize-svg.ts
1119
+ var ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
1120
+ "svg",
1121
+ "g",
1122
+ "defs",
1123
+ "symbol",
1124
+ "use",
1125
+ "title",
1126
+ "desc",
1127
+ "metadata",
1128
+ "path",
1129
+ "rect",
1130
+ "circle",
1131
+ "ellipse",
1132
+ "line",
1133
+ "polyline",
1134
+ "polygon",
1135
+ "text",
1136
+ "tspan",
1137
+ "textpath",
1138
+ "image",
1139
+ "lineargradient",
1140
+ "radialgradient",
1141
+ "stop",
1142
+ "clippath",
1143
+ "mask",
1144
+ "pattern",
1145
+ "marker",
1146
+ "filter",
1147
+ "fegaussianblur",
1148
+ "feoffset",
1149
+ "feblend",
1150
+ "fecolormatrix",
1151
+ "fecomposite",
1152
+ "feflood",
1153
+ "femerge",
1154
+ "femergenode",
1155
+ "fedropshadow",
1156
+ "style",
1157
+ "switch"
1158
+ ]);
1159
+ var ALLOWED_ATTRIBUTES = /* @__PURE__ */ new Set([
1160
+ "id",
1161
+ "class",
1162
+ "style",
1163
+ "href",
1164
+ "xlink:href",
1165
+ "xml:space",
1166
+ "lang",
1167
+ "xml:lang",
1168
+ "version",
1169
+ "baseprofile",
1170
+ "viewbox",
1171
+ "preserveaspectratio",
1172
+ "width",
1173
+ "height",
1174
+ "x",
1175
+ "y",
1176
+ "x1",
1177
+ "y1",
1178
+ "x2",
1179
+ "y2",
1180
+ "cx",
1181
+ "cy",
1182
+ "r",
1183
+ "rx",
1184
+ "ry",
1185
+ "fx",
1186
+ "fy",
1187
+ "fr",
1188
+ "d",
1189
+ "points",
1190
+ "transform",
1191
+ "transform-origin",
1192
+ "fill",
1193
+ "fill-opacity",
1194
+ "fill-rule",
1195
+ "stroke",
1196
+ "stroke-width",
1197
+ "stroke-linecap",
1198
+ "stroke-linejoin",
1199
+ "stroke-miterlimit",
1200
+ "stroke-dasharray",
1201
+ "stroke-dashoffset",
1202
+ "stroke-opacity",
1203
+ "opacity",
1204
+ "color",
1205
+ "display",
1206
+ "visibility",
1207
+ "overflow",
1208
+ "clip-path",
1209
+ "clip-rule",
1210
+ "clippathunits",
1211
+ "mask",
1212
+ "maskunits",
1213
+ "maskcontentunits",
1214
+ "filter",
1215
+ "filterunits",
1216
+ "primitiveunits",
1217
+ "in",
1218
+ "in2",
1219
+ "result",
1220
+ "mode",
1221
+ "operator",
1222
+ "values",
1223
+ "type",
1224
+ "stddeviation",
1225
+ "dx",
1226
+ "dy",
1227
+ "flood-color",
1228
+ "flood-opacity",
1229
+ "offset",
1230
+ "stop-color",
1231
+ "stop-opacity",
1232
+ "gradientunits",
1233
+ "gradienttransform",
1234
+ "spreadmethod",
1235
+ "patternunits",
1236
+ "patterncontentunits",
1237
+ "patterntransform",
1238
+ "markerunits",
1239
+ "markerwidth",
1240
+ "markerheight",
1241
+ "refx",
1242
+ "refy",
1243
+ "orient",
1244
+ "font-family",
1245
+ "font-size",
1246
+ "font-weight",
1247
+ "font-style",
1248
+ "letter-spacing",
1249
+ "text-anchor",
1250
+ "dominant-baseline",
1251
+ "alignment-baseline",
1252
+ "text-decoration",
1253
+ "word-spacing",
1254
+ "paint-order",
1255
+ "vector-effect",
1256
+ "shape-rendering",
1257
+ "text-rendering",
1258
+ "image-rendering",
1259
+ "color-interpolation",
1260
+ "color-interpolation-filters",
1261
+ "enable-background",
1262
+ "startoffset",
1263
+ "method",
1264
+ "spacing",
1265
+ "requiredfeatures",
1266
+ "systemlanguage",
1267
+ "role",
1268
+ "aria-label",
1269
+ "aria-labelledby",
1270
+ "aria-hidden",
1271
+ "focusable",
1272
+ "tabindex"
1273
+ ]);
1274
+ var TAG_RE = /^<\s*(\/?)\s*([A-Za-z_][\w:.-]*)([\s\S]*?)(\/?)\s*>$/;
1275
+ var ATTRIBUTE_RE = /([^\s=/"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
1276
+ var decodeEntities = (value) => value.replace(
1277
+ /&#x([0-9a-fA-F]+);?/g,
1278
+ (_m, hex) => String.fromCodePoint(parseInt(hex, 16))
1279
+ ).replace(
1280
+ /&#(\d+);?/g,
1281
+ (_m, dec) => String.fromCodePoint(parseInt(dec, 10))
1282
+ ).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
1283
+ var normaliseForSchemeCheck = (value) => decodeEntities(value).replace(/[\u0000-\u0020\u007f-\u00a0\u200b-\u200f\u2028\u2029\ufeff]/g, "").toLowerCase();
1284
+ var DANGEROUS_SCHEME_RE = /^(javascript|vbscript|livescript|mocha|data:text\/html|data:application)/;
1285
+ var isDangerousValue = (value) => {
1286
+ const normalised = normaliseForSchemeCheck(value);
1287
+ return DANGEROUS_SCHEME_RE.test(normalised) || normalised.includes("javascript:") || normalised.includes("vbscript:");
1288
+ };
1289
+ var SAFE_DATA_IMAGE_RE = /^data:image\/(png|jpeg|jpg|gif|webp);base64,[a-z0-9+/=]+$/;
1290
+ var isAcceptableHref = (value, element) => {
1291
+ const normalised = normaliseForSchemeCheck(value);
1292
+ if (normalised.startsWith("#")) return true;
1293
+ return element === "image" && SAFE_DATA_IMAGE_RE.test(normalised);
1294
+ };
1295
+ var LOCAL_URL_RE = /url\s*\(\s*(['"]?)#[^)'"]*\1\s*\)/gi;
1296
+ var STYLE_ESCAPE_RE = /url\s*\(|@import|expression\s*\(|-moz-binding|behavior\s*:/i;
1297
+ var isSafeStyle = (value) => !STYLE_ESCAPE_RE.test(decodeEntities(value).replace(LOCAL_URL_RE, ""));
1298
+ var tokenize = (input) => {
1299
+ const tokens = [];
1300
+ let index = 0;
1301
+ const length = input.length;
1302
+ const pushSpecial = (end, terminator) => {
1303
+ const close = input.indexOf(terminator, end);
1304
+ const stop = close === -1 ? length : close + terminator.length;
1305
+ tokens.push({ kind: "special", raw: input.slice(index, stop) });
1306
+ index = stop;
1307
+ };
1308
+ while (index < length) {
1309
+ const open = input.indexOf("<", index);
1310
+ if (open === -1) {
1311
+ tokens.push({ kind: "text", raw: input.slice(index) });
1312
+ break;
1313
+ }
1314
+ if (open > index) {
1315
+ tokens.push({ kind: "text", raw: input.slice(index, open) });
1316
+ index = open;
1317
+ }
1318
+ if (input.startsWith("<!--", index)) {
1319
+ pushSpecial(index + 4, "-->");
1320
+ continue;
1321
+ }
1322
+ if (input.startsWith("<![CDATA[", index)) {
1323
+ pushSpecial(index + 9, "]]>");
1324
+ continue;
1325
+ }
1326
+ if (input.startsWith("<?", index)) {
1327
+ pushSpecial(index + 2, "?>");
1328
+ continue;
1329
+ }
1330
+ if (input.startsWith("<!", index)) {
1331
+ const bracket = input.indexOf("[", index);
1332
+ const plainEnd = input.indexOf(">", index);
1333
+ if (bracket !== -1 && (plainEnd === -1 || bracket < plainEnd)) {
1334
+ const subsetEnd = input.indexOf("]", bracket);
1335
+ pushSpecial(subsetEnd === -1 ? index + 2 : subsetEnd, ">");
1336
+ } else {
1337
+ pushSpecial(index + 2, ">");
1338
+ }
1339
+ continue;
1340
+ }
1341
+ let cursor = index + 1;
1342
+ let quote = null;
1343
+ while (cursor < length) {
1344
+ const ch = input[cursor];
1345
+ if (quote) {
1346
+ if (ch === quote) quote = null;
1347
+ } else if (ch === '"' || ch === "'") {
1348
+ quote = ch;
1349
+ } else if (ch === ">") {
1350
+ break;
1351
+ }
1352
+ cursor += 1;
1353
+ }
1354
+ const stop = cursor >= length ? length : cursor + 1;
1355
+ tokens.push({ kind: "tag", raw: input.slice(index, stop) });
1356
+ index = stop;
1357
+ }
1358
+ return tokens;
1359
+ };
1360
+ var isXmlDeclaration = (raw) => /^<\?xml\b/i.test(raw);
1361
+ var cleanAttributes = (element, rawAttributes) => {
1362
+ let removed = 0;
1363
+ let dropElement = false;
1364
+ const kept = [];
1365
+ ATTRIBUTE_RE.lastIndex = 0;
1366
+ let match;
1367
+ while ((match = ATTRIBUTE_RE.exec(rawAttributes)) !== null) {
1368
+ const [whole, name, dq, sq, bare] = match;
1369
+ if (!name || name === "/") continue;
1370
+ const lower = name.toLowerCase();
1371
+ const value = dq ?? sq ?? bare ?? "";
1372
+ if (lower.startsWith("on")) {
1373
+ removed += 1;
1374
+ continue;
1375
+ }
1376
+ const isNamespaceDeclaration = lower === "xmlns" || lower.startsWith("xmlns:");
1377
+ const isData = lower.startsWith("data-");
1378
+ if (!isNamespaceDeclaration && !isData && !ALLOWED_ATTRIBUTES.has(lower)) {
1379
+ removed += 1;
1380
+ continue;
1381
+ }
1382
+ if (isDangerousValue(value)) {
1383
+ removed += 1;
1384
+ continue;
1385
+ }
1386
+ if (lower === "href" || lower === "xlink:href") {
1387
+ if (!isAcceptableHref(value, element)) {
1388
+ removed += 1;
1389
+ if (element === "image" || element === "use") dropElement = true;
1390
+ continue;
1391
+ }
1392
+ }
1393
+ if (lower === "style" && !isSafeStyle(value)) {
1394
+ removed += 1;
1395
+ continue;
1396
+ }
1397
+ kept.push(whole.trim());
1398
+ }
1399
+ return {
1400
+ attributes: kept.length ? ` ${kept.join(" ")}` : "",
1401
+ removed,
1402
+ dropElement
1403
+ };
1404
+ };
1405
+ var STYLE_OPEN_MARKER = "\0style-open\0";
1406
+ var sanitizeSvg = (input) => {
1407
+ const tokens = tokenize(input);
1408
+ const out = [];
1409
+ let removed = 0;
1410
+ let isSvg = false;
1411
+ let dropDepth = 0;
1412
+ const dropStack = [];
1413
+ let styleBuffer = null;
1414
+ for (const token of tokens) {
1415
+ if (token.kind === "special") {
1416
+ if (isXmlDeclaration(token.raw) && dropDepth === 0) {
1417
+ out.push(token.raw);
1418
+ } else {
1419
+ removed += 1;
1420
+ }
1421
+ continue;
1422
+ }
1423
+ if (token.kind === "text") {
1424
+ if (dropDepth > 0) continue;
1425
+ if (styleBuffer) {
1426
+ styleBuffer.push(token.raw);
1427
+ } else {
1428
+ out.push(token.raw);
1429
+ }
1430
+ continue;
1431
+ }
1432
+ const parsed = TAG_RE.exec(token.raw);
1433
+ if (!parsed) {
1434
+ removed += 1;
1435
+ continue;
1436
+ }
1437
+ const [, closing, rawName, rawAttributes, selfClosingSlash] = parsed;
1438
+ const name = rawName.toLowerCase();
1439
+ const isClosing = closing === "/";
1440
+ const isSelfClosing = selfClosingSlash === "/";
1441
+ if (name === "svg") isSvg = true;
1442
+ if (dropDepth > 0) {
1443
+ if (isClosing) {
1444
+ if (dropStack[dropStack.length - 1] === name) {
1445
+ dropStack.pop();
1446
+ dropDepth -= 1;
1447
+ }
1448
+ } else if (!isSelfClosing) {
1449
+ dropStack.push(name);
1450
+ dropDepth += 1;
1451
+ }
1452
+ continue;
1453
+ }
1454
+ if (isClosing) {
1455
+ if (name === "style" && styleBuffer) {
1456
+ const css = styleBuffer.join("");
1457
+ styleBuffer = null;
1458
+ if (isSafeStyle(css)) {
1459
+ out.push(css, token.raw);
1460
+ } else {
1461
+ const openIndex = out.lastIndexOf(STYLE_OPEN_MARKER);
1462
+ if (openIndex !== -1) out.splice(openIndex);
1463
+ removed += 1;
1464
+ }
1465
+ continue;
1466
+ }
1467
+ if (ALLOWED_ELEMENTS.has(name)) out.push(token.raw);
1468
+ continue;
1469
+ }
1470
+ if (!ALLOWED_ELEMENTS.has(name)) {
1471
+ removed += 1;
1472
+ if (!isSelfClosing) {
1473
+ dropStack.push(name);
1474
+ dropDepth += 1;
1475
+ }
1476
+ continue;
1477
+ }
1478
+ const cleaned = cleanAttributes(name, rawAttributes);
1479
+ removed += cleaned.removed;
1480
+ if (cleaned.dropElement) {
1481
+ removed += 1;
1482
+ if (!isSelfClosing) {
1483
+ dropStack.push(name);
1484
+ dropDepth += 1;
1485
+ }
1486
+ continue;
1487
+ }
1488
+ const rebuilt = `<${rawName}${cleaned.attributes}${isSelfClosing ? "/" : ""}>`;
1489
+ if (name === "style" && !isSelfClosing) {
1490
+ out.push(STYLE_OPEN_MARKER, rebuilt);
1491
+ styleBuffer = [];
1492
+ continue;
1493
+ }
1494
+ out.push(rebuilt);
1495
+ }
1496
+ if (styleBuffer) {
1497
+ const openIndex = out.lastIndexOf(STYLE_OPEN_MARKER);
1498
+ if (openIndex !== -1) out.splice(openIndex);
1499
+ removed += 1;
1500
+ }
1501
+ return {
1502
+ svg: out.filter((chunk) => chunk !== STYLE_OPEN_MARKER).join(""),
1503
+ removed,
1504
+ isSvg
1505
+ };
1506
+ };
1507
+
1508
+ // src/custom-chart/icon.ts
1509
+ var UNSAFE = "bundle.icon.unsafe";
1510
+ var RULES = [
1511
+ { pattern: /<\s*script\b/i, message: "Remove every <script> element from icon.svg." },
1512
+ { pattern: /<\s*foreignObject\b/i, message: "Remove every <foreignObject> element from icon.svg." },
1513
+ { pattern: /<\s*image\b/i, message: "Remove every <image> element from icon.svg. Draw with paths and shapes." },
1514
+ { pattern: /[\s/]on[a-z]+\s*=/i, message: "Remove every on*= event attribute from icon.svg." },
1515
+ { pattern: /<!--/, message: "Remove every <!-- comment --> from icon.svg." },
1516
+ { pattern: /<!(?:ENTITY|DOCTYPE)\b/i, message: "Remove the DOCTYPE and entity declarations from icon.svg." },
1517
+ { pattern: /<!\[CDATA\[/, message: "Remove every CDATA section from icon.svg." },
1518
+ { pattern: /(?:xlink:)?href\s*=\s*["']\s*(?!#)/i, message: "Every href in icon.svg must start with # (no external or data references)." },
1519
+ { pattern: /javascript\s*:/i, message: "Remove every javascript: reference from icon.svg." },
1520
+ { pattern: /@import|expression\s*\(|-moz-binding/i, message: "Remove @import, expression() and -moz-binding from icon.svg styles." },
1521
+ { pattern: /url\s*\(\s*['"]?\s*(?!#)/i, message: "Every url() in icon.svg must point at a same-document #id." }
1522
+ ];
1523
+ var GENERIC_UNSAFE = "icon.svg must use only plain SVG drawing elements and attributes: no script, event handlers, foreignObject, image, animation, external hrefs, url() styles, comments, DOCTYPE or unknown attributes.";
1524
+ var XML_DECLARATION = /^\s*<\?xml[^>]*\?>\s*/i;
1525
+ function isSingleSvgRoot(svg) {
1526
+ const body = svg.replace(/^/, "").replace(XML_DECLARATION, "").trim();
1527
+ return /^<svg[\s>][\s\S]*<\/svg>$/i.test(body) || /^<svg\b[^>]*\/>$/i.test(body);
1528
+ }
1529
+ function validateIcon(svg, path = "/icon.svg") {
1530
+ if (typeof svg !== "string" || svg.trim().length === 0) {
1531
+ return [{ code: "bundle.icon.required", path, message: "icon.svg is required and must not be empty." }];
1532
+ }
1533
+ if (Buffer.byteLength(svg, "utf8") > MAX_ICON_BYTES) {
1534
+ return [{ code: "bundle.icon.size", path, message: `icon.svg must be at most ${MAX_ICON_BYTES} bytes.` }];
1535
+ }
1536
+ const cleaned = sanitizeSvg(svg);
1537
+ if (!cleaned.isSvg || !isSingleSvgRoot(cleaned.svg)) {
1538
+ return [{ code: "bundle.icon.notSvg", path, message: "icon.svg must be one <svg> element with nothing after it." }];
1539
+ }
1540
+ if (cleaned.removed > 0 || /<\s*image\b/i.test(svg)) {
1541
+ const rule = RULES.find((candidate) => candidate.pattern.test(svg));
1542
+ return [{ code: UNSAFE, path, message: rule ? rule.message : GENERIC_UNSAFE }];
1543
+ }
1544
+ return [];
1545
+ }
1546
+ function sanitizeIcon(svg) {
1547
+ return svg.replace(/^/, "").replace(XML_DECLARATION, "").replace(/<!--[\s\S]*?-->/g, "").trim();
1548
+ }
1549
+
1550
+ // src/custom-chart/zip.ts
1551
+ import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
1552
+ var BUNDLE_ZIP_FILES = ["index.js", "manifest.json", "icon.svg", "index.css"];
1553
+ function writeBundleZip(files) {
1554
+ const entries = {};
1555
+ for (const name of BUNDLE_ZIP_FILES) {
1556
+ const content = files[name];
1557
+ if (typeof content === "string") entries[name] = strToU8(content);
1558
+ }
1559
+ return zipSync(entries, { level: 6, mtime: /* @__PURE__ */ new Date("2000-01-01T00:00:00Z") });
1560
+ }
1561
+
1562
+ // src/custom-chart/bundle.ts
1563
+ var utf8Bytes = (text) => Buffer.byteLength(text, "utf8");
1564
+ function contentHash(bundle) {
1565
+ const payload = JSON.stringify({
1566
+ manifest: sortKeys(bundle.manifest),
1567
+ bundleJs: bundle.bundleJs,
1568
+ bundleCss: bundle.bundleCss ?? "",
1569
+ iconSvg: bundle.iconSvg
1570
+ });
1571
+ return createHash("sha256").update(payload, "utf8").digest("hex");
1572
+ }
1573
+ function bundleSizeBytes(bundle) {
1574
+ return utf8Bytes(JSON.stringify(bundle.manifest)) + utf8Bytes(bundle.bundleJs) + (bundle.bundleCss ? utf8Bytes(bundle.bundleCss) : 0) + utf8Bytes(bundle.iconSvg);
1575
+ }
1576
+ function validateBundle(input) {
1577
+ const problems = validateManifest(input.manifest);
1578
+ if (input.bundleJs !== void 0) {
1579
+ if (typeof input.bundleJs !== "string") {
1580
+ problems.push({ code: "bundle.js.type", path: "/bundleJs", message: "bundleJs must be a string." });
1581
+ } else if (input.bundleJs.trim().length === 0) {
1582
+ problems.push({ code: "bundle.js.empty", path: "/bundleJs", message: "bundleJs must contain JavaScript." });
1583
+ } else if (utf8Bytes(input.bundleJs) > MAX_BUNDLE_JS_BYTES) {
1584
+ problems.push({
1585
+ code: "bundle.js.size",
1586
+ path: "/bundleJs",
1587
+ message: `bundleJs must be at most ${MAX_BUNDLE_JS_BYTES} bytes (2 MiB) as UTF-8. Drop or shrink a dependency.`
1588
+ });
1589
+ }
1590
+ }
1591
+ if (input.bundleCss !== void 0) {
1592
+ if (typeof input.bundleCss !== "string") {
1593
+ problems.push({ code: "bundle.css.type", path: "/bundleCss", message: "bundleCss must be a string when present." });
1594
+ } else if (utf8Bytes(input.bundleCss) > MAX_BUNDLE_CSS_BYTES) {
1595
+ problems.push({
1596
+ code: "bundle.css.size",
1597
+ path: "/bundleCss",
1598
+ message: `bundleCss must be at most ${MAX_BUNDLE_CSS_BYTES} bytes (256 KiB).`
1599
+ });
1600
+ }
1601
+ }
1602
+ if (input.iconSvg !== void 0) problems.push(...validateIcon(input.iconSvg, "/iconSvg"));
1603
+ return problems;
1604
+ }
1605
+ function readProject(dirInput) {
1606
+ const dir = resolve2(dirInput);
1607
+ const problems = [];
1608
+ const project = { dir, manifest: void 0, problems };
1609
+ const manifestPath = join2(dir, "manifest.json");
1610
+ if (!existsSync2(manifestPath)) {
1611
+ problems.push({ code: "project.manifest.missing", path: "/manifest.json", message: `No manifest.json in ${dir}.` });
1612
+ } else {
1613
+ project.manifestText = readFileSync2(manifestPath, "utf8");
1614
+ try {
1615
+ project.manifest = JSON.parse(project.manifestText);
1616
+ } catch (err) {
1617
+ problems.push({
1618
+ code: "project.manifest.parse",
1619
+ path: "/manifest.json",
1620
+ message: `manifest.json is not valid JSON: ${err.message}`
1621
+ });
1622
+ }
1623
+ }
1624
+ const entry = ["chart.ts", "chart.js"].map((name) => join2(dir, name)).find((path) => existsSync2(path));
1625
+ if (!entry) {
1626
+ problems.push({ code: "project.chart.missing", path: "/chart.ts", message: `No chart.ts in ${dir}. It must export render(ctx).` });
1627
+ } else {
1628
+ project.chartEntry = entry;
1629
+ }
1630
+ const cssPath = join2(dir, "chart.css");
1631
+ if (existsSync2(cssPath)) project.css = readFileSync2(cssPath, "utf8");
1632
+ const iconPath = join2(dir, "icon.svg");
1633
+ if (!existsSync2(iconPath)) {
1634
+ problems.push({ code: "bundle.icon.required", path: "/icon.svg", message: `No icon.svg in ${dir}.` });
1635
+ } else {
1636
+ project.icon = readFileSync2(iconPath, "utf8");
1637
+ }
1638
+ const samplePath = join2(dir, "sample-data.json");
1639
+ if (existsSync2(samplePath)) {
1640
+ try {
1641
+ project.sampleData = JSON.parse(readFileSync2(samplePath, "utf8"));
1642
+ } catch (err) {
1643
+ problems.push({
1644
+ code: "project.sampleData.parse",
1645
+ path: "/sample-data.json",
1646
+ message: `sample-data.json is not valid JSON: ${err.message}`
1647
+ });
1648
+ }
1649
+ }
1650
+ return project;
1651
+ }
1652
+ function validateProject(dir) {
1653
+ const project = readProject(dir);
1654
+ const problems = [...project.problems];
1655
+ if (project.manifest !== void 0) problems.push(...validateManifest(project.manifest));
1656
+ if (project.icon !== void 0) problems.push(...validateIcon(sanitizeIcon(project.icon)));
1657
+ if (project.css !== void 0 && utf8Bytes(project.css) > MAX_BUNDLE_CSS_BYTES) {
1658
+ problems.push({ code: "bundle.css.size", path: "/chart.css", message: `chart.css must be at most ${MAX_BUNDLE_CSS_BYTES} bytes.` });
1659
+ }
1660
+ return { ok: problems.length === 0, problems, project };
1661
+ }
1662
+ function insideProjectOnly(dir) {
1663
+ const root = dir.endsWith(sep) ? dir : `${dir}${sep}`;
1664
+ return {
1665
+ name: "databrain-inside-project-only",
1666
+ setup(build) {
1667
+ build.onResolve({ filter: /.*/ }, async (args) => {
1668
+ if (args.kind === "entry-point" || args.pluginData?.databrainChecked) return void 0;
1669
+ const resolved = await build.resolve(args.path, {
1670
+ kind: args.kind,
1671
+ resolveDir: args.resolveDir,
1672
+ importer: args.importer,
1673
+ pluginData: { databrainChecked: true }
1674
+ });
1675
+ if (resolved.errors.length > 0) return { errors: resolved.errors };
1676
+ if (resolved.external || resolved.path.startsWith(root)) return resolved;
1677
+ return {
1678
+ errors: [
1679
+ {
1680
+ text: `"${args.path}" is outside the chart project. Only files inside ${dir} can be imported.`
1681
+ }
1682
+ ]
1683
+ };
1684
+ });
1685
+ }
1686
+ };
1687
+ }
1688
+ async function buildProject(dir, options = {}) {
1689
+ const validated = validateProject(dir);
1690
+ const { project } = validated;
1691
+ const problems = [...validated.problems];
1692
+ if (!project.chartEntry || project.manifest === void 0 || problems.length > 0) {
1693
+ return { ok: false, problems, written: [] };
1694
+ }
1695
+ const manifest = project.manifest;
1696
+ const common = {
1697
+ plugins: [insideProjectOnly(project.dir)],
1698
+ entryPoints: [project.chartEntry],
1699
+ absWorkingDir: project.dir,
1700
+ bundle: true,
1701
+ target: "es2020",
1702
+ platform: "browser",
1703
+ write: false,
1704
+ legalComments: "none",
1705
+ logLevel: "silent",
1706
+ charset: "utf8"
1707
+ };
1708
+ let moduleJs;
1709
+ try {
1710
+ const shape = await esbuild({ ...common, format: "esm", metafile: true });
1711
+ const exported = Object.values(shape.metafile.outputs)[0]?.exports ?? [];
1712
+ if (!exported.includes("render")) {
1713
+ problems.push({
1714
+ code: "bundle.build.noRender",
1715
+ path: "/chart.ts",
1716
+ message: "chart.ts must be an ES module that exports a function named render: `export function render(ctx) { ... }`. module.exports and default exports are not read."
1717
+ });
1718
+ return { ok: false, problems, written: [] };
1719
+ }
1720
+ const result = await esbuild({
1721
+ ...common,
1722
+ format: "iife",
1723
+ globalName: "__dbnCustomChart",
1724
+ minify: true
1725
+ });
1726
+ moduleJs = result.outputFiles.map((file) => file.text).join("\n");
1727
+ } catch (err) {
1728
+ const errors = err.errors;
1729
+ if (Array.isArray(errors) && errors.length > 0) {
1730
+ for (const error of errors) {
1731
+ const where = error.location ? `${error.location.file ?? "chart.ts"}:${error.location.line ?? 0}` : "chart.ts";
1732
+ problems.push({ code: "bundle.build.failed", path: `/${where}`, message: error.text });
1733
+ }
1734
+ } else {
1735
+ problems.push({ code: "bundle.build.failed", path: "/chart.ts", message: err.message });
1736
+ }
1737
+ return { ok: false, problems, written: [] };
1738
+ }
1739
+ const shim = readRuntimeShim();
1740
+ const slotIds = manifest.slots.map((slot) => slot.id);
1741
+ const globals = `globalThis.__dbnCustomChartSlots=${JSON.stringify(slotIds)};
1742
+ `;
1743
+ const bundleJs = `${shim.trimEnd()}
1744
+ ${globals}${moduleJs.trimEnd()}
1745
+ globalThis.__dbnCustomChart=__dbnCustomChart;
1746
+ `;
1747
+ const iconSvg = sanitizeIcon(project.icon);
1748
+ const bundleCss = project.css;
1749
+ problems.push(...validateBundle({ manifest, bundleJs, bundleCss, iconSvg }));
1750
+ if (problems.length > 0) return { ok: false, problems, written: [] };
1751
+ const bundle = { manifest, bundleJs, bundleCss, iconSvg };
1752
+ const outputs = {
1753
+ ...bundle,
1754
+ contentHash: contentHash(bundle),
1755
+ sizeBytes: bundleSizeBytes(bundle)
1756
+ };
1757
+ const written = [];
1758
+ if (options.write !== false) {
1759
+ const dist = join2(project.dir, "dist");
1760
+ mkdirSync(dist, { recursive: true });
1761
+ const manifestJson = JSON.stringify(manifest, null, 2) + "\n";
1762
+ const write = (name, content) => {
1763
+ const path = join2(dist, name);
1764
+ writeFileSync(path, content);
1765
+ written.push(path);
1766
+ };
1767
+ write("index.js", bundleJs);
1768
+ write("manifest.json", manifestJson);
1769
+ write("icon.svg", iconSvg);
1770
+ if (bundleCss !== void 0) write("index.css", bundleCss);
1771
+ write(
1772
+ "bundle.zip",
1773
+ writeBundleZip({
1774
+ "index.js": bundleJs,
1775
+ "manifest.json": manifestJson,
1776
+ "icon.svg": iconSvg,
1777
+ ...bundleCss !== void 0 ? { "index.css": bundleCss } : {}
1778
+ })
1779
+ );
1780
+ }
1781
+ return { ok: true, problems: [], outputs, written };
1782
+ }
1783
+ function readDist(dir) {
1784
+ const dist = join2(resolve2(dir), "dist");
1785
+ const problems = [];
1786
+ const need = ["index.js", "manifest.json", "icon.svg"];
1787
+ for (const name of need) {
1788
+ if (!existsSync2(join2(dist, name))) {
1789
+ problems.push({ code: "project.dist.missing", path: `/dist/${name}`, message: `dist/${name} is missing. Run databrain custom-chart build first.` });
1790
+ }
1791
+ }
1792
+ if (problems.length > 0) return { problems };
1793
+ let manifest;
1794
+ try {
1795
+ manifest = JSON.parse(readFileSync2(join2(dist, "manifest.json"), "utf8"));
1796
+ } catch (err) {
1797
+ return { problems: [{ code: "project.dist.parse", path: "/dist/manifest.json", message: err.message }] };
1798
+ }
1799
+ const manifestProblems = validateManifest(manifest);
1800
+ if (manifestProblems.length > 0) return { problems: manifestProblems };
1801
+ const cssPath = join2(dist, "index.css");
1802
+ return {
1803
+ problems,
1804
+ bundle: {
1805
+ manifest,
1806
+ bundleJs: readFileSync2(join2(dist, "index.js"), "utf8"),
1807
+ bundleCss: existsSync2(cssPath) ? readFileSync2(cssPath, "utf8") : void 0,
1808
+ iconSvg: readFileSync2(join2(dist, "icon.svg"), "utf8")
1809
+ }
1810
+ };
1811
+ }
1812
+
1813
+ // src/api/custom-charts-api.ts
1814
+ var CUSTOM_CHARTS_PATH = "/api/mcp/custom-charts";
1815
+ var LIST_CACHE_KEY = "list_custom_charts";
1816
+ var encodeKey = (key) => encodeURIComponent(key);
1817
+ var CustomChartsApi = class {
1818
+ constructor(client) {
1819
+ this.client = client;
1820
+ }
1821
+ async list() {
1822
+ const response = await this.client.request({
1823
+ method: "GET",
1824
+ path: CUSTOM_CHARTS_PATH,
1825
+ auth: "service",
1826
+ cacheKey: LIST_CACHE_KEY,
1827
+ cacheTtlMs: 3e4
1828
+ });
1829
+ if (Array.isArray(response)) return response;
1830
+ return Array.isArray(response?.charts) ? response.charts : [];
1831
+ }
1832
+ async get(key) {
1833
+ const response = await this.client.request({
1834
+ method: "GET",
1835
+ path: `${CUSTOM_CHARTS_PATH}/${encodeKey(key)}`,
1836
+ auth: "service"
1837
+ });
1838
+ const wrapped = response;
1839
+ return wrapped.chart ? wrapped.chart : response;
1840
+ }
1841
+ /** Never persists. The server repeats every client side check. */
1842
+ async validate(body) {
1843
+ const response = await this.client.request({
1844
+ method: "POST",
1845
+ path: `${CUSTOM_CHARTS_PATH}/validate`,
1846
+ auth: "service",
1847
+ body,
1848
+ timeoutMs: 6e4
1849
+ });
1850
+ return {
1851
+ ok: response.ok === true,
1852
+ problems: Array.isArray(response.problems) ? response.problems : [],
1853
+ contentHash: response.contentHash,
1854
+ sizeBytes: response.sizeBytes
1855
+ };
1856
+ }
1857
+ /** Creates the chart (first upload) or a preview version; idempotent on contentHash. */
1858
+ async upload(body) {
1859
+ return this.client.request({
1860
+ method: "POST",
1861
+ path: CUSTOM_CHARTS_PATH,
1862
+ auth: "service",
1863
+ body,
1864
+ timeoutMs: 6e4,
1865
+ invalidateCache: [LIST_CACHE_KEY]
1866
+ });
1867
+ }
1868
+ async publish(key, versionId) {
1869
+ return this.client.request({
1870
+ method: "POST",
1871
+ path: `${CUSTOM_CHARTS_PATH}/${encodeKey(key)}/publish`,
1872
+ auth: "service",
1873
+ body: versionId ? { versionId } : {},
1874
+ invalidateCache: [LIST_CACHE_KEY]
1875
+ });
1876
+ }
1877
+ async discardPreview(key) {
1878
+ return this.client.request({
1879
+ method: "POST",
1880
+ path: `${CUSTOM_CHARTS_PATH}/${encodeKey(key)}/discard-preview`,
1881
+ auth: "service",
1882
+ body: {},
1883
+ invalidateCache: [LIST_CACHE_KEY]
1884
+ });
1885
+ }
1886
+ };
1887
+
1888
+ // src/custom-chart/api.ts
1889
+ function createCustomChartsApi(client) {
1890
+ return new CustomChartsApi(client);
1891
+ }
1892
+ function apiProblems(err) {
1893
+ if (!(err instanceof DatabrainApiError)) return [];
1894
+ const details = err.details;
1895
+ const list = details?.error?.problems ?? details?.problems;
1896
+ if (!Array.isArray(list)) return [];
1897
+ return list.filter(
1898
+ (item) => !!item && typeof item === "object" && typeof item.code === "string" && typeof item.message === "string"
1899
+ );
1900
+ }
1901
+ function planUpload(key, existing, versionsKnown) {
1902
+ if (!existing) {
1903
+ return { action: "create chart", key, nextVersionNumber: 1, summary: `will create chart custom:${key} (version 1, status private)` };
1904
+ }
1905
+ const count = typeof versionsKnown === "number" ? versionsKnown : typeof existing.versionsCount === "number" ? existing.versionsCount : void 0;
1906
+ const next = count !== void 0 ? count + 1 : NaN;
1907
+ const label = Number.isFinite(next) ? `version ${next}` : "the next version";
1908
+ return {
1909
+ action: "create version",
1910
+ key,
1911
+ existing,
1912
+ nextVersionNumber: Number.isFinite(next) ? next : 0,
1913
+ summary: `will create ${label} of custom:${key} in preview (status stays ${existing.status}; the published version keeps serving until you publish)`
1914
+ };
1915
+ }
1916
+
1917
+ export {
1918
+ oauthHttpsUrl,
1919
+ oauthConfigSchema,
1920
+ DatabrainApiError,
1921
+ toolError,
1922
+ stringify,
1923
+ toolSuccess,
1924
+ registerSecret,
1925
+ redactSecrets,
1926
+ logger,
1927
+ parseErrorBody,
1928
+ MCP_SERVER_VERSION,
1929
+ toolContext,
1930
+ DatabrainClient,
1931
+ CustomChartsApi,
1932
+ slotSchema,
1933
+ optionSchema,
1934
+ scaffoldProject,
1935
+ contentHash,
1936
+ bundleSizeBytes,
1937
+ validateBundle,
1938
+ readProject,
1939
+ validateProject,
1940
+ buildProject,
1941
+ readDist,
1942
+ createCustomChartsApi,
1943
+ apiProblems,
1944
+ planUpload
1945
+ };
1946
+ //# sourceMappingURL=chunk-OCBTNAAO.js.map