@datasynx/agentic-ai-cartography 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -130,6 +130,8 @@ var DOMAIN_PALETTE = [
130
130
  "#5eead4"
131
131
  ];
132
132
  var DRIFT_FIELDS = ["type", "name", "domain", "subDomain", "qualityScore", "metadata", "tags", "owner", "cost"];
133
+ var ANOMALY_KINDS = ["orphan", "shadow-it"];
134
+ var ANOMALY_SEVERITIES = ["low", "medium", "high"];
133
135
  var DEFAULT_ANOMALY_THRESHOLDS = {
134
136
  orphanWeakDegree: 1,
135
137
  shadowConfidence: 0.4,
@@ -154,15 +156,26 @@ var SECURITY_METADATA_KEYS = [
154
156
  var DriftConfigSchema = z.object({
155
157
  minSeverity: z.enum(SEVERITIES).default("info"),
156
158
  sinks: z.array(z.object({
157
- type: z.enum(["stdout", "webhook"]),
159
+ type: z.enum(["stdout", "webhook", "slack", "pagerduty", "jira"]),
158
160
  url: z.string().url().optional(),
159
161
  token: z.string().optional(),
160
- timeoutMs: z.number().int().positive().optional()
162
+ timeoutMs: z.number().int().positive().optional(),
163
+ routingKey: z.string().optional(),
164
+ email: z.string().optional(),
165
+ project: z.string().optional(),
166
+ issueType: z.string().optional()
161
167
  })).default([{ type: "stdout" }])
162
168
  }).superRefine((cfg, ctx) => {
163
169
  for (const [i, s] of cfg.sinks.entries()) {
164
- if (s.type === "webhook" && !s.url) {
165
- ctx.addIssue({ code: "custom", path: ["sinks", i, "url"], message: "webhook sink requires a url" });
170
+ const requireUrl = (msg) => {
171
+ if (!s.url) ctx.addIssue({ code: "custom", path: ["sinks", i, "url"], message: msg });
172
+ };
173
+ if (s.type === "webhook") requireUrl("webhook sink requires a url");
174
+ if (s.type === "slack") requireUrl("slack sink requires a webhook url");
175
+ if (s.type === "jira") {
176
+ requireUrl("jira sink requires a base url");
177
+ if (!s.email) ctx.addIssue({ code: "custom", path: ["sinks", i, "email"], message: "jira sink requires an email" });
178
+ if (!s.project) ctx.addIssue({ code: "custom", path: ["sinks", i, "project"], message: "jira sink requires a project key" });
166
179
  }
167
180
  }
168
181
  });
@@ -1275,8 +1288,8 @@ var cloudGcpScanner = {
1275
1288
  allowedCommands: ["gcloud"],
1276
1289
  detect: (ctx) => Boolean((ctx.commandExists ?? commandExists)("gcloud")),
1277
1290
  async scan(ctx) {
1278
- const { project } = parseScanHint(ctx.hint);
1279
- const pf = project ? ` --project ${project}` : "";
1291
+ const { project: project2 } = parseScanHint(ctx.hint);
1292
+ const pf = project2 ? ` --project ${project2}` : "";
1280
1293
  const runG = createScanRunner((c) => ctx.run(c, { timeout: 2e4 }), { threshold: 3 });
1281
1294
  const nodes = [];
1282
1295
  const edges = [];
@@ -1776,8 +1789,17 @@ function stripSensitive(target) {
1776
1789
  const stripped = `${url.hostname}${url.port ? ":" + url.port : ""}`;
1777
1790
  return stripped || raw;
1778
1791
  } catch {
1779
- const stripped = raw.replace(/\/.*$/, "").replace(/\?.*$/, "").replace(/@.*:/, ":");
1780
- return stripped || raw;
1792
+ let s = raw;
1793
+ const slash = s.indexOf("/");
1794
+ if (slash >= 0) s = s.slice(0, slash);
1795
+ const q = s.indexOf("?");
1796
+ if (q >= 0) s = s.slice(0, q);
1797
+ const at = s.indexOf("@");
1798
+ if (at >= 0) {
1799
+ const colon = s.lastIndexOf(":");
1800
+ if (colon > at) s = s.slice(0, at) + ":" + s.slice(colon + 1);
1801
+ }
1802
+ return s || raw;
1781
1803
  }
1782
1804
  }
1783
1805
  var SCAN_ARG_PATTERNS = {
@@ -1795,7 +1817,7 @@ function assertSafeScanArg(kind, value) {
1795
1817
  return value;
1796
1818
  }
1797
1819
  function redactSecrets(value) {
1798
- return value.replace(/([a-z][a-z0-9+.-]*:\/\/[^:@/\s]+):[^@/\s]+@/gi, "$1:***@");
1820
+ return value.replace(/([a-z][a-z0-9+.-]{0,63}:\/\/[^:@/\s]{1,256}):[^@/\s]{1,256}@/gi, "$1:***@");
1799
1821
  }
1800
1822
  function redactValue(value) {
1801
1823
  if (typeof value === "string") return redactSecrets(value);
@@ -1964,9 +1986,9 @@ async function buildCartographyToolHandlers(db, sessionId, opts = {}) {
1964
1986
  tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
1965
1987
  project: z2.string().regex(SCAN_ARG_PATTERNS["gcp-project"], "invalid GCP project id").optional().describe("GCP Project ID \u2014 default: current gcloud project")
1966
1988
  }, async (args) => {
1967
- const project = args["project"];
1968
- if (project) assertSafeScanArg("gcp-project", project);
1969
- return runScannerTool(cloudGcpScanner, project ? `project=${project}` : "");
1989
+ const project2 = args["project"];
1990
+ if (project2) assertSafeScanArg("gcp-project", project2);
1991
+ return runScannerTool(cloudGcpScanner, project2 ? `project=${project2}` : "");
1970
1992
  }, { annotations: READ_SCAN }),
1971
1993
  tool("scan_azure_resources", "Scan Azure infrastructure via az CLI \u2014 100% readonly (list, show)", {
1972
1994
  subscription: z2.string().regex(SCAN_ARG_PATTERNS["azure-subscription"], "invalid Azure subscription id").optional().describe("Azure Subscription ID"),
@@ -2118,14 +2140,14 @@ async function buildCartographyToolHandlers(db, sessionId, opts = {}) {
2118
2140
  "neon"
2119
2141
  ];
2120
2142
  const found = [];
2121
- const notFound = [];
2143
+ const notFound2 = [];
2122
2144
  for (const t of knownTools) {
2123
2145
  const r = commandExists(t);
2124
2146
  if (r) found.push(`${t}: ${r}`);
2125
- else notFound.push(t);
2147
+ else notFound2.push(t);
2126
2148
  }
2127
2149
  results["TOOLS_FOUND"] = found.join("\n") || "(none found)";
2128
- results["TOOLS_NOT_FOUND"] = notFound.join(", ");
2150
+ results["TOOLS_NOT_FOUND"] = notFound2.join(", ");
2129
2151
  if (hint) {
2130
2152
  const terms = hint.split(/[\s,]+/).filter(Boolean);
2131
2153
  const hintResults = [];
@@ -4262,6 +4284,86 @@ var SqliteStoreBackend = class {
4262
4284
  }
4263
4285
  };
4264
4286
 
4287
+ // src/store/query.ts
4288
+ var NotFoundError = class extends Error {
4289
+ constructor(message) {
4290
+ super(message);
4291
+ this.name = "NotFoundError";
4292
+ }
4293
+ };
4294
+ var MAX_NODE_LIMIT = 1e3;
4295
+ var MAX_DEPTH = 64;
4296
+ function clamp(value, min, max) {
4297
+ return Math.floor(Math.max(min, Math.min(value, max)));
4298
+ }
4299
+ var SqliteQueryBackend = class {
4300
+ constructor(db, defaultSession = "latest") {
4301
+ this.db = db;
4302
+ this.defaultSession = defaultSession;
4303
+ }
4304
+ /**
4305
+ * Resolve the session id for a request, scoped to `ctx.tenant`. An explicit id must
4306
+ * belong to the tenant or it resolves to undefined (cross-tenant isolation); else the
4307
+ * newest `discover` session for the tenant. Mirrors `resolveSession` in the MCP server.
4308
+ */
4309
+ resolveSession(ctx, sessionId) {
4310
+ const requested = sessionId ?? (this.defaultSession === "latest" ? void 0 : this.defaultSession);
4311
+ if (requested) {
4312
+ const s = this.db.getSession(requested);
4313
+ if (s && s.tenant === ctx.tenant) return s.id;
4314
+ throw new NotFoundError(`session not found`);
4315
+ }
4316
+ const latest = this.db.getLatestSession("discover", ctx.tenant) ?? this.db.getLatestSession(void 0, ctx.tenant);
4317
+ if (!latest) throw new NotFoundError(`no session available`);
4318
+ return latest.id;
4319
+ }
4320
+ summary(ctx, sessionId) {
4321
+ return this.db.getGraphSummary(this.resolveSession(ctx, sessionId));
4322
+ }
4323
+ nodes(ctx, q, sessionId) {
4324
+ const sid = this.resolveSession(ctx, sessionId);
4325
+ const limit = clamp(q.limit ?? 100, 1, MAX_NODE_LIMIT);
4326
+ const offset = Math.floor(Math.max(0, q.offset ?? 0));
4327
+ const total = this.db.getNodeCount(sid);
4328
+ if (q.search) {
4329
+ const nodes2 = this.db.searchNodes(sid, q.search, { ...q.types ? { types: q.types } : {}, limit });
4330
+ return { nodes: nodes2, total: nodes2.length, limit, offset: 0 };
4331
+ }
4332
+ const nodes = this.db.getNodes(sid, { limit, offset });
4333
+ return { nodes, total, limit, offset };
4334
+ }
4335
+ node(ctx, id, sessionId) {
4336
+ return this.db.getNode(this.resolveSession(ctx, sessionId), id);
4337
+ }
4338
+ dependencies(ctx, id, q, sessionId) {
4339
+ const sid = this.resolveSession(ctx, sessionId);
4340
+ return this.db.getDependencies(sid, id, {
4341
+ direction: q.direction ?? "downstream",
4342
+ maxDepth: clamp(q.maxDepth ?? 8, 1, MAX_DEPTH)
4343
+ });
4344
+ }
4345
+ diff(ctx, base, current) {
4346
+ for (const id of [base, current]) {
4347
+ const s = this.db.getSession(id);
4348
+ if (!s || s.tenant !== ctx.tenant) throw new NotFoundError(`session not found`);
4349
+ }
4350
+ try {
4351
+ return this.db.diffSessions(base, current);
4352
+ } catch (err) {
4353
+ throw new NotFoundError(err instanceof Error ? err.message : "diff failed");
4354
+ }
4355
+ }
4356
+ sessions(ctx) {
4357
+ return this.db.getSessions(ctx.tenant);
4358
+ }
4359
+ health(ctx) {
4360
+ return { store: "sqlite", sessions: this.db.getSessions(ctx.tenant).length };
4361
+ }
4362
+ };
4363
+ function createSqliteQueryBackend(db, defaultSession = "latest") {
4364
+ return new SqliteQueryBackend(db, defaultSession);
4365
+ }
4366
+
4265
4367
  // src/central/merge.ts
4266
4368
  function computeIdentity(org, node) {
4267
4369
  return {
@@ -4813,6 +4915,41 @@ var StdoutSink = class {
4813
4915
 
4814
4916
  // src/sinks/webhook.ts
4815
4917
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
4918
+ async function postJson(opts) {
4919
+ const doFetch = opts.fetchImpl ?? (typeof fetch === "function" ? fetch : void 0);
4920
+ if (!doFetch) {
4921
+ logWarn("sink unavailable: global fetch missing", { sink: opts.sinkName });
4922
+ return;
4923
+ }
4924
+ if (!opts.url) {
4925
+ logWarn("sink unavailable: no url configured", { sink: opts.sinkName });
4926
+ return;
4927
+ }
4928
+ if (!isSecureWebhookUrl(opts.url)) {
4929
+ logWarn("sink refused: insecure scheme (use https:// or a loopback host)", {
4930
+ sink: opts.sinkName,
4931
+ host: stripSensitive(opts.url)
4932
+ });
4933
+ return;
4934
+ }
4935
+ try {
4936
+ const res = await doFetch(opts.url, {
4937
+ method: "POST",
4938
+ headers: { "content-type": "application/json", ...opts.headers ?? {} },
4939
+ body: JSON.stringify(opts.body),
4940
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
4941
+ });
4942
+ if (!res.ok) {
4943
+ logError("sink delivery failed", { sink: opts.sinkName, host: stripSensitive(opts.url), status: res.status });
4944
+ }
4945
+ } catch (err) {
4946
+ logError("sink delivery failed", {
4947
+ sink: opts.sinkName,
4948
+ host: stripSensitive(opts.url),
4949
+ reason: err instanceof Error ? err.message : String(err)
4950
+ });
4951
+ }
4952
+ }
4816
4953
  function isSecureWebhookUrl(url, env = process.env) {
4817
4954
  if (env.CARTOGRAPHY_ALLOW_INSECURE_SYNC === "1") return true;
4818
4955
  let parsed;
@@ -4831,59 +4968,177 @@ var WebhookSink = class {
4831
4968
  }
4832
4969
  name = "webhook";
4833
4970
  async emit(alert) {
4834
- if (typeof fetch !== "function") {
4835
- logWarn("webhook sink unavailable: global fetch missing", { sink: this.name });
4836
- return;
4837
- }
4838
4971
  const { url, token, timeoutMs } = this.opts;
4839
- if (!url) {
4840
- logWarn("webhook sink unavailable: no url configured", { sink: this.name });
4841
- return;
4842
- }
4843
- if (!isSecureWebhookUrl(url)) {
4844
- logWarn("webhook sink refused: insecure scheme (use https:// or a loopback host)", {
4845
- sink: this.name,
4846
- host: stripSensitive(url)
4847
- });
4848
- return;
4849
- }
4850
- try {
4851
- const res = await fetch(url, {
4852
- method: "POST",
4853
- headers: {
4854
- "content-type": "application/json",
4855
- ...token ? { authorization: `Bearer ${token}` } : {}
4856
- },
4857
- body: JSON.stringify(redactValue(alert)),
4858
- signal: AbortSignal.timeout(timeoutMs ?? 1e4)
4859
- });
4860
- if (!res.ok) {
4861
- logError("webhook sink failed", { sink: this.name, host: stripSensitive(url), status: res.status });
4972
+ await postJson({
4973
+ url,
4974
+ body: redactValue(alert),
4975
+ ...token ? { headers: { authorization: `Bearer ${token}` } } : {},
4976
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
4977
+ sinkName: this.name
4978
+ });
4979
+ }
4980
+ };
4981
+
4982
+ // src/sinks/providers.ts
4983
+ var MAX_ITEMS2 = 20;
4984
+ var SEVERITY_EMOJI = { info: "\u{1F7E2}", warning: "\u{1F7E1}", critical: "\u{1F534}" };
4985
+ function headline(alert) {
4986
+ const s = alert.summary;
4987
+ return `${s.nodesAdded}+ / ${s.nodesRemoved}- / ${s.nodesChanged}~ nodes, ${s.edgesAdded}+ / ${s.edgesRemoved}- edges`;
4988
+ }
4989
+ function itemLine(it) {
4990
+ const sec = it.securityFields?.length ? ` [security: ${it.securityFields.join(", ")}]` : "";
4991
+ const fields = it.changedFields?.length ? ` (${it.changedFields.join(", ")})` : "";
4992
+ return `${it.severity.toUpperCase()} \xB7 ${it.kind} \xB7 ${it.label}${fields}${sec}`;
4993
+ }
4994
+ function bodyText(alert) {
4995
+ const lines = alert.items.slice(0, MAX_ITEMS2).map(itemLine);
4996
+ const more = alert.items.length > MAX_ITEMS2 ? [`\u2026and ${alert.items.length - MAX_ITEMS2} more`] : [];
4997
+ return [headline(alert), "", ...lines, ...more].join("\n");
4998
+ }
4999
+ function formatSlack(alert) {
5000
+ const title = `${SEVERITY_EMOJI[alert.severity]} Topology drift \u2014 ${alert.severity}`;
5001
+ return {
5002
+ text: `${title}: ${headline(alert)}`,
5003
+ blocks: [
5004
+ { type: "header", text: { type: "plain_text", text: title, emoji: true } },
5005
+ { type: "section", text: { type: "mrkdwn", text: "```" + bodyText(alert) + "```" } },
5006
+ { type: "context", elements: [{ type: "mrkdwn", text: `base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId} \xB7 ${alert.generatedAt}` }] }
5007
+ ]
5008
+ };
5009
+ }
5010
+ var PD_SEVERITY = {
5011
+ info: "info",
5012
+ warning: "warning",
5013
+ critical: "critical"
5014
+ };
5015
+ function formatPagerDuty(alert, routingKey) {
5016
+ return {
5017
+ routing_key: routingKey,
5018
+ event_action: "trigger",
5019
+ // Stable per base→current pair so repeated alerts for the same delta de-duplicate.
5020
+ dedup_key: `cartograph-drift:${alert.base.sessionId}:${alert.current.sessionId}`,
5021
+ payload: {
5022
+ summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
5023
+ source: "cartograph",
5024
+ severity: PD_SEVERITY[alert.severity],
5025
+ timestamp: alert.generatedAt,
5026
+ custom_details: {
5027
+ summary: alert.summary,
5028
+ items: alert.items.slice(0, MAX_ITEMS2).map((it) => ({
5029
+ kind: it.kind,
5030
+ ref: it.ref,
5031
+ severity: it.severity,
5032
+ ...it.changedFields ? { changedFields: it.changedFields } : {},
5033
+ ...it.securityFields ? { securityFields: it.securityFields } : {}
5034
+ }))
4862
5035
  }
4863
- } catch (err) {
4864
- logError("webhook sink failed", {
4865
- sink: this.name,
4866
- host: stripSensitive(url),
4867
- reason: err instanceof Error ? err.message : String(err)
4868
- });
4869
5036
  }
5037
+ };
5038
+ }
5039
+ function formatJira(alert, opts) {
5040
+ return {
5041
+ fields: {
5042
+ project: { key: opts.project },
5043
+ issuetype: { name: opts.issueType ?? "Task" },
5044
+ summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
5045
+ description: bodyText(alert) + `
5046
+
5047
+ base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId}
5048
+ generated ${alert.generatedAt}`
5049
+ }
5050
+ };
5051
+ }
5052
+
5053
+ // src/sinks/provider-sink.ts
5054
+ var PAGERDUTY_ENQUEUE_URL = "https://events.pagerduty.com/v2/enqueue";
5055
+ function deliver(name, url, body, opts, headers) {
5056
+ return postJson({
5057
+ url,
5058
+ body,
5059
+ sinkName: name,
5060
+ ...headers ? { headers } : {},
5061
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
5062
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
5063
+ });
5064
+ }
5065
+ var SlackSink = class {
5066
+ constructor(opts) {
5067
+ this.opts = opts;
5068
+ }
5069
+ name = "slack";
5070
+ async emit(alert) {
5071
+ await deliver(this.name, this.opts.url, formatSlack(redactValue(alert)), this.opts);
5072
+ }
5073
+ };
5074
+ var PagerDutySink = class {
5075
+ constructor(opts) {
5076
+ this.opts = opts;
5077
+ }
5078
+ name = "pagerduty";
5079
+ async emit(alert) {
5080
+ const body = formatPagerDuty(redactValue(alert), this.opts.routingKey);
5081
+ await deliver(this.name, this.opts.url || PAGERDUTY_ENQUEUE_URL, body, this.opts);
5082
+ }
5083
+ };
5084
+ var JiraSink = class {
5085
+ constructor(opts) {
5086
+ this.opts = opts;
5087
+ }
5088
+ name = "jira";
5089
+ async emit(alert) {
5090
+ const body = formatJira(redactValue(alert), {
5091
+ project: this.opts.project,
5092
+ ...this.opts.issueType ? { issueType: this.opts.issueType } : {}
5093
+ });
5094
+ const auth = Buffer.from(`${this.opts.email}:${this.opts.token}`).toString("base64");
5095
+ const base = this.opts.url.replace(/\/+$/, "");
5096
+ await deliver(this.name, `${base}/rest/api/2/issue`, body, this.opts, { authorization: `Basic ${auth}` });
4870
5097
  }
4871
5098
  };
4872
5099
 
4873
5100
  // src/sinks/index.ts
4874
5101
  function buildSinks(drift) {
4875
5102
  const configs = drift?.sinks && drift.sinks.length > 0 ? drift.sinks : [{ type: "stdout" }];
5103
+ const envSecret = process.env.CARTOGRAPHY_DRIFT_TOKEN;
4876
5104
  const sinks = [];
4877
5105
  for (const s of configs) {
4878
- if (s.type === "webhook") {
4879
- if (!s.url) continue;
4880
- sinks.push(new WebhookSink({
4881
- url: s.url,
4882
- token: s.token ?? process.env.CARTOGRAPHY_DRIFT_TOKEN,
4883
- timeoutMs: s.timeoutMs
4884
- }));
4885
- } else {
4886
- sinks.push(new StdoutSink());
5106
+ const timeoutMs = s.timeoutMs;
5107
+ switch (s.type) {
5108
+ case "webhook":
5109
+ if (!s.url) {
5110
+ logWarn("drift sink skipped: webhook requires a url", { sink: s.type });
5111
+ break;
5112
+ }
5113
+ sinks.push(new WebhookSink({ url: s.url, token: s.token ?? envSecret, timeoutMs }));
5114
+ break;
5115
+ case "slack":
5116
+ if (!s.url) {
5117
+ logWarn("drift sink skipped: slack requires a webhook url", { sink: s.type });
5118
+ break;
5119
+ }
5120
+ sinks.push(new SlackSink({ url: s.url, timeoutMs }));
5121
+ break;
5122
+ case "pagerduty": {
5123
+ const routingKey = s.routingKey ?? s.token ?? envSecret;
5124
+ if (!routingKey) {
5125
+ logWarn("drift sink skipped: pagerduty requires a routingKey (or CARTOGRAPHY_DRIFT_TOKEN)", { sink: s.type });
5126
+ break;
5127
+ }
5128
+ sinks.push(new PagerDutySink({ url: s.url ?? PAGERDUTY_ENQUEUE_URL, routingKey, timeoutMs }));
5129
+ break;
5130
+ }
5131
+ case "jira": {
5132
+ const token = s.token ?? envSecret;
5133
+ if (!s.url || !s.email || !s.project || !token) {
5134
+ logWarn("drift sink skipped: jira requires url, email, project and a token", { sink: s.type });
5135
+ break;
5136
+ }
5137
+ sinks.push(new JiraSink({ url: s.url, email: s.email, token, project: s.project, issueType: s.issueType, timeoutMs }));
5138
+ break;
5139
+ }
5140
+ default:
5141
+ sinks.push(new StdoutSink());
4887
5142
  }
4888
5143
  }
4889
5144
  return sinks.length > 0 ? sinks : [new StdoutSink()];
@@ -5292,7 +5547,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5292
5547
 
5293
5548
  // src/mcp/server.ts
5294
5549
  var SERVER_NAME = "cartography";
5295
- var SERVER_VERSION = "2.2.0";
5550
+ var SERVER_VERSION = "2.4.0";
5296
5551
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5297
5552
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5298
5553
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -5793,6 +6048,50 @@ import { randomUUID as randomUUID2 } from "crypto";
5793
6048
  import http from "http";
5794
6049
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5795
6050
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6051
+
6052
+ // src/api/auth.ts
6053
+ var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
6054
+ function isLoopbackHost(host2) {
6055
+ return LOOPBACK_HOSTS2.has(host2);
6056
+ }
6057
+ function timingSafeEqual(a, b) {
6058
+ if (a.length !== b.length) return false;
6059
+ let diff = 0;
6060
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
6061
+ return diff === 0;
6062
+ }
6063
+ function bearerToken(header) {
6064
+ if (!header) return void 0;
6065
+ const trimmed = header.trim();
6066
+ if (trimmed.length < 7 || trimmed.slice(0, 6).toLowerCase() !== "bearer") return void 0;
6067
+ const rest = trimmed.slice(6);
6068
+ if (!/^\s/.test(rest)) return void 0;
6069
+ const token = rest.trimStart();
6070
+ return token.length > 0 ? token : void 0;
6071
+ }
6072
+ function checkBearer(authorizationHeader, token) {
6073
+ if (!token) return true;
6074
+ const provided = bearerToken(authorizationHeader);
6075
+ return provided !== void 0 && timingSafeEqual(provided, token);
6076
+ }
6077
+ function assertSafeBind(opts) {
6078
+ if (isLoopbackHost(opts.host)) return;
6079
+ if (opts.allowedHosts === void 0) {
6080
+ throw new Error(
6081
+ `Refusing to bind a non-loopback host (${opts.host}) without an explicit allowedHosts allowlist. Pass { allowedHosts: ['your.public.host:port'] } to opt in, or bind 127.0.0.1 for local-only use.`
6082
+ );
6083
+ }
6084
+ if (!opts.token) {
6085
+ throw new Error(
6086
+ `Refusing to bind a non-loopback host (${opts.host}) without an auth token. Pass { token } (or --token / CARTOGRAPHY_HTTP_TOKEN) so requests must carry 'Authorization: Bearer <token>'.`
6087
+ );
6088
+ }
6089
+ }
6090
+ function defaultAllowedHosts(host2, port) {
6091
+ return [`${host2}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
6092
+ }
6093
+
6094
+ // src/mcp/transports.ts
5796
6095
  async function runStdio(server) {
5797
6096
  const transport = new StdioServerTransport();
5798
6097
  await server.connect(transport);
@@ -5821,17 +6120,6 @@ async function readCappedBody(req, cap) {
5821
6120
  return { overflow: false, value: void 0 };
5822
6121
  }
5823
6122
  }
5824
- function timingSafeEqual(a, b) {
5825
- if (a.length !== b.length) return false;
5826
- let diff = 0;
5827
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
5828
- return diff === 0;
5829
- }
5830
- function bearerToken(header) {
5831
- if (!header) return void 0;
5832
- const m = /^Bearer\s+(.+)$/i.exec(header.trim());
5833
- return m ? m[1] : void 0;
5834
- }
5835
6123
  async function readJsonBody(req) {
5836
6124
  const chunks = [];
5837
6125
  for await (const chunk of req) chunks.push(chunk);
@@ -5842,22 +6130,11 @@ async function readJsonBody(req) {
5842
6130
  return void 0;
5843
6131
  }
5844
6132
  }
5845
- var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
5846
6133
  async function runHttp(factory, opts = {}) {
5847
6134
  const host2 = opts.host ?? "127.0.0.1";
5848
6135
  const port = opts.port ?? 3737;
5849
- const isLoopback = LOOPBACK_HOSTS2.has(host2);
5850
- if (!isLoopback && opts.allowedHosts === void 0) {
5851
- throw new Error(
5852
- `Refusing to bind a non-loopback host (${host2}) without an explicit allowedHosts allowlist. Pass { allowedHosts: ['your.public.host:port'] } to opt in, or bind 127.0.0.1 for local-only use.`
5853
- );
5854
- }
5855
- if (!isLoopback && !opts.token) {
5856
- throw new Error(
5857
- `Refusing to bind a non-loopback host (${host2}) without an auth token. Pass { token } (or --token / CARTOGRAPHY_HTTP_TOKEN) so requests must carry 'Authorization: Bearer <token>'.`
5858
- );
5859
- }
5860
- const allowedHosts = opts.allowedHosts ?? [`${host2}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
6136
+ assertSafeBind({ host: host2, port, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...opts.token ? { token: opts.token } : {} });
6137
+ const allowedHosts = opts.allowedHosts ?? defaultAllowedHosts(host2, port);
5861
6138
  const token = opts.token;
5862
6139
  const transports = /* @__PURE__ */ new Map();
5863
6140
  const httpServer = http.createServer(async (req, res) => {
@@ -5868,12 +6145,9 @@ async function runHttp(factory, opts = {}) {
5868
6145
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
5869
6146
  return;
5870
6147
  }
5871
- if (token) {
5872
- const provided = bearerToken(req.headers["authorization"]);
5873
- if (!provided || !timingSafeEqual(provided, token)) {
5874
- res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
5875
- return;
5876
- }
6148
+ if (!checkBearer(req.headers["authorization"], token)) {
6149
+ res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
6150
+ return;
5877
6151
  }
5878
6152
  if (isIngest) {
5879
6153
  const hostHeader = (req.headers["host"] ?? "").toLowerCase();
@@ -5927,7 +6201,7 @@ async function runHttp(factory, opts = {}) {
5927
6201
  if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" }).end('{"error":"internal error"}');
5928
6202
  }
5929
6203
  });
5930
- await new Promise((resolve2) => httpServer.listen(port, host2, resolve2));
6204
+ await new Promise((resolve3) => httpServer.listen(port, host2, resolve3));
5931
6205
  return httpServer;
5932
6206
  }
5933
6207
 
@@ -6109,8 +6383,8 @@ async function createSemanticSearch(db, embedder, opts = {}) {
6109
6383
  return lexicalSearch2();
6110
6384
  }
6111
6385
  const store = new VectorStore(db, provider);
6112
- const ok = await store.init();
6113
- if (!ok) {
6386
+ const ok3 = await store.init();
6387
+ if (!ok3) {
6114
6388
  log2?.("semantic search: vector store unavailable (sqlite-vec not installed or failed to load) \u2014 using lexical search");
6115
6389
  return lexicalSearch2();
6116
6390
  }
@@ -6719,6 +6993,1037 @@ function localDiscoveryFn(registry, plugins) {
6719
6993
  };
6720
6994
  }
6721
6995
 
6996
+ // src/api/server.ts
6997
+ import http2 from "http";
6998
+
6999
+ // src/api/tenant.ts
7000
+ var TENANT_HEADER = "x-cartograph-tenant";
7001
+ var InvalidTenantError = class extends Error {
7002
+ constructor() {
7003
+ super("invalid tenant");
7004
+ this.name = "InvalidTenantError";
7005
+ }
7006
+ };
7007
+ function resolveTenant(req, url, opts = {}) {
7008
+ const headerName = (opts.header ?? TENANT_HEADER).toLowerCase();
7009
+ const raw = headerValue(req, headerName) ?? url.searchParams.get("tenant") ?? void 0;
7010
+ if (raw === void 0 || raw === "") {
7011
+ return { tenant: opts.defaultTenant ?? DEFAULT_TENANT };
7012
+ }
7013
+ if (raw.trim().length > 128) {
7014
+ throw new InvalidTenantError();
7015
+ }
7016
+ const normalized = normalizeTenant(raw);
7017
+ if (normalized === DEFAULT_TENANT && raw.trim() !== DEFAULT_TENANT) {
7018
+ throw new InvalidTenantError();
7019
+ }
7020
+ return { tenant: normalized };
7021
+ }
7022
+ function headerValue(req, name) {
7023
+ const v = req.headers[name];
7024
+ if (Array.isArray(v)) return v[0];
7025
+ return v;
7026
+ }
7027
+
7028
+ // src/api/schemas.ts
7029
+ import { z as z8 } from "zod";
7030
+ var DIRECTIONS = ["downstream", "upstream", "both"];
7031
+ var CostSchema = z8.object({
7032
+ amount: z8.number(),
7033
+ currency: z8.string(),
7034
+ period: z8.enum(COST_PERIODS),
7035
+ source: z8.string().optional()
7036
+ });
7037
+ var NodeSchema2 = z8.object({
7038
+ id: z8.string(),
7039
+ type: z8.string(),
7040
+ name: z8.string(),
7041
+ confidence: z8.number(),
7042
+ domain: z8.string().optional(),
7043
+ subDomain: z8.string().optional(),
7044
+ qualityScore: z8.number().optional(),
7045
+ owner: z8.string().optional(),
7046
+ cost: CostSchema.optional(),
7047
+ tags: z8.array(z8.string())
7048
+ });
7049
+ var EdgeSchema2 = z8.object({
7050
+ sourceId: z8.string(),
7051
+ targetId: z8.string(),
7052
+ relationship: z8.string(),
7053
+ confidence: z8.number(),
7054
+ evidence: z8.string()
7055
+ });
7056
+ var AnomalySchema = z8.object({
7057
+ nodeId: z8.string(),
7058
+ kind: z8.enum(ANOMALY_KINDS),
7059
+ severity: z8.enum(ANOMALY_SEVERITIES),
7060
+ reason: z8.string()
7061
+ });
7062
+ var TopConnectedSchema = z8.object({
7063
+ id: z8.string(),
7064
+ name: z8.string(),
7065
+ type: z8.string(),
7066
+ degree: z8.number().int()
7067
+ });
7068
+ var CostByDomainSchema = z8.object({
7069
+ domain: z8.string(),
7070
+ currency: z8.string(),
7071
+ period: z8.string(),
7072
+ total: z8.number(),
7073
+ nodes: z8.number().int()
7074
+ });
7075
+ var CostByOwnerSchema = z8.object({
7076
+ owner: z8.string(),
7077
+ currency: z8.string(),
7078
+ period: z8.string(),
7079
+ total: z8.number(),
7080
+ nodes: z8.number().int()
7081
+ });
7082
+ var SummaryResponse = z8.object({
7083
+ sessionId: z8.string(),
7084
+ totals: z8.object({ nodes: z8.number().int(), edges: z8.number().int() }),
7085
+ nodesByType: z8.record(z8.string(), z8.number().int()),
7086
+ nodesByDomain: z8.record(z8.string(), z8.number().int()),
7087
+ edgesByRelationship: z8.record(z8.string(), z8.number().int()),
7088
+ topConnected: z8.array(TopConnectedSchema),
7089
+ anomalies: z8.array(AnomalySchema),
7090
+ contributors: z8.number().int(),
7091
+ costByDomain: z8.array(CostByDomainSchema),
7092
+ costByOwner: z8.array(CostByOwnerSchema),
7093
+ costCoverage: z8.object({ withCost: z8.number().int(), total: z8.number().int() })
7094
+ });
7095
+ var NodesResponse = z8.object({
7096
+ nodes: z8.array(NodeSchema2),
7097
+ total: z8.number().int(),
7098
+ limit: z8.number().int(),
7099
+ offset: z8.number().int()
7100
+ });
7101
+ var DependencyNodeSchema = NodeSchema2.extend({ depth: z8.number().int() });
7102
+ var DependenciesResponse = z8.object({
7103
+ root: NodeSchema2.optional(),
7104
+ direction: z8.enum(DIRECTIONS),
7105
+ maxDepth: z8.number().int(),
7106
+ nodes: z8.array(DependencyNodeSchema),
7107
+ edges: z8.array(EdgeSchema2)
7108
+ });
7109
+ var SessionEndpointSchema = z8.object({
7110
+ sessionId: z8.string(),
7111
+ startedAt: z8.string(),
7112
+ nodeCount: z8.number().int(),
7113
+ edgeCount: z8.number().int()
7114
+ });
7115
+ var NodeChangeSchema = z8.object({
7116
+ id: z8.string(),
7117
+ changedFields: z8.array(z8.string()),
7118
+ confidenceDelta: z8.number()
7119
+ });
7120
+ var DiffResponse = z8.object({
7121
+ base: SessionEndpointSchema,
7122
+ current: SessionEndpointSchema,
7123
+ summary: z8.object({
7124
+ nodesAdded: z8.number().int(),
7125
+ nodesRemoved: z8.number().int(),
7126
+ nodesChanged: z8.number().int(),
7127
+ edgesAdded: z8.number().int(),
7128
+ edgesRemoved: z8.number().int()
7129
+ }),
7130
+ nodes: z8.object({
7131
+ added: z8.array(NodeSchema2),
7132
+ removed: z8.array(NodeSchema2),
7133
+ changed: z8.array(NodeChangeSchema),
7134
+ unchanged: z8.number().int()
7135
+ }),
7136
+ edges: z8.object({
7137
+ added: z8.array(EdgeSchema2),
7138
+ removed: z8.array(EdgeSchema2),
7139
+ unchanged: z8.number().int()
7140
+ }),
7141
+ anomalies: z8.object({ added: z8.array(AnomalySchema) })
7142
+ });
7143
+ var SessionSchema = z8.object({
7144
+ id: z8.string(),
7145
+ mode: z8.literal("discover"),
7146
+ startedAt: z8.string(),
7147
+ completedAt: z8.string().optional(),
7148
+ name: z8.string().optional(),
7149
+ tenant: z8.string(),
7150
+ lastScannedAt: z8.string().optional()
7151
+ });
7152
+ var SessionsResponse = z8.object({ sessions: z8.array(SessionSchema) });
7153
+ var HealthResponse = z8.object({
7154
+ status: z8.literal("ok"),
7155
+ version: z8.string(),
7156
+ store: z8.literal("sqlite"),
7157
+ sessions: z8.number().int()
7158
+ });
7159
+ var ErrorResponse = z8.object({
7160
+ error: z8.string(),
7161
+ code: z8.string().optional()
7162
+ });
7163
+ var API_SCHEMAS = {
7164
+ Node: NodeSchema2,
7165
+ Edge: EdgeSchema2,
7166
+ Anomaly: AnomalySchema,
7167
+ Summary: SummaryResponse,
7168
+ Nodes: NodesResponse,
7169
+ Dependencies: DependenciesResponse,
7170
+ Diff: DiffResponse,
7171
+ Session: SessionSchema,
7172
+ Sessions: SessionsResponse,
7173
+ Health: HealthResponse,
7174
+ Error: ErrorResponse
7175
+ };
7176
+
7177
+ // src/api/rest.ts
7178
+ function toApiNode(n) {
7179
+ const out = { id: n.id, type: n.type, name: n.name, confidence: n.confidence, tags: n.tags };
7180
+ if (n.domain !== void 0) out["domain"] = n.domain;
7181
+ if (n.subDomain !== void 0) out["subDomain"] = n.subDomain;
7182
+ if (n.qualityScore !== void 0) out["qualityScore"] = n.qualityScore;
7183
+ if (n.owner !== void 0) out["owner"] = n.owner;
7184
+ if (n.cost !== void 0) out["cost"] = n.cost;
7185
+ return out;
7186
+ }
7187
+ function toApiEdge(e) {
7188
+ return { sourceId: e.sourceId, targetId: e.targetId, relationship: e.relationship, confidence: e.confidence, evidence: e.evidence };
7189
+ }
7190
+ function toApiSession(s) {
7191
+ const out = { id: s.id, mode: s.mode, startedAt: s.startedAt, tenant: s.tenant };
7192
+ if (s.completedAt !== void 0) out["completedAt"] = s.completedAt;
7193
+ if (s.name !== void 0) out["name"] = s.name;
7194
+ if (s.lastScannedAt !== void 0) out["lastScannedAt"] = s.lastScannedAt;
7195
+ return out;
7196
+ }
7197
+ function toApiAnomaly(a) {
7198
+ return { nodeId: a.nodeId, kind: a.kind, severity: a.severity, reason: a.reason };
7199
+ }
7200
+ function projectDependencies(r) {
7201
+ return {
7202
+ ...r.root ? { root: toApiNode(r.root) } : {},
7203
+ direction: r.direction,
7204
+ maxDepth: r.maxDepth,
7205
+ nodes: r.nodes.map((n) => ({ ...toApiNode(n), depth: n.depth })),
7206
+ edges: r.edges.map(toApiEdge)
7207
+ };
7208
+ }
7209
+ function projectDiff(diff) {
7210
+ return {
7211
+ base: { sessionId: diff.base.sessionId, startedAt: diff.base.startedAt, nodeCount: diff.base.nodeCount, edgeCount: diff.base.edgeCount },
7212
+ current: { sessionId: diff.current.sessionId, startedAt: diff.current.startedAt, nodeCount: diff.current.nodeCount, edgeCount: diff.current.edgeCount },
7213
+ summary: diff.summary,
7214
+ nodes: {
7215
+ added: diff.nodes.added.map(toApiNode),
7216
+ removed: diff.nodes.removed.map(toApiNode),
7217
+ changed: diff.nodes.changed.map((c) => ({ id: c.id, changedFields: c.changedFields, confidenceDelta: c.confidenceDelta })),
7218
+ unchanged: diff.nodes.unchanged
7219
+ },
7220
+ edges: {
7221
+ added: diff.edges.added.map(toApiEdge),
7222
+ removed: diff.edges.removed.map(toApiEdge),
7223
+ unchanged: diff.edges.unchanged
7224
+ },
7225
+ anomalies: { added: diff.anomalies.added.map(toApiAnomaly) }
7226
+ };
7227
+ }
7228
+ function ok(body) {
7229
+ return { status: 200, body };
7230
+ }
7231
+ function badRequest(error) {
7232
+ return { status: 400, body: { error } };
7233
+ }
7234
+ function notFound(error = "not found") {
7235
+ return { status: 404, body: { error } };
7236
+ }
7237
+ function guard(fn) {
7238
+ try {
7239
+ return fn();
7240
+ } catch (err) {
7241
+ if (err instanceof NotFoundError) return notFound(err.message);
7242
+ throw err;
7243
+ }
7244
+ }
7245
+ function validateOut(schema, body) {
7246
+ if (process.env["NODE_ENV"] !== "production") {
7247
+ const r = schema.safeParse(body);
7248
+ if (!r.success) throw new Error(`API response failed its own schema contract: ${r.error.message}`);
7249
+ }
7250
+ return body;
7251
+ }
7252
+ function intParam(url, name) {
7253
+ const raw = url.searchParams.get(name);
7254
+ if (raw === null || raw.trim() === "") return void 0;
7255
+ const n = Number(raw);
7256
+ return Number.isInteger(n) ? n : void 0;
7257
+ }
7258
+ function sessionParam(url) {
7259
+ return url.searchParams.get("session") ?? void 0;
7260
+ }
7261
+ function handleSummary(ctx, url, d) {
7262
+ return guard(() => ok(validateOut(SummaryResponse, d.backend.summary(ctx, sessionParam(url)))));
7263
+ }
7264
+ function handleNodes(ctx, url, d) {
7265
+ return guard(() => {
7266
+ const search = url.searchParams.get("search") ?? void 0;
7267
+ const typesRaw = url.searchParams.get("types");
7268
+ const types = typesRaw ? typesRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
7269
+ const limit = intParam(url, "limit");
7270
+ const offset = intParam(url, "offset");
7271
+ const r = d.backend.nodes(
7272
+ ctx,
7273
+ { ...search ? { search } : {}, ...types ? { types } : {}, ...limit !== void 0 ? { limit } : {}, ...offset !== void 0 ? { offset } : {} },
7274
+ sessionParam(url)
7275
+ );
7276
+ return ok(validateOut(NodesResponse, { nodes: r.nodes.map(toApiNode), total: r.total, limit: r.limit, offset: r.offset }));
7277
+ });
7278
+ }
7279
+ function handleDependencies(ctx, id, url, d) {
7280
+ const directionRaw = url.searchParams.get("direction");
7281
+ if (directionRaw !== null && !DIRECTIONS.includes(directionRaw)) {
7282
+ return badRequest(`direction must be one of ${DIRECTIONS.join(", ")}`);
7283
+ }
7284
+ return guard(() => {
7285
+ const direction = directionRaw ?? void 0;
7286
+ const maxDepth = intParam(url, "maxDepth");
7287
+ const r = d.backend.dependencies(
7288
+ ctx,
7289
+ id,
7290
+ { ...direction ? { direction } : {}, ...maxDepth !== void 0 ? { maxDepth } : {} },
7291
+ sessionParam(url)
7292
+ );
7293
+ return ok(validateOut(DependenciesResponse, projectDependencies(r)));
7294
+ });
7295
+ }
7296
+ function handleDiff(ctx, url, d) {
7297
+ const base = url.searchParams.get("base");
7298
+ const current = url.searchParams.get("current");
7299
+ if (!base || !current) return badRequest("both `base` and `current` query params are required");
7300
+ return guard(() => {
7301
+ const diff = d.backend.diff(ctx, base, current);
7302
+ return ok(validateOut(DiffResponse, projectDiff(diff)));
7303
+ });
7304
+ }
7305
+ function handleSessions(ctx, d) {
7306
+ return guard(() => ok(validateOut(SessionsResponse, { sessions: d.backend.sessions(ctx).map(toApiSession) })));
7307
+ }
7308
+ function handleHealth(ctx, d) {
7309
+ const h = d.backend.health(ctx);
7310
+ return ok(validateOut(HealthResponse, { status: "ok", version: d.version, store: h.store, sessions: h.sessions }));
7311
+ }
7312
+
7313
+ // src/api/openapi.ts
7314
+ function defOf(schema) {
7315
+ return schema.def ?? {};
7316
+ }
7317
+ function unwrapOptional(schema) {
7318
+ const def = defOf(schema);
7319
+ if ((def.type === "optional" || def.type === "nullable") && def.innerType) {
7320
+ return { inner: def.innerType, optional: true };
7321
+ }
7322
+ return { inner: schema, optional: false };
7323
+ }
7324
+ function zodToJsonSchema(schema) {
7325
+ const def = defOf(schema);
7326
+ switch (def.type) {
7327
+ case "string":
7328
+ return { type: "string" };
7329
+ case "number": {
7330
+ const isInt = (def.checks ?? []).some((c) => c._zod?.def?.check === "number_format");
7331
+ return { type: isInt ? "integer" : "number" };
7332
+ }
7333
+ case "boolean":
7334
+ return { type: "boolean" };
7335
+ case "literal": {
7336
+ const values = def.values ?? [];
7337
+ return values.length === 1 ? { const: values[0] } : { enum: values };
7338
+ }
7339
+ case "enum":
7340
+ return { type: "string", enum: Object.values(def.entries ?? {}) };
7341
+ case "array":
7342
+ return { type: "array", items: def.element ? zodToJsonSchema(def.element) : {} };
7343
+ case "record":
7344
+ return { type: "object", additionalProperties: def.valueType ? zodToJsonSchema(def.valueType) : true };
7345
+ case "optional":
7346
+ case "nullable":
7347
+ return def.innerType ? zodToJsonSchema(def.innerType) : {};
7348
+ case "object": {
7349
+ const shape = def.shape ?? {};
7350
+ const properties = {};
7351
+ const required = [];
7352
+ for (const key of Object.keys(shape)) {
7353
+ const { inner, optional } = unwrapOptional(shape[key]);
7354
+ properties[key] = zodToJsonSchema(inner);
7355
+ if (!optional) required.push(key);
7356
+ }
7357
+ return { type: "object", properties, required, additionalProperties: false };
7358
+ }
7359
+ default:
7360
+ throw new Error(`zodToJsonSchema: unsupported zod construct "${def.type ?? "unknown"}". Extend src/api/openapi.ts.`);
7361
+ }
7362
+ }
7363
+ var TENANT_PARAM = {
7364
+ name: "tenant",
7365
+ in: "query",
7366
+ required: false,
7367
+ description: 'Tenant/org scope (also accepted via the X-Cartograph-Tenant header). Defaults to "local".',
7368
+ schema: { type: "string" }
7369
+ };
7370
+ var SESSION_PARAM = {
7371
+ name: "session",
7372
+ in: "query",
7373
+ required: false,
7374
+ description: "Session id to query, or omit for the latest discovery session.",
7375
+ schema: { type: "string" }
7376
+ };
7377
+ function errorResponses() {
7378
+ const err = { description: "Error", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } };
7379
+ return { "400": { ...err, description: "Bad request" }, "401": { ...err, description: "Unauthorized" }, "404": { ...err, description: "Not found" } };
7380
+ }
7381
+ function ok2(ref, description) {
7382
+ return { description, content: { "application/json": { schema: { $ref: `#/components/schemas/${ref}` } } } };
7383
+ }
7384
+ function buildOpenApiDocument(opts) {
7385
+ const schemas = {};
7386
+ for (const [name, schema] of Object.entries(API_SCHEMAS)) {
7387
+ schemas[name] = zodToJsonSchema(schema);
7388
+ }
7389
+ return {
7390
+ openapi: "3.1.0",
7391
+ info: {
7392
+ title: "Cartograph API",
7393
+ version: opts.version,
7394
+ description: "Read-only REST API over the discovered infrastructure/agentic-AI topology. Every endpoint is tenant-scoped and bearer-authenticated."
7395
+ },
7396
+ servers: [{ url: "/" }],
7397
+ security: [{ bearerAuth: [] }],
7398
+ components: {
7399
+ securitySchemes: { bearerAuth: { type: "http", scheme: "bearer" } },
7400
+ schemas
7401
+ },
7402
+ paths: {
7403
+ "/v1/health": {
7404
+ get: {
7405
+ summary: "Liveness + store/coverage probe",
7406
+ security: [],
7407
+ responses: { "200": ok2("Health", "Service health") }
7408
+ }
7409
+ },
7410
+ "/v1/openapi.json": {
7411
+ get: {
7412
+ summary: "This OpenAPI document",
7413
+ security: [],
7414
+ responses: { "200": { description: "OpenAPI 3.1 document", content: { "application/json": { schema: { type: "object" } } } } }
7415
+ }
7416
+ },
7417
+ "/v1/summary": {
7418
+ get: {
7419
+ summary: "Low-token topology aggregate for the resolved session",
7420
+ parameters: [SESSION_PARAM, TENANT_PARAM],
7421
+ responses: { "200": ok2("Summary", "Topology summary"), ...errorResponses() }
7422
+ }
7423
+ },
7424
+ "/v1/nodes": {
7425
+ get: {
7426
+ summary: "List/search/paginate nodes",
7427
+ parameters: [
7428
+ { name: "search", in: "query", required: false, description: "Lexical/semantic search anchor.", schema: { type: "string" } },
7429
+ { name: "types", in: "query", required: false, description: "Comma-separated node-type filter.", schema: { type: "string" } },
7430
+ { name: "limit", in: "query", required: false, description: "Page size (default 100, max 1000).", schema: { type: "integer" } },
7431
+ { name: "offset", in: "query", required: false, description: "Page offset (ignored for search).", schema: { type: "integer" } },
7432
+ SESSION_PARAM,
7433
+ TENANT_PARAM
7434
+ ],
7435
+ responses: { "200": ok2("Nodes", "A page of nodes"), ...errorResponses() }
7436
+ }
7437
+ },
7438
+ "/v1/nodes/{id}/dependencies": {
7439
+ get: {
7440
+ summary: "Dependency traversal from a node",
7441
+ parameters: [
7442
+ { name: "id", in: "path", required: true, description: 'Node id ("{type}:{id}").', schema: { type: "string" } },
7443
+ { name: "direction", in: "query", required: false, description: "downstream | upstream | both (default downstream).", schema: { type: "string", enum: ["downstream", "upstream", "both"] } },
7444
+ { name: "maxDepth", in: "query", required: false, description: "Traversal depth (default 8, max 64).", schema: { type: "integer" } },
7445
+ SESSION_PARAM,
7446
+ TENANT_PARAM
7447
+ ],
7448
+ responses: { "200": ok2("Dependencies", "Traversal result"), ...errorResponses() }
7449
+ }
7450
+ },
7451
+ "/v1/diff": {
7452
+ get: {
7453
+ summary: "Compare two sessions (drift)",
7454
+ parameters: [
7455
+ { name: "base", in: "query", required: true, description: "Base session id.", schema: { type: "string" } },
7456
+ { name: "current", in: "query", required: true, description: "Current session id.", schema: { type: "string" } },
7457
+ TENANT_PARAM
7458
+ ],
7459
+ responses: { "200": ok2("Diff", "Topology delta"), ...errorResponses() }
7460
+ }
7461
+ },
7462
+ "/v1/sessions": {
7463
+ get: {
7464
+ summary: "List discovery sessions for the tenant",
7465
+ parameters: [TENANT_PARAM],
7466
+ responses: { "200": ok2("Sessions", "Sessions"), ...errorResponses() }
7467
+ }
7468
+ }
7469
+ }
7470
+ };
7471
+ }
7472
+
7473
+ // src/api/graphql.ts
7474
+ var SDL = `# Cartograph read-only GraphQL API (4.2). Mirrors the REST surface.
7475
+ schema { query: Query }
7476
+
7477
+ type Query {
7478
+ summary(session: String): Summary
7479
+ nodes(search: String, types: [String!], limit: Int, offset: Int, session: String): NodeConnection
7480
+ node(id: String!, session: String): Node
7481
+ dependencies(id: String!, direction: Direction, maxDepth: Int, session: String): Dependencies
7482
+ diff(base: String!, current: String!): Diff
7483
+ sessions: [Session!]!
7484
+ }
7485
+
7486
+ enum Direction { downstream upstream both }
7487
+
7488
+ type Totals { nodes: Int! edges: Int! }
7489
+ type Count { key: String! value: Int! }
7490
+ type TopConnected { id: String! name: String! type: String! degree: Int! }
7491
+ type Anomaly { nodeId: String! kind: String! severity: String! reason: String! }
7492
+ type Cost { amount: Float! currency: String! period: String! source: String }
7493
+ type CostRollup { key: String! currency: String! period: String! total: Float! nodes: Int! }
7494
+ type CostCoverage { withCost: Int! total: Int! }
7495
+
7496
+ type Node {
7497
+ id: String! type: String! name: String! confidence: Float!
7498
+ domain: String subDomain: String qualityScore: Float owner: String cost: Cost tags: [String!]!
7499
+ }
7500
+ type DependencyNode {
7501
+ id: String! type: String! name: String! confidence: Float!
7502
+ domain: String subDomain: String qualityScore: Float owner: String cost: Cost tags: [String!]! depth: Int!
7503
+ }
7504
+ type Edge { sourceId: String! targetId: String! relationship: String! confidence: Float! evidence: String! }
7505
+
7506
+ type Summary {
7507
+ sessionId: String!
7508
+ totals: Totals!
7509
+ topConnected: [TopConnected!]!
7510
+ anomalies: [Anomaly!]!
7511
+ contributors: Int!
7512
+ costByDomain: [CostRollup!]!
7513
+ costByOwner: [CostRollup!]!
7514
+ costCoverage: CostCoverage!
7515
+ }
7516
+
7517
+ type NodeConnection { nodes: [Node!]! total: Int! limit: Int! offset: Int! }
7518
+ type Dependencies { root: Node direction: Direction! maxDepth: Int! nodes: [DependencyNode!]! edges: [Edge!]! }
7519
+
7520
+ type SessionEndpoint { sessionId: String! startedAt: String! nodeCount: Int! edgeCount: Int! }
7521
+ type DiffSummary { nodesAdded: Int! nodesRemoved: Int! nodesChanged: Int! edgesAdded: Int! edgesRemoved: Int! }
7522
+ type NodeChange { id: String! changedFields: [String!]! confidenceDelta: Float! }
7523
+ type DiffNodes { added: [Node!]! removed: [Node!]! changed: [NodeChange!]! unchanged: Int! }
7524
+ type DiffEdges { added: [Edge!]! removed: [Edge!]! unchanged: Int! }
7525
+ type DiffAnomalies { added: [Anomaly!]! }
7526
+ type Diff {
7527
+ base: SessionEndpoint! current: SessionEndpoint! summary: DiffSummary!
7528
+ nodes: DiffNodes! edges: DiffEdges! anomalies: DiffAnomalies!
7529
+ }
7530
+
7531
+ type Session { id: String! mode: String! startedAt: String! completedAt: String name: String tenant: String! lastScannedAt: String }
7532
+ `;
7533
+ var resolvers = {
7534
+ summary: (ctx, args, backend) => backend.summary(ctx, str(args["session"])),
7535
+ nodes: (ctx, args, backend) => {
7536
+ const r = backend.nodes(
7537
+ ctx,
7538
+ {
7539
+ ...str(args["search"]) ? { search: str(args["search"]) } : {},
7540
+ ...Array.isArray(args["types"]) ? { types: args["types"].map(String) } : {},
7541
+ ...num(args["limit"]) !== void 0 ? { limit: num(args["limit"]) } : {},
7542
+ ...num(args["offset"]) !== void 0 ? { offset: num(args["offset"]) } : {}
7543
+ },
7544
+ str(args["session"])
7545
+ );
7546
+ return { nodes: r.nodes.map(toApiNode), total: r.total, limit: r.limit, offset: r.offset };
7547
+ },
7548
+ node: (ctx, args, backend) => {
7549
+ const n = backend.node(ctx, String(args["id"]), str(args["session"]));
7550
+ return n ? toApiNode(n) : null;
7551
+ },
7552
+ dependencies: (ctx, args, backend) => {
7553
+ const r = backend.dependencies(
7554
+ ctx,
7555
+ String(args["id"]),
7556
+ {
7557
+ ...str(args["direction"]) ? { direction: str(args["direction"]) } : {},
7558
+ ...num(args["maxDepth"]) !== void 0 ? { maxDepth: num(args["maxDepth"]) } : {}
7559
+ },
7560
+ str(args["session"])
7561
+ );
7562
+ return projectDependencies(r);
7563
+ },
7564
+ diff: (ctx, args, backend) => projectDiff(backend.diff(ctx, String(args["base"]), String(args["current"]))),
7565
+ sessions: (ctx, _args, backend) => backend.sessions(ctx).map(toApiSession)
7566
+ };
7567
+ function str(v) {
7568
+ return typeof v === "string" ? v : void 0;
7569
+ }
7570
+ function num(v) {
7571
+ return typeof v === "number" && Number.isInteger(v) ? v : void 0;
7572
+ }
7573
+ var NAME_RE = /[_A-Za-z][_0-9A-Za-z]*/y;
7574
+ function tokenize2(src) {
7575
+ const tokens = [];
7576
+ let i = 0;
7577
+ while (i < src.length) {
7578
+ const c = src[i];
7579
+ if (/\s|,/.test(c)) {
7580
+ i++;
7581
+ continue;
7582
+ }
7583
+ if (c === "#") {
7584
+ while (i < src.length && src[i] !== "\n") i++;
7585
+ continue;
7586
+ }
7587
+ if ("{}()[]:!$".includes(c)) {
7588
+ tokens.push(c);
7589
+ i++;
7590
+ continue;
7591
+ }
7592
+ if (c === '"') {
7593
+ let j = i + 1;
7594
+ let s = "";
7595
+ while (j < src.length && src[j] !== '"') {
7596
+ s += src[j];
7597
+ j++;
7598
+ }
7599
+ tokens.push(JSON.stringify(s));
7600
+ i = j + 1;
7601
+ continue;
7602
+ }
7603
+ NAME_RE.lastIndex = i;
7604
+ const m = NAME_RE.exec(src);
7605
+ if (m && m.index === i) {
7606
+ tokens.push(m[0]);
7607
+ i = NAME_RE.lastIndex;
7608
+ continue;
7609
+ }
7610
+ const numMatch = /-?\d+(\.\d+)?/y;
7611
+ numMatch.lastIndex = i;
7612
+ const nm = numMatch.exec(src);
7613
+ if (nm && nm.index === i) {
7614
+ tokens.push(nm[0]);
7615
+ i = numMatch.lastIndex;
7616
+ continue;
7617
+ }
7618
+ throw new Error(`unexpected character '${c}'`);
7619
+ }
7620
+ return tokens;
7621
+ }
7622
+ var MAX_SELECTION_DEPTH = 32;
7623
+ var Parser = class {
7624
+ constructor(tokens, variables) {
7625
+ this.tokens = tokens;
7626
+ this.variables = variables;
7627
+ }
7628
+ pos = 0;
7629
+ depth = 0;
7630
+ peek() {
7631
+ return this.tokens[this.pos];
7632
+ }
7633
+ next() {
7634
+ return this.tokens[this.pos++];
7635
+ }
7636
+ expect(tok) {
7637
+ if (this.tokens[this.pos] !== tok) throw new Error(`expected '${tok}', got '${this.tokens[this.pos] ?? "<eof>"}'`);
7638
+ this.pos++;
7639
+ }
7640
+ parseDocument() {
7641
+ if (this.peek() === "mutation" || this.peek() === "subscription") {
7642
+ throw new Error("only query operations are supported (read-only API)");
7643
+ }
7644
+ if (this.peek() === "query") {
7645
+ this.next();
7646
+ if (this.peek() && this.peek() !== "{" && this.peek() !== "(") this.next();
7647
+ if (this.peek() === "(") this.skipBalanced("(", ")");
7648
+ }
7649
+ this.expect("{");
7650
+ const selections = this.parseSelectionSet();
7651
+ return selections;
7652
+ }
7653
+ skipBalanced(open, close) {
7654
+ this.expect(open);
7655
+ let depth = 1;
7656
+ while (depth > 0) {
7657
+ const t = this.next();
7658
+ if (t === void 0) throw new Error("unbalanced");
7659
+ if (t === open) depth++;
7660
+ else if (t === close) depth--;
7661
+ }
7662
+ }
7663
+ parseSelectionSet() {
7664
+ if (++this.depth > MAX_SELECTION_DEPTH) throw new Error(`selection set nested deeper than ${MAX_SELECTION_DEPTH}`);
7665
+ const out = [];
7666
+ while (this.peek() !== "}") {
7667
+ if (this.peek() === void 0) throw new Error("unexpected end of selection set");
7668
+ out.push(this.parseSelection());
7669
+ }
7670
+ this.expect("}");
7671
+ this.depth--;
7672
+ return out;
7673
+ }
7674
+ parseSelection() {
7675
+ let name = this.next();
7676
+ const alias = name;
7677
+ if (this.peek() === ":") {
7678
+ this.next();
7679
+ name = this.next();
7680
+ }
7681
+ const args = {};
7682
+ if (this.peek() === "(") {
7683
+ this.next();
7684
+ while (this.peek() !== ")") {
7685
+ const argName = this.next();
7686
+ this.expect(":");
7687
+ args[argName] = this.parseValue();
7688
+ }
7689
+ this.expect(")");
7690
+ }
7691
+ let selections = [];
7692
+ if (this.peek() === "{") {
7693
+ this.next();
7694
+ selections = this.parseSelectionSet();
7695
+ }
7696
+ return { name, alias, args, selections };
7697
+ }
7698
+ parseValue() {
7699
+ const t = this.next();
7700
+ if (t === "$") {
7701
+ const v = this.next();
7702
+ return this.variables[v];
7703
+ }
7704
+ if (t === "[") {
7705
+ const arr = [];
7706
+ while (this.peek() !== "]") arr.push(this.parseValue());
7707
+ this.expect("]");
7708
+ return arr;
7709
+ }
7710
+ if (t.startsWith('"')) return JSON.parse(t);
7711
+ if (t === "true") return true;
7712
+ if (t === "false") return false;
7713
+ if (t === "null") return null;
7714
+ if (/^-?\d+(\.\d+)?$/.test(t)) return Number(t);
7715
+ return t;
7716
+ }
7717
+ };
7718
+ function project(value, selections) {
7719
+ if (value === null || value === void 0) return null;
7720
+ if (selections.length === 0) return value;
7721
+ if (Array.isArray(value)) return value.map((v) => project(v, selections));
7722
+ if (typeof value !== "object") return value;
7723
+ const obj = value;
7724
+ const out = {};
7725
+ for (const sel of selections) {
7726
+ if (sel.name === "__typename") {
7727
+ out[sel.alias] = void 0;
7728
+ continue;
7729
+ }
7730
+ out[sel.alias] = project(obj[sel.name], sel.selections);
7731
+ }
7732
+ return out;
7733
+ }
7734
+ function introspectionSchema() {
7735
+ const names = [...SDL.matchAll(/^(?:type|enum)\s+([_A-Za-z][_0-9A-Za-z]*)/gm)].map((m) => m[1]);
7736
+ const types = names.map((name) => ({ name, kind: /^[A-Z]/.test(name) ? "OBJECT" : "SCALAR" }));
7737
+ return {
7738
+ __schema: {
7739
+ queryType: { name: "Query" },
7740
+ mutationType: null,
7741
+ subscriptionType: null,
7742
+ types,
7743
+ directives: []
7744
+ }
7745
+ };
7746
+ }
7747
+ async function executeGraphql(ctx, body, deps) {
7748
+ const req = body ?? {};
7749
+ if (typeof req.query !== "string" || req.query.trim() === "") {
7750
+ return { errors: [{ message: "missing query" }] };
7751
+ }
7752
+ const variables = typeof req.variables === "object" && req.variables !== null ? req.variables : {};
7753
+ let selections;
7754
+ try {
7755
+ selections = new Parser(tokenize2(req.query), variables).parseDocument();
7756
+ } catch (err) {
7757
+ return { errors: [{ message: `syntax error: ${err instanceof Error ? err.message : String(err)}` }] };
7758
+ }
7759
+ const data = {};
7760
+ const errors = [];
7761
+ for (const sel of selections) {
7762
+ try {
7763
+ if (sel.name === "__schema") {
7764
+ data[sel.alias] = project(introspectionSchema()["__schema"], sel.selections);
7765
+ continue;
7766
+ }
7767
+ if (sel.name === "__typename") {
7768
+ data[sel.alias] = "Query";
7769
+ continue;
7770
+ }
7771
+ const resolver = resolvers[sel.name];
7772
+ if (!resolver) {
7773
+ errors.push({ message: `Cannot query field "${sel.name}" on type "Query"` });
7774
+ continue;
7775
+ }
7776
+ const resolved = resolver(ctx, sel.args, deps.backend);
7777
+ data[sel.alias] = project(resolved, sel.selections);
7778
+ } catch (err) {
7779
+ errors.push({ message: err instanceof Error ? err.message : String(err) });
7780
+ }
7781
+ }
7782
+ return errors.length > 0 ? { data, errors } : { data };
7783
+ }
7784
+ function handleGraphqlGet() {
7785
+ return { status: 200, body: SDL };
7786
+ }
7787
+
7788
+ // src/api/server.ts
7789
+ var DEPENDENCIES_RE = /^\/v1\/nodes\/(.+)\/dependencies$/;
7790
+ var MAX_GRAPHQL_BYTES = 1024 * 1024;
7791
+ function send(res, status, body, headers = {}) {
7792
+ res.writeHead(status, { "content-type": "application/json", ...headers }).end(JSON.stringify(body));
7793
+ }
7794
+ async function readBody(req, cap) {
7795
+ const chunks = [];
7796
+ let total = 0;
7797
+ let overflow = false;
7798
+ for await (const chunk of req) {
7799
+ if (overflow) continue;
7800
+ const buf = chunk;
7801
+ total += buf.length;
7802
+ if (total > cap) {
7803
+ overflow = true;
7804
+ chunks.length = 0;
7805
+ continue;
7806
+ }
7807
+ chunks.push(buf);
7808
+ }
7809
+ if (overflow) return { overflow: true, value: void 0 };
7810
+ if (chunks.length === 0) return { overflow: false, value: void 0 };
7811
+ try {
7812
+ return { overflow: false, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) };
7813
+ } catch {
7814
+ return { overflow: false, value: void 0 };
7815
+ }
7816
+ }
7817
+ async function runApi(opts) {
7818
+ const host2 = opts.host ?? "127.0.0.1";
7819
+ const requestedPort = opts.port ?? 3737;
7820
+ const token = opts.token;
7821
+ const graphqlEnabled = opts.graphql !== false;
7822
+ const defaultTenant = opts.tenant?.defaultTenant ?? DEFAULT_TENANT;
7823
+ const log2 = opts.log ?? (() => {
7824
+ });
7825
+ const restDeps = { backend: opts.backend, version: opts.version };
7826
+ const openApiDoc = buildOpenApiDocument({ version: opts.version });
7827
+ const allowedOrigins = opts.allowedOrigins ?? [];
7828
+ assertSafeBind({ host: host2, port: requestedPort, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...token ? { token } : {} });
7829
+ let allowedHosts = opts.allowedHosts ?? [];
7830
+ const corsHeaders = (req) => {
7831
+ const origin = req.headers["origin"];
7832
+ if (typeof origin === "string" && allowedOrigins.includes(origin)) {
7833
+ return {
7834
+ "access-control-allow-origin": origin,
7835
+ "vary": "Origin",
7836
+ "access-control-allow-methods": "GET, POST, OPTIONS",
7837
+ "access-control-allow-headers": "authorization, content-type, x-cartograph-tenant"
7838
+ };
7839
+ }
7840
+ return {};
7841
+ };
7842
+ const server = http2.createServer((req, res) => {
7843
+ const started = Date.now();
7844
+ let tenantLabel = "-";
7845
+ const finish = (status) => {
7846
+ log2(`[cartography-api] ${req.method ?? "-"} ${req.url ?? "-"} ${status} ${Date.now() - started}ms tenant=${tenantLabel}`);
7847
+ };
7848
+ void (async () => {
7849
+ try {
7850
+ const url = new URL(req.url ?? "/", `http://${req.headers["host"] ?? host2}`);
7851
+ const path = url.pathname;
7852
+ const cors = corsHeaders(req);
7853
+ if (req.method === "OPTIONS") {
7854
+ res.writeHead(204, cors).end();
7855
+ finish(204);
7856
+ return;
7857
+ }
7858
+ const hostHeader = (req.headers["host"] ?? "").toLowerCase();
7859
+ if (!allowedHosts.some((h) => h.toLowerCase() === hostHeader)) {
7860
+ send(res, 403, { error: "host not allowed" }, cors);
7861
+ finish(403);
7862
+ return;
7863
+ }
7864
+ if (path === "/v1/openapi.json" && req.method === "GET") {
7865
+ send(res, 200, openApiDoc, cors);
7866
+ finish(200);
7867
+ return;
7868
+ }
7869
+ if (path === "/v1/health") {
7870
+ if (req.method !== "GET") {
7871
+ send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
7872
+ finish(405);
7873
+ return;
7874
+ }
7875
+ tenantLabel = defaultTenant;
7876
+ const r = handleHealth({ tenant: defaultTenant }, restDeps);
7877
+ send(res, r.status, r.body, cors);
7878
+ finish(r.status);
7879
+ return;
7880
+ }
7881
+ if (!checkBearer(req.headers["authorization"], token)) {
7882
+ send(res, 401, { error: "unauthorized" }, { "www-authenticate": "Bearer", ...cors });
7883
+ finish(401);
7884
+ return;
7885
+ }
7886
+ let ctx;
7887
+ try {
7888
+ ctx = resolveTenant(req, url, opts.tenant ?? {});
7889
+ tenantLabel = ctx.tenant;
7890
+ } catch (err) {
7891
+ if (err instanceof InvalidTenantError) {
7892
+ send(res, 400, { error: "invalid tenant" }, cors);
7893
+ finish(400);
7894
+ return;
7895
+ }
7896
+ throw err;
7897
+ }
7898
+ if (graphqlEnabled && path === "/graphql") {
7899
+ if (req.method === "GET") {
7900
+ const g = handleGraphqlGet();
7901
+ res.writeHead(g.status, { "content-type": "text/plain; charset=utf-8", ...cors }).end(g.body);
7902
+ finish(g.status);
7903
+ return;
7904
+ }
7905
+ if (req.method === "POST") {
7906
+ const { overflow, value } = await readBody(req, MAX_GRAPHQL_BYTES);
7907
+ if (overflow) {
7908
+ send(res, 413, { error: "payload too large" }, cors);
7909
+ finish(413);
7910
+ return;
7911
+ }
7912
+ const result = await executeGraphql(ctx, value, { backend: opts.backend });
7913
+ send(res, 200, result, cors);
7914
+ finish(200);
7915
+ return;
7916
+ }
7917
+ send(res, 405, { error: "method not allowed" }, { allow: "GET, POST", ...cors });
7918
+ finish(405);
7919
+ return;
7920
+ }
7921
+ if (path.startsWith("/v1/")) {
7922
+ if (req.method !== "GET") {
7923
+ send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
7924
+ finish(405);
7925
+ return;
7926
+ }
7927
+ const result = dispatchRest(ctx, path, url, restDeps);
7928
+ if (result) {
7929
+ send(res, result.status, result.body, cors);
7930
+ finish(result.status);
7931
+ return;
7932
+ }
7933
+ }
7934
+ send(res, 404, { error: "not found" }, cors);
7935
+ finish(404);
7936
+ } catch (err) {
7937
+ process.stderr.write(`[cartography-api] request failed: ${err instanceof Error ? err.message : String(err)}
7938
+ `);
7939
+ if (!res.headersSent) send(res, 500, { error: "internal error" });
7940
+ finish(500);
7941
+ }
7942
+ })();
7943
+ });
7944
+ await new Promise((resolve3) => server.listen(requestedPort, host2, resolve3));
7945
+ const actualPort = server.address().port;
7946
+ if (allowedHosts.length === 0) allowedHosts = defaultAllowedHosts(host2, actualPort);
7947
+ return server;
7948
+ }
7949
+ function dispatchRest(ctx, path, url, deps) {
7950
+ switch (path) {
7951
+ case "/v1/summary":
7952
+ return handleSummary(ctx, url, deps);
7953
+ case "/v1/nodes":
7954
+ return handleNodes(ctx, url, deps);
7955
+ case "/v1/diff":
7956
+ return handleDiff(ctx, url, deps);
7957
+ case "/v1/sessions":
7958
+ return handleSessions(ctx, deps);
7959
+ default: {
7960
+ const m = DEPENDENCIES_RE.exec(path);
7961
+ if (m) return handleDependencies(ctx, decodeURIComponent(m[1]), url, deps);
7962
+ return void 0;
7963
+ }
7964
+ }
7965
+ }
7966
+
7967
+ // src/api/start.ts
7968
+ import { readFileSync as readFileSync4 } from "fs";
7969
+ import { dirname as dirname3, resolve } from "path";
7970
+ import { fileURLToPath } from "url";
7971
+ function readVersion() {
7972
+ try {
7973
+ const dir = import.meta.dirname ?? dirname3(fileURLToPath(import.meta.url));
7974
+ return JSON.parse(readFileSync4(resolve(dir, "..", "package.json"), "utf-8")).version ?? "0.0.0";
7975
+ } catch {
7976
+ return "0.0.0";
7977
+ }
7978
+ }
7979
+ function parseApiArgs(argv) {
7980
+ const opts = {};
7981
+ for (let i = 0; i < argv.length; i++) {
7982
+ const a = argv[i];
7983
+ if (a === "--http") continue;
7984
+ else if (a === "--no-graphql") opts.graphql = false;
7985
+ else if (a === "--port") opts.port = Number(argv[++i]);
7986
+ else if (a === "--host") opts.host = argv[++i];
7987
+ else if (a === "--allowed-hosts") opts.allowedHosts = splitList(argv[++i]);
7988
+ else if (a === "--allowed-origins") opts.allowedOrigins = splitList(argv[++i]);
7989
+ else if (a === "--token") opts.token = argv[++i];
7990
+ else if (a === "--db") opts.dbPath = argv[++i];
7991
+ else if (a === "--session") opts.session = argv[++i];
7992
+ else if (a === "--tenant" || a === "--org") opts.tenant = argv[++i];
7993
+ else if (a === "--help" || a === "-h") opts.help = true;
7994
+ }
7995
+ return opts;
7996
+ }
7997
+ function splitList(raw) {
7998
+ return (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
7999
+ }
8000
+ async function startApi(opts = {}) {
8001
+ const log2 = opts.log ?? ((m) => process.stderr.write(m + "\n"));
8002
+ const db = new CartographyDB(opts.dbPath ?? defaultConfig().dbPath);
8003
+ const backend = createSqliteQueryBackend(db, opts.session ?? "latest");
8004
+ const token = opts.token ?? process.env["CARTOGRAPHY_HTTP_TOKEN"];
8005
+ const host2 = opts.host ?? "127.0.0.1";
8006
+ const port = opts.port ?? 3737;
8007
+ const version = readVersion();
8008
+ const server = await runApi({
8009
+ host: host2,
8010
+ port,
8011
+ backend,
8012
+ version,
8013
+ ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {},
8014
+ ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
8015
+ ...token ? { token } : {},
8016
+ ...opts.graphql === false ? { graphql: false } : {},
8017
+ ...opts.tenant ? { tenant: { defaultTenant: normalizeTenant(opts.tenant) } } : {},
8018
+ log: log2
8019
+ });
8020
+ const graphqlNote = opts.graphql === false ? " [REST only]" : " + /graphql";
8021
+ log2(
8022
+ `Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})`
8023
+ );
8024
+ return server;
8025
+ }
8026
+
6722
8027
  // src/installer/format.ts
6723
8028
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
6724
8029
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
@@ -6791,8 +8096,8 @@ function defaultServerEntry(opts = {}) {
6791
8096
  }
6792
8097
 
6793
8098
  // src/installer/install.ts
6794
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
6795
- import { dirname as dirname3 } from "path";
8099
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
8100
+ import { dirname as dirname4 } from "path";
6796
8101
  import { homedir as homedir3 } from "os";
6797
8102
  function currentOs() {
6798
8103
  if (process.platform === "win32") return "win";
@@ -6808,7 +8113,7 @@ function planInstall(spec, ctx, opts) {
6808
8113
  throw new Error(`${spec.label} does not support the "${ctx.scope}" scope.`);
6809
8114
  }
6810
8115
  const fileExists = existsSync4(path);
6811
- const before = fileExists ? readFileSync4(path, "utf8") : "";
8116
+ const before = fileExists ? readFileSync5(path, "utf8") : "";
6812
8117
  const existing = parseConfig(before, spec.format);
6813
8118
  const merged = spec.apply(existing, opts.serverName ?? DEFAULT_SERVER_NAME, opts.entry);
6814
8119
  const after = serializeConfig(merged, spec.format);
@@ -6825,7 +8130,7 @@ function planInstall(spec, ctx, opts) {
6825
8130
  };
6826
8131
  }
6827
8132
  function applyInstall(plan) {
6828
- mkdirSync4(dirname3(plan.path), { recursive: true });
8133
+ mkdirSync4(dirname4(plan.path), { recursive: true });
6829
8134
  writeFileSync3(plan.path, plan.after, "utf8");
6830
8135
  }
6831
8136
  function renderDiff(before, after) {
@@ -7199,13 +8504,13 @@ function createClaudeProvider() {
7199
8504
  }
7200
8505
 
7201
8506
  // src/providers/shell.ts
7202
- import { z as z8 } from "zod";
8507
+ import { z as z9 } from "zod";
7203
8508
  function createBashTool() {
7204
8509
  const shell = IS_WIN ? "powershell" : "posix";
7205
8510
  return {
7206
8511
  name: "Bash",
7207
8512
  description: "Run a read-only shell command (inspect ports, processes, config). Mutating or destructive commands are blocked by the read-only allowlist.",
7208
- inputShape: { command: z8.string().describe("The read-only shell command to run") },
8513
+ inputShape: { command: z9.string().describe("The read-only shell command to run") },
7209
8514
  annotations: { readOnlyHint: true, openWorldHint: true },
7210
8515
  handler: async (args) => {
7211
8516
  const command = String(args["command"] ?? "").trim();
@@ -7674,8 +8979,8 @@ Use ask_user when you need context from the user.`;
7674
8979
  }
7675
8980
 
7676
8981
  // src/cost.ts
7677
- import { readFileSync as readFileSync5 } from "fs";
7678
- import { resolve } from "path";
8982
+ import { readFileSync as readFileSync6 } from "fs";
8983
+ import { resolve as resolve2 } from "path";
7679
8984
  function splitCsvLine(line) {
7680
8985
  const out = [];
7681
8986
  let cur = "";
@@ -7753,7 +9058,7 @@ var CsvCostSource = class {
7753
9058
  }
7754
9059
  id;
7755
9060
  async fetch() {
7756
- const text = readFileSync5(resolve(this.opts.filePath), "utf-8");
9061
+ const text = readFileSync6(resolve2(this.opts.filePath), "utf-8");
7757
9062
  const records = parseCostCsv(text);
7758
9063
  const match = this.opts.match ?? "nodeId";
7759
9064
  const out = /* @__PURE__ */ new Map();
@@ -7784,11 +9089,11 @@ async function enrichCosts(db, sessionId, source) {
7784
9089
  let matched = 0;
7785
9090
  const unmatchedIds = [];
7786
9091
  for (const [nodeId, rec] of records) {
7787
- const ok = db.enrichNodeAttribution(sessionId, nodeId, {
9092
+ const ok3 = db.enrichNodeAttribution(sessionId, nodeId, {
7788
9093
  owner: rec.owner ?? void 0,
7789
9094
  cost: rec.cost ?? void 0
7790
9095
  });
7791
- if (ok) matched++;
9096
+ if (ok3) matched++;
7792
9097
  else unmatchedIds.push(nodeId);
7793
9098
  }
7794
9099
  return { source: source.id, total: records.size, matched, unmatched: unmatchedIds.length, unmatchedIds };
@@ -7895,10 +9200,10 @@ function assignColors(domains) {
7895
9200
  return result;
7896
9201
  }
7897
9202
  function shadeVariant(hex, amount) {
7898
- const num = parseInt(hex.replace("#", ""), 16);
7899
- const r = Math.min(255, (num >> 16) + amount);
7900
- const g = Math.min(255, (num >> 8 & 255) + amount);
7901
- const b = Math.min(255, (num & 255) + amount);
9203
+ const num2 = parseInt(hex.replace("#", ""), 16);
9204
+ const r = Math.min(255, (num2 >> 16) + amount);
9205
+ const g = Math.min(255, (num2 >> 8 & 255) + amount);
9206
+ const b = Math.min(255, (num2 & 255) + amount);
7902
9207
  return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
7903
9208
  }
7904
9209
  function groupByDomain(assets) {
@@ -9541,7 +10846,7 @@ function formatComplianceText(report) {
9541
10846
  }
9542
10847
 
9543
10848
  // src/config.ts
9544
- import { readFileSync as readFileSync6 } from "fs";
10849
+ import { readFileSync as readFileSync7 } from "fs";
9545
10850
  var ConfigError = class extends Error {
9546
10851
  constructor(message) {
9547
10852
  super(message);
@@ -9566,7 +10871,7 @@ function loadConfig(path) {
9566
10871
  function readConfigFile(path) {
9567
10872
  let raw;
9568
10873
  try {
9569
- raw = readFileSync6(path, "utf-8");
10874
+ raw = readFileSync7(path, "utf-8");
9570
10875
  } catch (err) {
9571
10876
  throw new ConfigError(
9572
10877
  `Cannot read config file ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -9827,7 +11132,7 @@ async function pushDeltas(config, items, opts = {}) {
9827
11132
  sentHashes.push(...batch.map((b) => b.contentHash));
9828
11133
  continue;
9829
11134
  }
9830
- let ok = false;
11135
+ let ok3 = false;
9831
11136
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
9832
11137
  const controller = new AbortController();
9833
11138
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -9846,7 +11151,7 @@ async function pushDeltas(config, items, opts = {}) {
9846
11151
  const elapsed = Date.now() - startedAt;
9847
11152
  if (res.ok) {
9848
11153
  log2(`pushed ${batch.length} item(s) \u2192 ${safeUrl} [${res.status}] ${elapsed}ms (attempt ${attempt + 1})`);
9849
- ok = true;
11154
+ ok3 = true;
9850
11155
  break;
9851
11156
  }
9852
11157
  if (res.status >= 400 && res.status < 500) {
@@ -9865,7 +11170,7 @@ async function pushDeltas(config, items, opts = {}) {
9865
11170
  await sleep(base + Math.floor(Math.random() * 100));
9866
11171
  }
9867
11172
  }
9868
- if (ok) {
11173
+ if (ok3) {
9869
11174
  sent += batch.length;
9870
11175
  sentHashes.push(...batch.map((b) => b.contentHash));
9871
11176
  } else {
@@ -9924,14 +11229,14 @@ function runSyncClassify(db, sessionId, config, opts = {}) {
9924
11229
 
9925
11230
  // src/preflight.ts
9926
11231
  import { execSync as execSync2 } from "child_process";
9927
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
11232
+ import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
9928
11233
  import { join as join6 } from "path";
9929
11234
  function isOAuthLoggedIn() {
9930
11235
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
9931
11236
  const credFile = join6(home, ".claude", ".credentials.json");
9932
11237
  if (!existsSync5(credFile)) return false;
9933
11238
  try {
9934
- const creds = JSON.parse(readFileSync7(credFile, "utf8"));
11239
+ const creds = JSON.parse(readFileSync8(credFile, "utf8"));
9935
11240
  const oauth = creds["claudeAiOauth"];
9936
11241
  return typeof oauth?.["accessToken"] === "string" && oauth["accessToken"].length > 0;
9937
11242
  } catch {
@@ -9997,37 +11302,51 @@ export {
9997
11302
  DriftConfigSchema,
9998
11303
  INGEST_SCHEMA_VERSION,
9999
11304
  IngestEnvelopeSchema,
11305
+ InvalidTenantError,
11306
+ JiraSink,
11307
+ LOOPBACK_HOSTS2 as LOOPBACK_HOSTS,
10000
11308
  MCP_BIN,
11309
+ NotFoundError,
10001
11310
  PACKAGE_NAME,
11311
+ PAGERDUTY_ENQUEUE_URL,
10002
11312
  PERSONAL,
10003
11313
  PORT_MAP,
10004
11314
  PRIVATE_IP,
10005
11315
  PUSH_SCHEMA_VERSION,
11316
+ PagerDutySink,
10006
11317
  ProviderRegistry,
10007
11318
  RELATION_TO_DIRECTION,
10008
11319
  RuleCheckSchema,
10009
11320
  RulesetSchema,
10010
11321
  SCAN_ARG_PATTERNS,
11322
+ SDL,
10011
11323
  SEVERITY_WEIGHT,
10012
11324
  SHARING_LEVELS,
10013
11325
  ScannerRegistry,
10014
11326
  ScannerShape,
10015
11327
  SharingLevelSchema,
11328
+ SlackSink,
11329
+ SqliteQueryBackend,
10016
11330
  SqliteStoreBackend,
10017
11331
  StdoutSink,
11332
+ TENANT_HEADER,
10018
11333
  VectorStore,
10019
11334
  WebhookSink,
10020
11335
  applyInstall,
10021
11336
  applySharingLevel,
10022
11337
  assertReadOnly,
11338
+ assertSafeBind,
10023
11339
  assertSafeScanArg,
10024
11340
  assignColors,
11341
+ bearerToken,
10025
11342
  bookmarksScanner,
10026
11343
  buildCartographyToolHandlers,
10027
11344
  buildMapData,
11345
+ buildOpenApiDocument,
10028
11346
  buildReport,
10029
11347
  buildSinks,
10030
11348
  centralDbFromEnv,
11349
+ checkBearer,
10031
11350
  checkPrerequisites,
10032
11351
  checkReadOnly,
10033
11352
  clampText,
@@ -10055,10 +11374,12 @@ export {
10055
11374
  createOpenAIProvider,
10056
11375
  createScanRunner,
10057
11376
  createSemanticSearch,
11377
+ createSqliteQueryBackend,
10058
11378
  currentOs,
10059
11379
  cursorDeeplink,
10060
11380
  databasesScanner,
10061
11381
  deepMerge,
11382
+ defaultAllowedHosts,
10062
11383
  defaultConfig,
10063
11384
  defaultContext,
10064
11385
  defaultProviderRegistry,
@@ -10075,6 +11396,7 @@ export {
10075
11396
  evaluateCheck,
10076
11397
  evaluateRule,
10077
11398
  evidenceLine,
11399
+ executeGraphql,
10078
11400
  executeNlQuery,
10079
11401
  exportAll,
10080
11402
  exportBackstageYAML,
@@ -10088,6 +11410,9 @@ export {
10088
11410
  filterBySeverity,
10089
11411
  findAnonViolations,
10090
11412
  formatComplianceText,
11413
+ formatJira,
11414
+ formatPagerDuty,
11415
+ formatSlack,
10091
11416
  generateDependencyMermaid,
10092
11417
  generateDiffMermaid,
10093
11418
  generateTopologyMermaid,
@@ -10095,6 +11420,7 @@ export {
10095
11420
  getRuleset,
10096
11421
  globalId,
10097
11422
  groupByDomain,
11423
+ handleGraphqlGet,
10098
11424
  hexCorners,
10099
11425
  hexDistance,
10100
11426
  hexNeighbors,
@@ -10105,9 +11431,11 @@ export {
10105
11431
  hostname,
10106
11432
  ingestEnvelope,
10107
11433
  installedAppsScanner,
11434
+ isLoopbackHost,
10108
11435
  isPersonalHost,
10109
11436
  isReadOnlyCommand,
10110
11437
  isRemembered,
11438
+ isSecureWebhookUrl,
10111
11439
  k8sScanner,
10112
11440
  keyMetaOf,
10113
11441
  layoutClusters,
@@ -10133,6 +11461,7 @@ export {
10133
11461
  normalizeTenant,
10134
11462
  orgKeyPath,
10135
11463
  osUser,
11464
+ parseApiArgs,
10136
11465
  parseComposeDeps,
10137
11466
  parseConfig,
10138
11467
  parseConnectionString,
@@ -10145,6 +11474,7 @@ export {
10145
11474
  pixelToHex,
10146
11475
  planInstall,
10147
11476
  portsScanner,
11477
+ postJson,
10148
11478
  previewShare,
10149
11479
  pseudonymize,
10150
11480
  pseudonymizeFragment,
@@ -10158,10 +11488,12 @@ export {
10158
11488
  resolveEffectiveLevel,
10159
11489
  resolveNlQuery,
10160
11490
  resolveSharingLevel,
11491
+ resolveTenant,
10161
11492
  revalidateAnonymized,
10162
11493
  reversalKey,
10163
11494
  reversePseudonym,
10164
11495
  rotateOrgKey,
11496
+ runApi,
10165
11497
  runDiscovery,
10166
11498
  runDrift,
10167
11499
  runHttp,
@@ -10184,8 +11516,11 @@ export {
10184
11516
  shareHash,
10185
11517
  splitSegments,
10186
11518
  stableStringify,
11519
+ startApi,
10187
11520
  stripSensitive,
11521
+ timingSafeEqual,
10188
11522
  validateScanner,
10189
- vscodeDeeplink
11523
+ vscodeDeeplink,
11524
+ zodToJsonSchema
10190
11525
  };
10191
11526
  //# sourceMappingURL=index.js.map