@datasynx/agentic-ai-cartography 2.3.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
@@ -156,15 +156,26 @@ var SECURITY_METADATA_KEYS = [
156
156
  var DriftConfigSchema = z.object({
157
157
  minSeverity: z.enum(SEVERITIES).default("info"),
158
158
  sinks: z.array(z.object({
159
- type: z.enum(["stdout", "webhook"]),
159
+ type: z.enum(["stdout", "webhook", "slack", "pagerduty", "jira"]),
160
160
  url: z.string().url().optional(),
161
161
  token: z.string().optional(),
162
- 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()
163
167
  })).default([{ type: "stdout" }])
164
168
  }).superRefine((cfg, ctx) => {
165
169
  for (const [i, s] of cfg.sinks.entries()) {
166
- if (s.type === "webhook" && !s.url) {
167
- 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" });
168
179
  }
169
180
  }
170
181
  });
@@ -1778,8 +1789,17 @@ function stripSensitive(target) {
1778
1789
  const stripped = `${url.hostname}${url.port ? ":" + url.port : ""}`;
1779
1790
  return stripped || raw;
1780
1791
  } catch {
1781
- const stripped = raw.replace(/\/.*$/, "").replace(/\?.*$/, "").replace(/@.*:/, ":");
1782
- 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;
1783
1803
  }
1784
1804
  }
1785
1805
  var SCAN_ARG_PATTERNS = {
@@ -1797,7 +1817,7 @@ function assertSafeScanArg(kind, value) {
1797
1817
  return value;
1798
1818
  }
1799
1819
  function redactSecrets(value) {
1800
- 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:***@");
1801
1821
  }
1802
1822
  function redactValue(value) {
1803
1823
  if (typeof value === "string") return redactSecrets(value);
@@ -4895,6 +4915,41 @@ var StdoutSink = class {
4895
4915
 
4896
4916
  // src/sinks/webhook.ts
4897
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
+ }
4898
4953
  function isSecureWebhookUrl(url, env = process.env) {
4899
4954
  if (env.CARTOGRAPHY_ALLOW_INSECURE_SYNC === "1") return true;
4900
4955
  let parsed;
@@ -4913,59 +4968,177 @@ var WebhookSink = class {
4913
4968
  }
4914
4969
  name = "webhook";
4915
4970
  async emit(alert) {
4916
- if (typeof fetch !== "function") {
4917
- logWarn("webhook sink unavailable: global fetch missing", { sink: this.name });
4918
- return;
4919
- }
4920
4971
  const { url, token, timeoutMs } = this.opts;
4921
- if (!url) {
4922
- logWarn("webhook sink unavailable: no url configured", { sink: this.name });
4923
- return;
4924
- }
4925
- if (!isSecureWebhookUrl(url)) {
4926
- logWarn("webhook sink refused: insecure scheme (use https:// or a loopback host)", {
4927
- sink: this.name,
4928
- host: stripSensitive(url)
4929
- });
4930
- return;
4931
- }
4932
- try {
4933
- const res = await fetch(url, {
4934
- method: "POST",
4935
- headers: {
4936
- "content-type": "application/json",
4937
- ...token ? { authorization: `Bearer ${token}` } : {}
4938
- },
4939
- body: JSON.stringify(redactValue(alert)),
4940
- signal: AbortSignal.timeout(timeoutMs ?? 1e4)
4941
- });
4942
- if (!res.ok) {
4943
- 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
+ }))
4944
5035
  }
4945
- } catch (err) {
4946
- logError("webhook sink failed", {
4947
- sink: this.name,
4948
- host: stripSensitive(url),
4949
- reason: err instanceof Error ? err.message : String(err)
4950
- });
4951
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}` });
4952
5097
  }
4953
5098
  };
4954
5099
 
4955
5100
  // src/sinks/index.ts
4956
5101
  function buildSinks(drift) {
4957
5102
  const configs = drift?.sinks && drift.sinks.length > 0 ? drift.sinks : [{ type: "stdout" }];
5103
+ const envSecret = process.env.CARTOGRAPHY_DRIFT_TOKEN;
4958
5104
  const sinks = [];
4959
5105
  for (const s of configs) {
4960
- if (s.type === "webhook") {
4961
- if (!s.url) continue;
4962
- sinks.push(new WebhookSink({
4963
- url: s.url,
4964
- token: s.token ?? process.env.CARTOGRAPHY_DRIFT_TOKEN,
4965
- timeoutMs: s.timeoutMs
4966
- }));
4967
- } else {
4968
- 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());
4969
5142
  }
4970
5143
  }
4971
5144
  return sinks.length > 0 ? sinks : [new StdoutSink()];
@@ -5374,7 +5547,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5374
5547
 
5375
5548
  // src/mcp/server.ts
5376
5549
  var SERVER_NAME = "cartography";
5377
- var SERVER_VERSION = "2.3.0";
5550
+ var SERVER_VERSION = "2.4.0";
5378
5551
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5379
5552
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5380
5553
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -11130,14 +11303,17 @@ export {
11130
11303
  INGEST_SCHEMA_VERSION,
11131
11304
  IngestEnvelopeSchema,
11132
11305
  InvalidTenantError,
11306
+ JiraSink,
11133
11307
  LOOPBACK_HOSTS2 as LOOPBACK_HOSTS,
11134
11308
  MCP_BIN,
11135
11309
  NotFoundError,
11136
11310
  PACKAGE_NAME,
11311
+ PAGERDUTY_ENQUEUE_URL,
11137
11312
  PERSONAL,
11138
11313
  PORT_MAP,
11139
11314
  PRIVATE_IP,
11140
11315
  PUSH_SCHEMA_VERSION,
11316
+ PagerDutySink,
11141
11317
  ProviderRegistry,
11142
11318
  RELATION_TO_DIRECTION,
11143
11319
  RuleCheckSchema,
@@ -11149,6 +11325,7 @@ export {
11149
11325
  ScannerRegistry,
11150
11326
  ScannerShape,
11151
11327
  SharingLevelSchema,
11328
+ SlackSink,
11152
11329
  SqliteQueryBackend,
11153
11330
  SqliteStoreBackend,
11154
11331
  StdoutSink,
@@ -11233,6 +11410,9 @@ export {
11233
11410
  filterBySeverity,
11234
11411
  findAnonViolations,
11235
11412
  formatComplianceText,
11413
+ formatJira,
11414
+ formatPagerDuty,
11415
+ formatSlack,
11236
11416
  generateDependencyMermaid,
11237
11417
  generateDiffMermaid,
11238
11418
  generateTopologyMermaid,
@@ -11255,6 +11435,7 @@ export {
11255
11435
  isPersonalHost,
11256
11436
  isReadOnlyCommand,
11257
11437
  isRemembered,
11438
+ isSecureWebhookUrl,
11258
11439
  k8sScanner,
11259
11440
  keyMetaOf,
11260
11441
  layoutClusters,
@@ -11293,6 +11474,7 @@ export {
11293
11474
  pixelToHex,
11294
11475
  planInstall,
11295
11476
  portsScanner,
11477
+ postJson,
11296
11478
  previewShare,
11297
11479
  pseudonymize,
11298
11480
  pseudonymizeFragment,